domm.plix.at.atom.xml - sfeed_tests - sfeed tests and RSS and Atom files
(HTM) git clone git://git.codemadness.org/sfeed_tests
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
domm.plix.at.atom.xml (37839B)
---
1 <?xml version="1.0" encoding="us-ascii"?>
2 <feed xmlns="http://www.w3.org/2005/Atom"><title>domm.plix.at</title><author><name>Thomas Klausner</name></author><link href="https://domm.plix.at/index.xml" rel="self"/><id>https://domm.plix.at/index.xml</id><updated>2025-10-06T15:00:00+00:00</updated><generator uri="https://metacpan.org/pod/XML::Atom::SimpleFeed" version="0.905">XML::Atom::SimpleFeed</generator><entry><title>I brain coded a static image gallery in a few hours: snig.pl</title><link href="https://domm.plix.at/perl/2025_10_braincoded_static_image_gallery.html"/><id>https://domm.plix.at/perl/2025_10_braincoded_static_image_gallery.html</id><updated>2025-10-06T15:00:00+00:00</updated><category term="perl"/><summary>For quite some I wanted to write a small static image gallery so I can share my pictures with friends and family. Of course there are a gazillion tools like this, but, well, sometimes I just want to ...</summary><content type="html"><p>For quite some I wanted to write a small static image gallery so I can share my pictures with friends and family. Of course there are a gazillion tools like this, but, well, sometimes I just want to roll my own.</p>
3
4 <p>I took the opportunity during our <a href="/reisen/2025_schwaz.html">stay in Schwaz</a> to take a few hours and hack together <a href="https://snig.plix.at/">snig</a>, the (small | static | simple | stupid | ...) image gallery. <a href="https://snig.plix.at/pub/202509_a_few_days_in_tyrol/">Here</a> you can see the example gallery (showing some of the pictures I took in Schwaz).</p>
5
6 <p>I used the old, well tested technique I call <b>brain coding</b><sup class="footnote"><a href="#fn0">0</a></sup>, where you start with an empty vim buffer and type some code (Perl, <span class="caps">HTML, CSS</span>) until you're happy with the result. It helps to think a bit (aka use your brain) during this process.</p>
7
8 <p>According to my <a href="http://timetracker.plix.at/">timetracker</a> I spend 8h 15min (probably half of it spend fiddling with <span class="caps">CSS...</span>).</p>
9
10 <h3>Installation</h3>
11
12 <p>I used the new <a href="https://perldoc.perl.org/perlclass">Perl class</a> feature, so you'll need at least Perl 5.40 which was released last year and is included in current Debian.</p>
13
14 <p>I prefer <a href="https://metacpan.org/pod/App::cpm">cpm</a> to install <span class="caps">CPAN </span>modules:</p>
15
16 <pre><code>cpm install -g Snig</code></pre>
17
18 <p>I haven't provided a <code>Containerfile</code> yet, but if somebody is interested, drop me a line.</p>
19
20 <p>You can get the raw source code from <a href="https://git.sr.ht/~domm/snig">Source Hut</a> (as I don't want to support the big <span class="caps">LLM </span>vacuum machines formerly known as Git(Hub|Lab)).</p>
21
22 <h3>Example usage</h3>
23
24 <p>You need a folder filled with images (eg <code>some-pictures</code>) and some place where you can host a folder and a bunch of <span class="caps">HTML </span>files.</p>
25
26 <pre><code>ls -la some-pictures/
27 7156 -rw------- 1 domm domm 7322112 Oct 6 09:14 P1370198.JPG
28 7188 -rw------- 1 domm domm 7354880 Oct 6 09:14 P1370208.JPG
29 7204 -rw------- 1 domm domm 7369728 Oct 6 09:14 P1370257.JPG</code></pre>
30
31 <p>Then you do</p>
32
33 <pre><code>snig.pl --input some-pictures --output /var/web/my-server-net/gallery/2025-10-some-pictures --name &quot;Some nice pictures&quot;</code></pre>
34
35 <p>This will:</p>
36
37 <ul>
38 <li>find all <code>jpgs</code> in the folder <code>some-pictures</code></li>
39 <li>copy them into the output folder</li>
40 <li>generate a thumbnail (for use in the list)</li>
41 <li>generate a preview (for use in the detail page)</li>
42 <li>generate a <span class="caps">HTML </span>overview page</li>
43 <li>generate a <span class="caps">HTML </span>detail page for each image, linked to the next/prev image</li>
44 <li>generate a zip archive of the images</li>
45 <li><span class="caps">EXIF </span>rotation hints are used to properly orient the previews</li>
46 <li>images are sorted by the <span class="caps">EXIF </span>timestamp (per default, you could also use mtime)</li>
47 </ul>
48
49 <pre><code>ls -la /var/web/my-server-net/gallery/2025-10-some-pictures
50 -rw-rw-r-- 1 domm domm 1370 Sep 26 21:15 20250914_p1370198.html
51 -rw-rw-r-- 1 domm domm 1370 Sep 26 21:15 20250914_p1370208.html
52 -rw-rw-r-- 1 domm domm 1370 Sep 26 21:15 20250915_p1370253.html
53 -rw-rw-r-- 1 domm domm 171738640 Sep 26 21:12 2025-10-some-pictures.zip
54 -rw-rw-r-- 1 domm domm 3977 Sep 26 21:15 index.html
55 -rw-rw-r-- 1 domm domm 7322112 Sep 26 21:12 orig_20250914_P1370198.JPG
56 -rw-rw-r-- 1 domm domm 7354880 Sep 26 21:12 orig_20250914_P1370208.JPG
57 -rw-rw-r-- 1 domm domm 7686656 Sep 26 21:12 orig_20250915_P1370253.JPG
58 -rw-rw-r-- 1 domm domm 106382 Sep 26 21:12 preview_20250914_P1370198.JPG
59 -rw-rw-r-- 1 domm domm 170346 Sep 26 21:12 preview_20250914_P1370208.JPG
60 -rw-rw-r-- 1 domm domm 133342 Sep 26 21:12 preview_20250915_P1370253.JPG
61 -rw-rw-r-- 1 domm domm 1434 Sep 26 21:15 snig.css
62 -rw-rw-r-- 1 domm domm 5128 Sep 26 21:12 thumbnail_20250914_P1370198.JPG
63 -rw-rw-r-- 1 domm domm 9495 Sep 26 21:12 thumbnail_20250914_P1370208.JPG
64 -rw-rw-r-- 1 domm domm 6408 Sep 26 21:12 thumbnail_20250915_P1370253.JPG</code></pre>
65
66 <p>You can then take the output folder and rsync it onto a static file server. Or install <code>snig</code> on your web server and do the conversion there..</p>
67
68 <p>Again, take a look at the <a href="https://snig.plix.at/pub/202509_a_few_days_in_tyrol/">example gallery</a>.</p>
69
70 <h3>Limitations</h3>
71
72 <p>For now, <code>snig</code> is very simple and stupid, so quite a few things are still hard coded (like copyright and license). If somebody else wants to use it, I will add some more command line flags and/or a config file. But for now this is not needed, so I did not add it.</p>
73
74 <h3>Have fun</h3>
75
76 <p>I had quite some fun hacking <code>snig</code> and also used it as an opportunity to learn the new <code>class</code> syntax (and fresh up on subroutine / method signatures). So not only did I use my brain, I actually learned something new!</p>
77
78 <p>Please feel free to give <code>snig</code> a try. Or just use one of the countless other static image gallery generators. Or just write your own!</p>
79
80 <p><a href="https://lobste.rs/s/bu1a84/i_brain_coded_static_image_gallery_few">lobste.rs</a>, <a href="https://news.ycombinator.com/item?id=45492242">hacker news</a>, <a href="https://www.reddit.com/r/perl/comments/1nzls1e/i_brain_coded_a_static_image_gallery_in_a_few/">reddit</a></p>
81
82 <h3>Footnotes</h3>
83
84 <p class="footnote" id="fn0"><sup>0</sup> As opposed to <a href="https://en.wikipedia.org/wiki/Vibe_coding">vibe coding</a>.</p></content><category term="Perl"/><category term="CPAN"/><category term="fun"/><category term="release"/><category term="open source"/><category term="~/bin"/></entry><entry><title>Bike trip from Schwaz to Wels</title><link href="https://domm.plix.at/reisen/2025_biketrip_schwaz_wels.html"/><id>https://domm.plix.at/reisen/2025_biketrip_schwaz_wels.html</id><updated>2025-09-27T15:46:00+02:00</updated><category term="reisen"/><summary type="html">After spending 1.5 weeks in Schwaz with the family looking after a friends cats and house, I cycled back (not the complete distance to Vienna..)
85
86 Day 1: Schwaz - Waidring
87
88 96.75km, 5:40, ⌀ 17.03, ...</summary><content type="html"><p>After spending 1.5 weeks in Schwaz with the family looking after a friends cats and house, I cycled back (not the complete distance to Vienna..)</p>
89
90 <h3>Day 1: Schwaz - Waidring</h3>
91
92 <p>96.75km, 5:40, ⌀ 17.03, from 11:00 - 17:30</p>
93
94 <p>Along the Inn to Wörgel, then into the "Wilder Kaiser" area, very touristy. I once had to stop a bit because a movie shoot (well, actually for some <span class="caps">ORF</span> Krimi) needed a clean view of the small mountain road I was climbing. Stayed in a nice <span class="caps">B&amp;B </span>and had Blunzn for dinner.</p>
95
96 <h3>Day 2: Waidring - Nußdorf / Attersse</h3>
97
98 <p>112km, 5:56, ⌀ 18.86, from 9:45 to 17:30</p>
99
100 <p>A bit more downhill today, through the very nice area around Lofer and Bad Reichenhall. Got some "fake" Mozartkugeln at a Reber factory shop right before the "border" back into Austria. Drachenwand impressive as always, as is riding through the bike tunnel along Mondsee. Did extra 10km because I could not find a place to sleep. All the <span class="caps">B&amp;B</span>s were "full", but I think they just did not want to prepare a single room for one night...</p>
101
102 <h3>Attersse - Wels</h3>
103
104 <p>72.74km, 3:44, ⌀ 19,41, from 10:00 to 14:15</p>
105
106 <p>Mostly flat, except some detours along Attersee so the "slow" bikes don't block the main road running along the shore (I skipped a few, esp the one to "Berg im Attergau"...). Not so nice landscape between Attersee an Schwanstadt (lots of industry), but then some nice parts along Ager and Traun and through small villages. For some weird reason all three Kebab shops <b>and</b> the big Spar Supermarket next to Wels Railway Station are closed on Saturday, but I found some "Chinese" noodles.</p>
107
108 <h3>Now sitting in the train back to Vienna.</h3>
109
110 <p>A nice trip, though I feel my ass and my left knee.</p>
111
112 <p>I think the next time I do some multi-day long distance cycling I will target ~80km / 4:30h stages, so I can spend some time hacking at fun projects (or work..) for a few hours after cycling.</p>
113
114 <p>I had my <span class="caps">GPS </span>tracker set to 15sec interval, which proved to be quite inaccurate. For example for day 2 the <span class="caps">GPS </span>tracker recorded 104km, but my old-school simple wired bike computer 112km. Next time I'll either not record at all, or use a shorter interval (and hope that this won't kill the battery).</p>
115
116 <p>Much of this route was actually the same as <a href="/reisen/2016_rad_wien_innsbruck.html">this route</a>, only in the other direction...</p></content><category term="bicycle"/></entry><entry><title>Cat and house sitting in Schwaz</title><link href="https://domm.plix.at/reisen/2025_schwaz.html"/><id>https://domm.plix.at/reisen/2025_schwaz.html</id><updated>2025-09-13T12:00:00+02:00</updated><category term="reisen"/><summary>A friend of a friend was doing a lot of long bike trips this year and was looking for people to sit their cats and house. We happily volunteered! We (me, BaHo, Older and Younger Son plus partner) ...</summary><content type="html"><p>A friend of a friend was doing a lot of long bike trips this year and was looking for people to sit their cats and house. We happily volunteered!</p>
117
118 <p>We (me, BaHo, Older and Younger Son plus partner) spend a nice week there, with some hiking (Kellerjoch, Wolfsklamm, Achensee) and even more relaxing. And tending to the cats...</p>
119
120 <p><a href="https://snig.plix.at/pub/202509_a_few_days_in_tyrol/">Here</a> are some more pictures.</p></content></entry><entry><title>Installing DarkPAN Perl modules via GitLab</title><link href="https://domm.plix.at/perl/2025_09_install_darkpan_gitlab.html"/><id>https://domm.plix.at/perl/2025_09_install_darkpan_gitlab.html</id><updated>2025-09-07T15:00:00+00:00</updated><category term="perl"/><summary>This week Farhad and me finally found some time to improve a part of our build pipeline that was nagging me for years. We can now release our DarkPAN modules via CI/CD into a GitLab generic packages ...</summary><content type="html"><p>This week <a href="https://farhad.shahbazi.at/">Farhad</a> and me finally found some time to improve a part of our build pipeline that was nagging me for years. We can now release our DarkPAN modules via CI/CD into a <a href="https://docs.gitlab.com/user/packages/generic_packages/">GitLab generic packages repository</a> and install them from there into our app containers, also via CI/CD pipelines.</p>
121
122 <p>But before we start with the details, a little background:</p>
123
124 <h3>DarkPAN</h3>
125
126 <p><a href="https://www.perl.com/">Perl</a> modules are published to <a href="https://metacpan.org/"><span class="caps">CPAN</span></a>. But not all Perl code goes to <span class="caps">CPAN, </span>and this "dark Perl matter" is called "DarkPAN". You especially don't want to publish the internal code that's running your company, or the set of various helper code that you collect over the years that's not really tied to one app, and also (for whatever reasons) not suitable to be properly release to <span class="caps">CPAN.</span> If you still want to use all the best practices and tools established in the last 30+ years, but in private, you can set up an internal <span class="caps">CPAN</span>-like repository, for example using tools like <a href="https://metacpan.org/dist/Pinto/view/bin/pinto">Pinto</a>. Then you can use standard tools to install your internal dependencies from your internal <span class="caps">CPAN</span>-like repo (which I also call DarkPAN).</p>
127
128 <p>So this is what we did in the last ~10 years:</p>
129
130 <ul>
131 <li>We have a bunch of applications</li>
132 <li>We have a bunch of libraries that are used by those applications</li>
133 <li>When we push a library repo, GitLab will run a CI job that<br />
134 * uses <a href="http://Dist::Zilla">Dist::Zilla</a> to build a proper package for the library<br />
135 * also uses Dist::Zilla to release the package to our custom Pinto</li>
136 <li>When an app needs to use such a library (or a new version), we add it to the apps <code>cpanfile</code></li>
137 <li>And when a new release of the app is built (again via GitLab CI/CD pipeline), we use <a href="https://metacpan.org/dist/App-cpm/view/script/cpm">cpm</a> with a custom <code>resolver</code> to install the dependency from our DarkPAN</li>
138 </ul>
139
140 <p><code>cpm install -g --resolver metadb --resolver 02packages,https://pinto.internal.example.com/stacks/darkpan</code></p>
141
142 <p>This worked quite well.</p>
143
144 <h3>Shared Libs</h3>
145
146 <p>It worked so well that we also used this to release and install what I now call "shared libs" inside monorepos: We have a few monorepos for different projects, where each monorepo contains multiple apps (different <span class="caps">API </span>backends, frontends and anything in between). Inside a monorepo we have code that we want to share between all apps, for example the database abstraction (DBIx::Class). Deploying these shared libs via the above method was working, but not very smoothly, especially when there's a lot of development happening: You had to push a new version of the lib, wait for it to be released, and then bump the version in all the <code>cpanfiles</code> using this library.</p>
147
148 <p>So a few weeks ago I changed our handling of these shared libs. Instead of properly installing them via <code>cpm</code>, I can copy the source code directly into the app container and set <code>PERL5LIB</code> accordingly. This is possible because all of the code is in the same monorepo and thus available in the CI/CD pipeline. (material for another blog post..)</p>
149
150 <p>This hack is not an (easy) option for code that has to be shared between projects. But I wanted to get rid of maintaining a server to host Pinto, especially as we already have GitLab running, which supports a large range of <a href="https://docs.gitlab.com/user/packages/package_registry/">language specific repositories</a>. Unfortunately, Perl/CPAN is not implemented. But they have a "generic" repository, so I tried to use it to solve our problem.</p>
151
152 <h3>Publishing a Perl package to into a GitLab generic packages repository</h3>
153
154 <p>The first step is to publish the freshly build Perl distribution into the GitLab repo. This is easy to do via the GitLab <span class="caps">API </span>and <code>curl</code>. The <span class="caps">API </span>endpoint is not very nice <span class="caps">IMO</span>: <code>api/v4/projects/{project-id}/packages/generic/{name}/{version}/{file}</code>, and I find it a bit weird that you set up a <code>name</code> and <code>version</code> and then add files to it (instead of just uploading a tarball), but whatever.</p>
155
156 <p>In our <code>Makefile</code> we added:</p>
157
158 <pre><code>package_registry := &quot;https://gitlab.example.com/api/v4/projects/foo%2Fbar/packages/generic/$(application_name)&quot;
159 get_version = $(shell ls $(application_name)-*.tar.gz | sed &quot;s/$(application_name)-//; s/\.tar\.gz//&quot;)
160 get_filename = $(shell ls $(application_name)-*.tar.gz)
161 private_token ?= ${CI_JOB_TOKEN}
162
163 .PHONY: build
164 build:
165 dzil authordeps --missing | xargs -r cpm install --global
166 cpm install --global
167 dzil build
168
169 .PHONY: release
170 release:
171 $(eval VERSION := $(get_version))
172 $(eval FILENAME := $(get_filename))
173 curl -L -H &quot;JOB-TOKEN: $(private_token)&quot; \
174 --upload-file &quot;$(FILENAME)&quot; \
175 &quot;$(package_registry)/$(VERSION)/$(FILENAME)&quot;</code></pre>
176
177 <p>We set the <code>package_registry</code> base <span class="caps">URL </span>(note that I use the <span class="caps">URI</span>-escaped string <code>foo%2Fbar</code> to address the project <code>bar</code> in group <code>foo</code>, because I find using the project ID even uglier that escaping the <code>/</code> as <code>%2F</code>). Then we use some shell / sed to "parse" the filename and get the version number (which is set automatically via Dist::Zilla) in <code>build</code>.</p>
178
179 <p>In <code>release</code>, we construct the final <span class="caps">URL, </span>authorize using the <code>CI_JOB_TOKEN</code> and call <code>curl</code> to upload the file.</p>
180
181 <h4>Why are we using a <code>Makefile</code> instead of defining the steps in <code>.gitlab-ci.yml</code>?</h4>
182
183 <ul>
184 <li>I can use the <code>Makefile</code> locally, so I can run whatever steps that are triggered by the pipeline without having to push a change to gitlab (no more <code>force pipeline</code> commits!)</li>
185 <li>We prefer writing <code>Makefile</code> to "programming" in <span class="caps">YAML</span></li>
186 <li>The CI stages are very short: <code>script: - make build release</code></li>
187 </ul>
188
189 <h3>Installing from GitLab</h3>
190
191 <p>Actually installing the Perl distribution from GitLab seemed easy, but GitLab threw a few stumbling blocks in our way.</p>
192
193 <p>My plan was to just take the <span class="caps">URL </span>of the distribution in the GitLab Generic Registry and use that in <code>cpanfile</code> via the nice <code>url</code> addon which allows you to install a <span class="caps">CPAN </span>distribution directly from the given <span class="caps">URL</span>:</p>
194
195 <pre><code>requires 'Internal::Package' =&gt; '1.42', url =&gt; 'https://gitlab.example.com/foo/bar/packages/generic/Internal-Package-1.42.tar.gz';</code></pre>
196
197 <p>But for weird reasons, GitLab does not provide a simple <span class="caps">URL </span>like that, esp not for unauthorized users. Instead you have to call the <span class="caps">API, </span>provide a token and only then you can download the tarball. And there is no way to add a custom header to pass the token in <code>cpanfile</code>.</p>
198
199 <p>After some more reading of the docs, we found that instead of using a header, we can also stuff a deploy token into basic auth using <code>https://user:password@url</code> format, and thus can specify the install <span class="caps">URL </span>like this:</p>
200
201 <pre><code>requires 'Internal::Package' =&gt; '1.42', url =&gt; 'https://deployer:gldt-234lkndfg@gitlab.example.com/foo/bar/packages/generic/Internal-Package-1.42.tar.gz';</code></pre>
202
203 <p>And this works!!</p>
204
205 <p>Well, the <span class="caps">URL </span>(using the actual <span class="caps">API </span>call) in fact looks like this:</p>
206
207 <pre><code>requires 'Internal::Package' =&gt; '1.42', url =&gt;
208 'https://deployer:gldt-234lkndfg@gitlab.example.com/api/v4/projects/foo%2Fbar/packages/generic/Internal-Package/1.42/Internal-Package-1.42.tar.gz/';</code></pre>
209
210 <p>This is not very nice:</p>
211
212 <ul>
213 <li>The token is embedded in the <code>cpanfile</code></li>
214 <li>We have to define the package name three times</li>
215 <li>And we also have to define the version three times, and adapt it correctly every time we release a new version</li>
216 </ul>
217
218 <p>So we continued to improve this in a maybe crazy but perlish way:</p>
219
220 <h3>Dynamic cpanfile</h3>
221
222 <p>One of the nice things of <code>cpanfile</code> (and Perl in general) is that instead of inventing some stupid <span class="caps">DSL </span>to specify your requirements, we just use code:</p>
223
224 <pre><code>requires 'Foo::Bar';</code></pre>
225
226 <p>is actually calling a function somewhere that does stuff.</p>
227
228 <p>So we can run code in <code>cpanfile</code>:</p>
229
230 <pre><code>my @DB = qw(Pg mysql DB2 CSV);
231 my $yolo_db = 'DB::' . $DB[rand @DB];
232 requires $yolo_db;</code></pre>
233
234 <p>The above code is of course crazy, but we can use this power for good and write a nice little wrapper to make depending on our DarkPAN easier.</p>
235
236 <h3>Validad::InstallFromGitlab</h3>
237
238 <p>I wrote a small tool, <code>Validad::InstallFromGitlab</code>, which is configured with the GitLab base <span class="caps">URL, </span>the project name and the token. It exports a function <code>from_gitlab</code>, which takes the name and the version of the distribution and returns the long line that <code>requires</code> needs.</p>
239
240 <p>And because <code>cpanfile</code> is just Perl, we can easily use this module there:</p>
241
242 <pre><code>use Validad::InstallFromGitlab (gitlab =&gt; 'https://gitlab.example.com', project =&gt; 'validad%2fcontainer', auth =&gt; $ENV{DARKPAN_ACCESS});
243 requires from_gitlab( 'Validad::Mailer' =&gt; '1.20250904.144530');
244 requires from_gitlab( 'Accounts::Client' =&gt; '1.20250904.144543');</code></pre>
245
246 <p>I decided to use some rather old but very powerful method to make <code>Validad::InstallFromGitlab</code> easy to use: A custom <code>import()</code> function:</p>
247
248 <pre><code>package Validad::InstallFromGitlab;
249 use v5.40;
250 use Carp qw(croak);
251
252 sub import {
253 my ($class, %args) = @_;
254 if (!$args{gitlab} || !$args{project}) {
255 croak(&quot;gitlab and/or project missing&quot;);
256 }
257
258 my $registry_url = sprintf('%s/api/v4/projects/%s/packages/generic', $args{gitlab}, $args{project});
259 if (my $auth = $args{auth}) {
260 $registry_url =~s{://}{'://'.$auth.'@'}e;
261 }
262
263 my $caller=caller();
264 no strict 'refs';
265 *{&quot;$caller\::from_gitlab&quot;} = sub {
266 my ($module, $version) = @_;
267
268 my $package = $module;
269 $package =~s/::/-/g;
270 my $tarball = $package .'-' . $version . '.tar.gz' ;
271 my $url = join('/',$registry_url, $package, $version, $tarball);
272 return ($module, url =&gt; $url);
273 };
274 }
275
276 1;</code></pre>
277
278 <p><code>import()</code> is called when you use the module in the calling code or <code>cpanfile</code> :-)</p>
279
280 <pre><code>use Validad::InstallFromGitlab (
281 gitlab =&gt; 'https://gitlab.example.com',
282 project =&gt; 'validad%2fcontainer',
283 auth =&gt; $ENV{DARKPAN_ACCESS}
284 );</code></pre>
285
286 <p>The parameters passed to <code>use</code> are passed on to <code>import</code>, where I do some light checking and build the long and cumbersome GitLab url.</p>
287
288 <p>Then I use <code>caller()</code> to get the name of the calling namespace and use a typoglob (been some time since I used that..) to install a function named <code>from_gitlab</code> into the caller.</p>
289
290 <p>This function takes two params, the module name and version, and finally constructs the data needed by require, i.e. the module name, version and the whole gitlab <span class="caps">URL.</span></p>
291
292 <p>So I can now specify my requirements very easily in the apps <code>cpanfiles</code> and still use the GitLab generic package registry to distribute my DarkPAN modules!</p>
293
294 <h3>Installing Validad::InstallFromGitlab</h3>
295
296 <p>But how do I install <code>Validad::InstallFromGitlab</code>?</p>
297
298 <p>I don't. But all of our apps use a shared base container (which also helps to keep container size down). And in the Containerfile of the base container, I copy <code>Validad::InstallFromGitlab</code> to a well-known location <code>/opt/perl/darkpan</code> and load it from there via <code>PERL5LIB</code>:</p>
299
300 <pre><code>RUN mkdir -p /opt/perl/darkpan/Validad/
301 COPY Validad-InstallFromGitlab.pm /opt/perl/darkpan/Validad/InstallFromGitlab.pm
302
303 ONBUILD ARG DARKPAN_ACCESS
304 ONBUILD RUN PERL5LIB=/opt/perl/darkpan/ \
305 /opt/perl/bin/cpm install --cpanfile cpanfile \
306 --show-build-log-on-failure -g</code></pre>
307
308 <p>But again that's material for another blog post...</p></content><category term="Perl"/><category term="gitlab"/><category term="DevOps"/><category term="CI"/><category term="container"/><category term="CPAN"/></entry><entry><title>Koha Hackfest 2025 in Marseille</title><link href="https://domm.plix.at/perl/2025_04_koha_hackfest.html"/><id>https://domm.plix.at/perl/2025_04_koha_hackfest.html</id><updated>2025-04-04T10:00:00+00:00</updated><category term="perl"/><summary type="html">I'm currently sitting in a TGV doing 300km/h from Marseille to Paris, traveling back home from the Koha Hackfest, hosted by BibLibre.
309
310 Results
311
312 This year I did a lot of QA, which means reviewing ...</summary><content type="html"><p>I'm currently sitting in a <span class="caps">TGV </span>doing 300km/h from Marseille to Paris, traveling back home from the <a href="https://koha-community.org/">Koha</a> Hackfest, hosted by <a href="https://www.biblibre.com/">BibLibre</a>.</p>
313
314 <h3>Results</h3>
315
316 <p>This year I did a lot of <span class="caps">QA, </span>which means reviewing patches, running their test plan, verifying that everything works and finally signing off the patches and marking the bug as "Passed QA". The process is documented in <a href="https://wiki.koha-community.org/wiki/How_to_QA">the wiki</a>. According to the <a href="https://scoreboard.koha-community.org/">scoreboard</a> I <span class="caps">QA'</span>ed 8 bugs (the second highest number!). After the third or fourth time I did not even have to look up all the steps anymore.</p>
317
318 <p>I moderated a short panel on ElasticSearch, because I found some weird behaviors on which I needed feedback from the experts. This resulted in a bunch of new "bugs" (Koha speak for issues, in this case a mix of actual bugs an feature requests): <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39493">39494</a>, <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39548">39548</a>, <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39549">39549</a>, <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39551">39551</a>, <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39552">39552</a>.</p>
319
320 <p>I did a rather detailed review of <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=37020">37020 - bulkmarcimport gets killed when inserting large files</a>. The problem here is that the current code uses <span class="caps">MARC</span>::Batch, which does some horrible regex "parsing" of <span class="caps">XML </span>to implement a stream parser (so it can handle large files without using <span class="caps">ALL </span>the <span class="caps">RAM</span>) (see more details at the end of this post). But a <a href="https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=37478">recent change</a> added a check-step which validates the records and puts the valid ones onto an Perl array. Which now again takes up <span class="caps">ALL </span>the <span class="caps">RAM.</span> I reviewed the two proposed patches, but I think we should use <span class="caps">XML</span>::LibXML::Reader directly, which should result in cleaner, faster, less-RAM-using and correct code.</p>
321
322 <p>I also participated in various other discussions and hope to have provided some helpful ideas &amp; feedback from my still semi-external Koha perspective and semi-extensive knowledge of other environments and projects (I have been doing this "web dev" stuff for quite some time now..).</p>
323
324 <p>After help Clemens setup <span class="caps">L10N </span>on his <span class="caps">KTD </span>setup, I submitted a <a href="https://gitlab.com/koha-community/koha-testing-docker/-/merge_requests/540">doc patch</a> to <span class="caps">KTD </span>explaining the <code>SKIP_L10N</code> setup and hopefully making the general <span class="caps">L10N </span>setup a bit clearer. I generally try to improve the docs if I hit a problem and was able to fix it. Give it a try the next time, it's very rewarding!</p>
325
326 <p>I could also provide some Perl help to various other attendees. But I still failed most of the questions of joubus <a href="https://2025-hackfest-perl-quizz-e8dbca.gitlab.io/perl_quizz.html">Perl quiz</a>. My excuse is that I trained my brain on writing only good/sane/nice Perl so that I forgot how to parse all the <a href="https://domm.plix.at/perl/space_invaders.pl.txt">weird corner cases</a>...</p>
327
328 <h3>Social</h3>
329
330 <p>But the Hackfest is not only about hacking, there's also the "fest" part (or party?). I really enjoyed hanging out with the other attendees on the terrace during lunch in the sun. The food was as usual excellent and not too unhealthy (of course depending on how much cheese one is able to stack onto his plate). The evenings at various bars and restaurants where fun and entertaining (even though I did manage to go to bed early enough this year, and hardly had any alcohol).</p>
331
332 <p>I did not do any sightseeing or even just walking around Marseille this year. I blame the fact that our hotel was very near to the venue and most of the after-hack locations. And I didn't bring my swimming trunks so I was not motivated to go to the beach (but I've ticked that off last year..)</p>
333
334 <p>I had a lot of nice chats with old and new friends on topics ranging from the obvious (A.I., the sorry state of the world, Koha, Perl) to the obscure (US garbage collection trucks, the lifetime of ropes for hand-pulled elevators up to Greek monasteries, sweet potato heritage of Aotearoa, chicken egg sizes, anarcho-syndicalism, ...)</p>
335
336 <h3>Thanks</h3>
337
338 <p>Thanks to BibLibre and Paul Poulain for organizing the event, and to all the attendees for making it such a wonderful 4 days!</p>
339
340
341 <h4>Postscript: The horrors of <span class="caps">MARC</span>::Batch</h4>
342
343 <p>So, how does <a href="https://metacpan.org/pod/MARC::Batch"><span class="caps">MARC</span>::Batch</a> handle importing huge <span class="caps">XML </span>files without using too much <span class="caps">RAM</span>?</p>
344
345 <p>By breaking the first rule of <span class="caps">XML </span>handling: It "parses" the <span class="caps">XML </span>via regex!</p>
346
347 <p>This is actually implemented in <a href="https://metacpan.org/pod/MARC::File::XML"><span class="caps">MARC</span>::File::XML</a>, namely <a href="https://metacpan.org/dist/MARC-File-XML/source/lib/MARC/File/XML.pm#L385">here</a>. If you have a strong stomach I'll wait for you to take a look at that code.</p>
348
349 <p>Here are some "highlights":</p>
350
351 <pre><code> ## get a chunk of xml for a record
352 local $/ = 'record&gt;';
353 my $xml = &lt;$fh&gt;;</code></pre>
354
355 <p>Set the <code>input record separator</code> (usually newline <code>\n</code>, and telling Perl what it should consider a line) to the string <code>record&gt;</code> so, basically something which looks like and <span class="caps">XML </span>tag ending with record. It is <span class="caps">NOT </span>including the start <code>&lt;</code> because the code wants to ignore <span class="caps">XML </span>namespaces.</p>
356
357 <p>The it uses <code>&lt;$fh&gt;</code> to read the next "line" from the record, which isn't a line in any usual sense, but all bytes up to the next occurrence of <code>record&gt;</code>.</p>
358
359 <pre><code> ## do we have enough?
360 $xml .= &lt;$fh&gt; if $xml !~ m!&lt;/([^:]+:){0,1}record&gt;$!;</code></pre>
361
362 <p>It continues reading until it find something that looks like a closing <code>&lt;/record&gt;</code> tag (which might contain a namespace). Then some more "cleanup", and finally the xml chunk is returned.</p>
363
364 <p>Obviously this works, as it is used by thousands of libraries around the world on millions of records all the time.</p>
365
366