{"version":"https://jsonfeed.org/version/1.1","title":"Vic Demuzere","home_page_url":"https://vic.demuzere.be/articles/","feed_url":"https://vic.demuzere.be/articles/feed.json","description":"Blog posts written by Vic Demuzere","language":"en","items":[{"id":"https://vic.demuzere.be/articles/bots-crawlers-and-geoblocks/","content_html":"<p>The last few years, web traffic from bots has increased rapidly.\nSome sources<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup> claim that automated activity now accounts for over half of all web traffic.\nThis is even more pronounced on small websites like mine, as I don't receive many human visitors.</p>\n<p>The table below shows traffic on this website for the last few days:</p>\n<table>\n<thead>\n<tr>\n<th align=\"center\"></th>\n<th>Path</th>\n<th align=\"right\">Hits</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td align=\"center\">1</td>\n<td>/wp-login.php</td>\n<td align=\"right\">2751</td>\n</tr>\n<tr>\n<td align=\"center\">2</td>\n<td>/wp-admin/index.php</td>\n<td align=\"right\">2707</td>\n</tr>\n<tr>\n<td align=\"center\">3</td>\n<td>/wp-admin/edit.php</td>\n<td align=\"right\">2354</td>\n</tr>\n<tr>\n<td align=\"center\">4</td>\n<td>/wp-admin/profile.php</td>\n<td align=\"right\">2353</td>\n</tr>\n<tr>\n<td align=\"center\">5</td>\n<td>/wp-admin/plugins.php</td>\n<td align=\"right\">2351</td>\n</tr>\n<tr>\n<td align=\"center\">6</td>\n<td>/robots.txt</td>\n<td align=\"right\">1131</td>\n</tr>\n<tr>\n<td align=\"center\">7</td>\n<td>/</td>\n<td align=\"right\">994</td>\n</tr>\n<tr>\n<td align=\"center\">8</td>\n<td>/articles/feed.rss</td>\n<td align=\"right\">989</td>\n</tr>\n<tr>\n<td align=\"center\">9</td>\n<td>/articles/how-i-almost-lost-my-backups/</td>\n<td align=\"right\">314</td>\n</tr>\n<tr>\n<td align=\"center\">10</td>\n<td>/wp-content/themes/vic/style.css</td>\n<td align=\"right\">299</td>\n</tr>\n</tbody>\n</table>\n<p>This clearly shows two things:</p>\n<ul>\n<li>The first useful page for a real visitor is in sixth place<sup class=\"footnote-ref\"><a href=\"#fn-2\" id=\"fnref-2\" data-footnote-ref>2</a></sup>.</li>\n<li>My stylesheet has a low number of hits compared to the frontpage and my most recently published article.\nOne would expect each unique visitor to download the stylesheet at least once.</li>\n</ul>\n<p>That is to say, my own statistics show that I'm mainly serving our robot overlords.</p>\n<p>The situation is worse on other kinds of web apps, like my <a href=\"https://forgejo.org/\" title=\"Forgejo is a self-hosted lightweight software forge.\">Forgejo</a> instance.\nIt's a single-user instance, and most projects are private, but I do publish a bunch of open-source projects there.\nRecent bot traffic on applications like these is causing significant load on my server.</p>\n<h2 id=\"keeping-the-bots-out\">Keeping the bots out<a href=\"#keeping-the-bots-out\" aria-label=\"Link to heading 'Keeping the bots out'\" data-heading-content=\"Keeping the bots out\" class=\"anchor\"></a></h2>\n<p>Previously, the majority of crawlers were relatively decent.\nThey would crawl your website slowly, making sure not to cause too much load.\nTheir main purpose was serving search results to people looking for the content you publish.\nBringing down your website would only result in their own users landing on error pages.</p>\n<p>The recent <a href=\"https://en.wikipedia.org/wiki/Generative_AI\" title=\"Generative AI on Wikipedia\">Generative AI</a> (GenAI) craze changed this completely.\nMultiple companies are now racing to collect as much data as possible.\nThey no longer care what happens with the websites they're crawling.\nOnce they have collected enough content, their GenAI agents provide users with answers directly.\nThe source website is treated as irrelevant.\nObviously, as a website owner, I do not want these thankless robots on my property<sup class=\"footnote-ref\"><a href=\"#fn-3\" id=\"fnref-3\" data-footnote-ref>3</a></sup>!</p>\n<p>So I went through the standard ways of blocking them:</p>\n<ol>\n<li>Asking nicely in <a href=\"https://www.robotstxt.org/\" title=\"The Web Robots Pages\"><code>robots.txt</code></a>.</li>\n<li>Denying requests based on the <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent\" title=\"User-Agent HTTP header on Mozilla Developer Network\">User-Agent</a> header.</li>\n<li>Blocking IP ranges from known data centers and cloud providers.</li>\n</ol>\n<p>None of these methods work.</p>\n<p>GenAI companies frequently ignore <code>robots.txt</code> files.\nThey also usually disguise themselves as normal browsers instead of identifying as crawlers.\nMore recently, I've also noticed them increasingly using consumer IP space.\nCombined with rapidly changing addresses, this makes it impossible to block them by IP address alone.</p>\n<p>There are some promising new tools like <a href=\"https://anubis.techaro.lol/\" title=\"Anubis\">Anubis</a> that work<sup class=\"footnote-ref\"><a href=\"#fn-4\" id=\"fnref-4\" data-footnote-ref>4</a></sup>, for now.</p>\n<h2 id=\"giving-up\">Giving up<a href=\"#giving-up\" aria-label=\"Link to heading 'Giving up'\" data-heading-content=\"Giving up\" class=\"anchor\"></a></h2>\n<p>Making sure websites stay online is part of my job.\nI don't want to have to go through the same hassle for my own web apps when I get home.\nI'm tired, and the few things I'm hosting just aren't worth it.</p>\n<p>So, I decided to take two different approaches:</p>\n<ol>\n<li>\n<p>Websites that are <strong>meant to be public</strong>, like the one you're reading now.\nI've always optimized these for high traffic, and will continue to do so.\nThese are built to withstand much more than the AI crawlers I'm currently seeing.\n<strong>I'm no longer blocking any bots on public websites.</strong></p>\n</li>\n<li>\n<p>Websites (and web apps) that are <strong>primarily meant for myself</strong>.\nThese are <strong>now geoblocked</strong> and only accept traffic from Belgium.\nAs these are mainly open source applications that I don't maintain myself, they are harder to optimize.\nThis includes my <a href=\"https://forgejo.org/\" title=\"Forgejo is a self-hosted lightweight software forge.\">Forgejo</a> instance.\nIt's sad that part of my open source code disappears behind a firewall.\nI hope to open it up again once AI companies start behaving more responsibly.</p>\n</li>\n</ol>\n<p>My geoblock page contains contact information.\nSo if people <em>really want to go through the hassle</em> of requesting access (which I very much doubt), they can be allowed in.</p>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-1\">\n<p>Every article I've found claiming this reads like marketing material for a web security firm.\nFor that reason, I decided not to link to any of them.\nBased on my own traffic stats though, I don't doubt their numbers. <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n<li id=\"fn-2\">\n<p>I do read robots.txt on some sites I visit.\nIt's a treasure trove of information about the website you're visiting.\nBut that's probably not normal human behaviour. <a href=\"#fnref-2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n<li id=\"fn-3\">\n<p>I don't really mind AI crawlers, as long as they behave.\nThey usually don't. <a href=\"#fnref-3\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"3\" aria-label=\"Back to reference 3\">↩</a></p>\n</li>\n<li id=\"fn-4\">\n<p>Anubis is great!\nEspecially since it lets through the bots that clearly identify as a bot. <a href=\"#fnref-4\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"4\" aria-label=\"Back to reference 4\">↩</a></p>\n</li>\n</ol>\n</section>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Bots%20are%20dominating%20the%20web\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/bots-crawlers-and-geoblocks/","date_published":"2026-05-14T13:41:51Z","date_modified":"2026-05-14T13:41:51Z","language":"en"},{"id":"https://vic.demuzere.be/articles/how-i-almost-lost-my-backups/","content_html":"<p>My backup strategy follows the <a href=\"https://www.backblaze.com/blog/the-3-2-1-backup-strategy/\" title=\"Backblaze on the 3-2-1 backup rule\">3-2-1 backup rule</a>:</p>\n<ul>\n<li><strong>Three</strong> copies of your data</li>\n<li>On <strong>two</strong> different media</li>\n<li><strong>One</strong> copy off-site</li>\n</ul>\n<p>Some people argue that your production data (the copy stored on your laptop, for example) counts as one of those copies.\nI disagree.\nWhile it may be a copy of your <em>current</em> data, it's not a copy of your historical data.\nIf I need to recover a file that I modified or deleted 2 years ago, my laptop won't have it.</p>\n<h2 id=\"device-backups\">Device backups<a href=\"#device-backups\" aria-label=\"Link to heading 'Device backups'\" data-heading-content=\"Device backups\" class=\"anchor\"></a></h2>\n<p>I use <strong><a href=\"https://restic.net/\" title=\"Restic\">Restic</a></strong> to write encrypted backups to a 2.5&quot; SSD inside an external USB enclosure.\nThis disk also contains a few scripts to automate the process.\nFriday is backup day: connect the disk, run the script, done.</p>\n<p>Using <code>rsync</code>, I make copies of this master disk:</p>\n<ul>\n<li>\n<p>To another USB 2.5&quot; hard drive located at the office.\nThis is a physical copy I can easily access if my house burns down.\nRestic's encryption is vital here, as coworkers have access to the area where this disk is kept.</p>\n</li>\n<li>\n<p>To a dedicated server at <a href=\"https://www.hetzner.com/sb/\" title=\"Refurbished dedicated servers at Hetzner\">Hetzner</a>.\nA few friends have SSH access<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup> to the account that stores this copy.\nThis provides another &quot;worst-case scenario&quot; path to my data.</p>\n</li>\n</ul>\n<p>Every few months I run a validation script on my Hetzner server.\nThis executes <a href=\"https://restic.readthedocs.io/en/latest/045_working_with_repos.html#checking-integrity-and-consistency\" title=\"Restic: checking integrity and consistency\">Restic with the <code>check --read-data</code></a> subcommand on the copy stored there.\nA few days later, I know whether the data is still correct.\nWhile not exactly the same as a full restore test, it gives me some piece of mind.</p>\n<h3 id=\"additional-backup-for-documents\">Additional backup for documents<a href=\"#additional-backup-for-documents\" aria-label=\"Link to heading 'Additional backup for documents'\" data-heading-content=\"Additional backup for documents\" class=\"anchor\"></a></h3>\n<p>Important folders on my desktop and laptop are synced using <a href=\"https://syncthing.net/\" title=\"Syncthing\">Syncthing</a>.\nIn addition to my personal devices, these also replicate to one of my servers.\nFrom there, an hourly <a href=\"https://restic.net/\" title=\"Restic\">Restic</a> snapshot is made to <a href=\"https://www.backblaze.com/cloud-storage\" title=\"Backblaze B2 cloud storage\">Backblaze B2</a>.\nIn case my 3-2-1 drives fail, I have one last chance to recovery my important documents here.</p>\n<h2 id=\"post-mortem\">Post mortem<a href=\"#post-mortem\" aria-label=\"Link to heading 'Post mortem'\" data-heading-content=\"Post mortem\" class=\"anchor\"></a></h2>\n<p>I was assured my backup scheme was solid, until it wasn't.</p>\n<p>Suddenly, the data validation on the Hetzner server failed.<sup class=\"footnote-ref\"><a href=\"#fn-2\" id=\"fnref-2\" data-footnote-ref>2</a></sup>\nRestic saves backups as <a href=\"https://en.wikipedia.org/wiki/Data_deduplication\" title=\"Data Deduplication on Wikipedia\">deduplicated</a> chunks in what it calls &quot;pack files.&quot;\nSeveral of these files no longer matched their checksums.\nSince pack files are named after their SHA-256 hash, I checked them again manually.\nSadly, <code>sha256sum</code> confirmed the worst: the data in these files was not what it was supposed to be.</p>\n<p>This server has two 4TB disks in RAID 1.\nI assumed there was a hardware issue with the server, but everything looked fine.\nThe other services on the server ran fine, the disks reported no errors, and the RAID was healthy.</p>\n<p>Then I realized: <em>the Hetzner server was not the source of corruption</em>.\nChecking the pack files on my USB disk quickly revealed the truth.\n<strong>These files were also corrupt</strong>, and <code>rsync</code> had dutifully <strong>replicated the corrupted data to my server</strong>.</p>\n<p>It quickly became obvious what was happening.\nDuring additional checks on my USB disk, my laptop started reporting I/O errors.\nThe SSD in this enclosure has been failing silently for a while, returning random data instead of my files.\nI hadn't noticed because I rarely <em>use</em> these backups.\nThe data is usually only read by rsync to push it to the server.</p>\n<p>An hour later, the SSD died completely.</p>\n<p>Two days later, when I was back at the office, I checked the second USB drive.\nI had little hope.\nI usually sync both the Hetzner server and this disk at the same time.\nAs expected, the same errors were present.</p>\n<p><strong>I had managed to corrupt all three copies of my backups simultaneously.</strong></p>\n<h2 id=\"recovery\">Recovery<a href=\"#recovery\" aria-label=\"Link to heading 'Recovery'\" data-heading-content=\"Recovery\" class=\"anchor\"></a></h2>\n<p>There is no magic recovery procedure here.\nIf all copies of a piece of data are gone, they are gone forever.\nRegrettably, I did lose some data that day.</p>\n<p>However, Restic was <a href=\"https://restic.readthedocs.io/en/latest/077_troubleshooting.html\">quite helpful</a> during the recovery process:</p>\n<ul>\n<li>\n<p>A nice side-effect of the data deduplication is that <strong>you can &quot;import&quot; files again</strong>.\nBy creating a temporary snapshot with files gathered from other locations (like old laptop folders or emails), Restic recreates the deduplicated chunks.\nThis makes those files usable again in the older, previously &quot;corrupt&quot; snapshots!</p>\n</li>\n<li>\n<p>Dry-running <code>restic repair snapshots --forget</code> listed exactly which files would be removed due to missing data.\nUse this as a to-do list to gather missing files.</p>\n</li>\n<li>\n<p>Part of the missing data turned out to be logfiles I didn't care about.\nPurging these from snapshots made it a lot easier to identify which important files were actually missing.<sup class=\"footnote-ref\"><a href=\"#fn-3\" id=\"fnref-3\" data-footnote-ref>3</a></sup></p>\n</li>\n</ul>\n<p>In the end, I lost only three possibly important files: pictures.\nSince the data is gone, I don't know exactly what was in them, but I know which albums they belonged to.\nMy backup disk now contains a text file listing those three missing filenames as a permanent reminder.</p>\n<h2 id=\"preventing-this-in-the-future\">Preventing this in the future<a href=\"#preventing-this-in-the-future\" aria-label=\"Link to heading 'Preventing this in the future'\" data-heading-content=\"Preventing this in the future\" class=\"anchor\"></a></h2>\n<p>Clearly, I needed safeguards.\nI made two specific changes to the script I use to sync backups from my local USB SSD to the other destinations:</p>\n<ol>\n<li>\n<p>Before executing <code>rsync</code>, the script now runs <code>restic check</code>.\nWhile a full <code>--read-data</code> takes too long for a weekly routine, a basic check catches structural errors.\nBefore my previous SSD died, the basic check also found errors.\nI'm not sure it would have catched the SSD returning small bits of random data, though.</p>\n</li>\n<li>\n<p>Except for the <code>locks</code> directory, restic never overwrites files.\nIt only creates new ones.\nOld files stay until you run <code>restic forget</code> to clean up your repository.\nI now run <code>rsync</code> with the <code>--ignore-existing</code> flag, as there is no reason to overwrite remote files.</p>\n</li>\n</ol>\n<p>I'm confident that this would prevent data loss if a drive fails silently again.</p>\n<p>Once <a href=\"https://www.techpowerup.com/345260/hdd-prices-increase-by-an-average-of-46-over-the-past-four-months\" title=\"Random article about disk prices increases\">hard drive prices stabilize</a>, I'll likely buy another external drive for a yearly &quot;cold&quot; archive.\nThe hardest files to replace where those that have been removed from my devices years ago.\nHaving an additional offline drive with an old copy would allow me to recover those files.</p>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-1\">\n<p>Not all of them know about it though. <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n<li id=\"fn-2\">\n<p>I would have never discovered about this if it wasn't for this regular data check.\nBackups are not backups if you don't check them. <a href=\"#fnref-2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n<li id=\"fn-3\">\n<p>Figuring out what data I had lost took a few days and I didn't really take notes.\nDocumenting which commands I used here would have been the most interesting part of the article.\nOh well.. <a href=\"#fnref-3\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"3\" aria-label=\"Back to reference 3\">↩</a></p>\n</li>\n</ol>\n</section>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20How%20I%20almost%20lost%20my%20backups\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/how-i-almost-lost-my-backups/","date_published":"2026-05-07T16:08:09Z","date_modified":"2026-05-07T16:08:09Z","language":"en"},{"id":"https://vic.demuzere.be/articles/generating-uuids-in-sqlite/","content_html":"<p>SQLite does not have built-in UUID functions, but by getting creative with the <code>randomblob(N)</code> function we can generate version 4 (random) UUIDs.</p>\n<p>Before we continue, it's important to note that you should verify whether SQLite's random functions are adequate for your usecase.\nThey may not return truly random data.\nHowever, as <a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-format\" title=\"RFC 9562 section 4\">the RFC</a> notes, &quot;they (UUIDs) MUST NOT be used as security capabilities&quot;.\nAnd since we all follow that to the letter, it doesn't really matter.. right?</p>\n<p>The <a href=\"https://sqlite.org/lang_corefunc.html#randomblob\" title=\"randomblob(N) function at SQLite manual\">excellent SQLite manual</a> gives us a starting point:</p>\n<blockquote>\n<p>Hint: applications can generate globally unique identifiers using this function together with hex() and/or lower().</p>\n</blockquote>\n<p>Combining the <code>randomblob(N)</code> function with <code>hex(N)</code> does indeed create something that vaguely resembles a UUID.\nFor some purposes this may suffice, but the result isn't actually a valid UUID<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup>:\nvalid UUIDs must have <a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-format\" title=\"RFC 9562 section 4\">specific bits set</a> to indicate their version and variant.</p>\n<p>We can do better!</p>\n<h2 id=\"as-binary-blob\">As binary blob<a href=\"#as-binary-blob\" aria-label=\"Link to heading 'As binary blob'\" data-heading-content=\"As binary blob\" class=\"anchor\"></a></h2>\n<p>The binary representation of a version 4 UUID consists of:</p>\n<ul>\n<li>48 bits (6 bytes) random data</li>\n<li>4 bits (0.5 bytes) <a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-version-field\" title=\"RFC 9562 section 4.2: Version Field\">version field</a></li>\n<li>6 bits (1.5 bytes) random data</li>\n<li>4 bits (0.5 bytes) <a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-variant-field\" title=\"RFC 9562 section 4.1: Variant Field\">variant field</a></li>\n<li>60 bits (7.5 bytes) random data</li>\n</ul>\n<p>As far as I know, we can't modify individual bytes in a blob using the available functions in SQLite.\nHowever, we do have access to a lot of string functions.\nTaking a detour using HEX encoding provides a solution.\nWe'll first concatenate a HEX string containing the data we want, then convert it back to a binary blob.\nNote that each byte is represented by two hexadecimal characters.</p>\n<p>The first 48 random bits are easy:</p>\n<pre><code class=\"language-sqlite\">sqlite&gt; select HEX(RANDOMBLOB(6));\r\n4004714237AF\r\n</code></pre>\n<p>The first 4 bits of the next byte need to be set to <code>0100</code> (or <code>4</code> in hex).\nAs every byte is represented by two characters in hexadecimal, we can just use a literal <code>4</code> for the next character.</p>\n<pre><code class=\"language-sqlite\">sqlite&gt; select '4';\r\n4\r\n</code></pre>\n<p>Next, we need 6 bits of random data.\nWe can only generate random bytes using <code>RANDOMBLOB(N)</code>, but we need 1.5 bytes.\nLuckily, because we are working with a hexadecimal string, we can generate 2 bytes (4 hex characters) and take the first three characters.</p>\n<pre><code>sqlite&gt; select HEX(RANDOMBLOB(2));\r\n7B4A\r\n\r\nsqlite&gt; select SUBSTR(HEX(RANDOMBLOB(2)),0,4);\r\n5BD\r\n</code></pre>\n<p>The next one is a little more complex.\nThe <a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-variant-field\" title=\"RFC 9562 section 4.1: Variant Field\">variant field</a> consists of a variable number of bits.\nFor our version 4 UUID, we need to set the first two bits to <code>10</code>, the others can be random.\nThat gives us a possible range between <code>1000</code> and <code>1011</code>, or a hexadecimal character between <code>8</code> and <code>B</code> inclusive.</p>\n<pre><code>select FORMAT('%X', 8 + ABS(RANDOM() % 4));\r\nA\r\n</code></pre>\n<p>After this, we need 60 more random bits.\nSince this again includes a half byte again (7.5 bytes), we're doing the same <code>SUBSTR</code> trick as before:</p>\n<pre><code>sqlite&gt; select SUBSTR(HEX(RANDOMBLOB(8)),0,16);\r\nE83EF2136842130\r\n</code></pre>\n<h3 id=\"stitching-it-together\">Stitching it together<a href=\"#stitching-it-together\" aria-label=\"Link to heading 'Stitching it together'\" data-heading-content=\"Stitching it together\" class=\"anchor\"></a></h3>\n<p>Using <code>CONCAT(X, ...)</code> we create a single string:</p>\n<pre><code>sqlite&gt; select CONCAT(\r\n(x1...&gt;   HEX(RANDOMBLOB(6)),\r\n(x1...&gt;   '4',\r\n(x1...&gt;   SUBSTR(HEX(RANDOMBLOB(2)),0,4),\r\n(x1...&gt;   FORMAT('%X', 8 + ABS(RANDOM() % 4)),\r\n(x1...&gt;   SUBSTR(HEX(RANDOMBLOB(8)),0,16)\r\n(x1...&gt; );\r\n\r\n64DF281823BE47249BB2CA8D6450066B\r\n</code></pre>\n<p>Then use <code>UNHEX(X)</code> to convert it back into a binary blob:</p>\n<pre><code>select UNHEX(CONCAT(\r\n  HEX(RANDOMBLOB(6)),\r\n  '4',\r\n  SUBSTR(HEX(RANDOMBLOB(2)),0,4),\r\n  FORMAT('%X', 8 + ABS(RANDOM() % 4)),\r\n  SUBSTR(HEX(RANDOMBLOB(8)),0,16)\r\n));\r\n</code></pre>\n<h3 id=\"generating-uuids-automatically\">Generating UUIDs automatically<a href=\"#generating-uuids-automatically\" aria-label=\"Link to heading 'Generating UUIDs automatically'\" data-heading-content=\"Generating UUIDs automatically\" class=\"anchor\"></a></h3>\n<p>The SQL above can be used as the default value for a blob column:</p>\n<pre><code>CREATE TABLE &quot;animals&quot; (\r\n  &quot;id&quot; INTEGER PRIMARY KEY NOT NULL,\r\n  &quot;uuid&quot; BLOB UNIQUE NOT NULL\r\n    DEFAULT (UNHEX(CONCAT(\r\n      HEX(RANDOMBLOB(6)), '4',\r\n      SUBSTR(HEX(RANDOMBLOB(2)),0,4),\r\n      FORMAT('%X', 8 + ABS(RANDOM() % 4)),\r\n      SUBSTR(HEX(RANDOMBLOB(8)),0,16))))\r\n    UNIQUE CHECK(LENGTH(&quot;uuid&quot;) = 16),\r\n   &quot;name&quot; TEXT NOT NULL\r\n) STRICT;\r\n</code></pre>\n<p>This will generate a UUID if we don't specify one on insert:</p>\n<pre><code>sqlite&gt; INSERT INTO &quot;animals&quot; (&quot;name&quot;) VALUES ('Fluffy');\r\n\r\nsqlite&gt; SELECT * FROM &quot;animals&quot;;\r\n1|[blob]|Fluffy\r\n\r\nsqlite&gt; SELECT &quot;id&quot;, HEX(&quot;uuid&quot;), &quot;name&quot; FROM &quot;animals&quot;;\r\n1|85671F7120F14AA6802B8B4E9213B8BE|Fluffy\r\n</code></pre>\n<h2 id=\"in-human-readable-hexadecimal-form\">In human readable (hexadecimal) form<a href=\"#in-human-readable-hexadecimal-form\" aria-label=\"Link to heading 'In human readable (hexadecimal) form'\" data-heading-content=\"In human readable (hexadecimal) form\" class=\"anchor\"></a></h2>\n<p>Depending on your use case, you <a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-dbms-and-database-considera\" title=\"RFC 9562 section 6.13: Database Considerations\">may want to store UUIDs in a <code>TEXT</code> column</a>.</p>\n<p>We were close to a &quot;hex-and-dash&quot; string format with the SQL above, until we call the <code>UNHEX(X)</code> function.\n<a href=\"https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-format\" title=\"RFC 9562 section 4\">RFC 9562</a> specifies that dashes are required, so we'll add them in the right places.</p>\n<pre><code>sqlite&gt; select CONCAT(\r\n  HEX(RANDOMBLOB(4)),\r\n  '-',\r\n  HEX(RANDOMBLOB(2)),\r\n  '-',\r\n  '4',\r\n  SUBSTR(HEX(RANDOMBLOB(2)),0,4),\r\n  '-',\r\n  FORMAT('%X', 8 + ABS(RANDOM() % 4)),\r\n  SUBSTR(HEX(RANDOMBLOB(2)),0,4),\r\n  '-',\r\n  SUBSTR(HEX(RANDOMBLOB(8)),0,13)\r\n);\r\n\r\n199ED82A-3102-42DB-B59B-8C356B6F3FF7\r\n</code></pre>\n<p>Voila!</p>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-1\">\n<p>As technically correct is the best kind of correct: if you get <em>really</em> lucky with the generated random data, they might actually be valid. <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n</ol>\n</section>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Generating%20UUIDs%20in%20SQLite\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/generating-uuids-in-sqlite/","date_published":"2025-12-24T15:28:41Z","date_modified":"2026-01-04T17:26:44Z","language":"en"},{"id":"https://vic.demuzere.be/articles/pxc-cluster-innodb-page-size/","content_html":"<p>Today, I worked on a ticket for a customer with a <a href=\"https://craftcms.com\" title=\"Craft CMS\">Craft 4.x</a> CMS that created a MySQL table with a staggering 270 columns.\nMost of them were of type <code>TEXT</code>, but it also contained about 50 <code>VARCHAR</code> columns of varying lengths.\nAttempting to import the table resulted in a <em>Row Size Too Large</em> error.</p>\n<pre><code>ERROR 1118 (42000): Row size too large (&gt; 8126). Changing some columns to TEXT or BLOB may help. In current row format, BLOB prefix of 0 bytes is stored inline.\n</code></pre>\n<h2 id=\"initial-attempts\">Initial attempts<a href=\"#initial-attempts\" aria-label=\"Link to heading 'Initial attempts'\" data-heading-content=\"Initial attempts\" class=\"anchor\"></a></h2>\n<p>The first attempt to resolve the issue involved converting all columns to <code>TEXT</code>, as their contents <a href=\"https://dev.mysql.com/doc/refman/8.4/en/storage-requirements.html#data-types-storage-reqs-strings\" title=\"MySQL string type storage requirements\">are stored off-page</a>. That didn't work: there were too much columns, and even the references consumed too much space.</p>\n<p>Next, I considered <a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-row-format.html\" title=\"InnoDB row formats\">changing the row format</a>, which can sometimes resolve this issue. Unfortunately, the customer had already set it to <code>DYNAMIC</code>, the best available option for this scenario. No gains there.</p>\n<h2 id=\"innodb-page-size\">InnoDB page size<a href=\"#innodb-page-size\" aria-label=\"Link to heading 'InnoDB page size'\" data-heading-content=\"InnoDB page size\" class=\"anchor\"></a></h2>\n<p>The obvious fix for running out of space is creating more space.\nMySQL has an <a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_page_size\" title=\"InnoDB page size\"><code>innodb_page_size</code></a> parameter that defines the tablespace's <em>page size</em>.\nThe data file consists of virtual pages, which are completely overwritten each time a change is made.</p>\n<p>MySQL stores two rows in a page, meaning that when you update one, you're effectively saving two.\nThis is an important setting for performance, but in this case, it's relevant for another reason: <strong>a row can use at most half of the InnoDB page size</strong>.</p>\n<p>The <code>innodb_page_size</code> setting is somewhat misleading because its value is stored inside the <code>ibdata1</code> file, which contains your MySQL data. Once the first page has been written, <strong>you can't change the value anymore</strong>.\nAll pages inside the file must be the same size.</p>\n<p>Thus, changing it means <strong>recreating everything</strong>, which involves a process like this:</p>\n<pre><code class=\"language-bash\">mysqldump --all-databases &gt; backup.sql\nsystemctl stop mysql\nrm -rf /var/lib/mysql/*\nmysqld --user mysql --initialize --innodb-page-size=32K\nsystemctl start mysql\ncat backup.sql | mysql\n</code></pre>\n<p>However, on our <strong>Percona PXC cluster</strong>, I was met with an error:</p>\n<pre><code>2025-03-04T13:50:32.929013Z 0 [ERROR] [MY-010872] [InnoDB] InnoDB: Invalid page size=32768.\n</code></pre>\n<p>That was surprising, as <a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_page_size\" title=\"InnoDB page size\">the documentation</a> lists <code>32768</code> (<code>32K</code>) as a valid option.</p>\n<h3 id=\"the-real-problem\">The real problem<a href=\"#the-real-problem\" aria-label=\"Link to heading 'The real problem'\" data-heading-content=\"The real problem\" class=\"anchor\"></a></h3>\n<p>I spent way too much time debugging this because the error message didn't lead to any useful documentation.\nIt turns out that <strong>Percona PXC cluster does not allow changing <code>innodb_page_size</code></strong>.\nThey only accept the default value of <code>16K</code>, and any other setting will fail.</p>\n<p>From (an older) <a href=\"https://docs.percona.com/legacy-documentation/percona-xtradb-cluster/Percona-XtraDB-Cluster-5.6.51-28.pdf\" title=\"Percona PXC cluster manual\">Percona PDF manual</a>:</p>\n<blockquote>\n<p>Only the default 16 KB page size (innodb_page_size=16384) is accepted until the relevant upstream bug is fixed by Codership (see https://github.com/codership/galera/issues/398).\nAll other sizes will report <em>Invalid page size</em> and shut down (the server will not start up).</p>\n</blockquote>\n<p>Newer documentation doesn't seem to mention this anymore, but it still applies.</p>\n<h2 id=\"watch-out-with-innodb_strict_mode\">Watch out with <code>innodb_strict_mode</code><a href=\"#watch-out-with-innodb_strict_mode\" aria-label=\"Link to heading 'Watch out with innodb_strict_mode'\" data-heading-content=\"Watch out with innodb_strict_mode\" class=\"anchor\"></a></h2>\n<p>While researching this issue, I found many Stack Overflow posts suggesting that disabling <code>innodb_strict_mode</code> might help. Be <strong>very</strong> careful with this!</p>\n<p>Disabling strict mode <strong>only replaces the critical error with a warning</strong>. You'll be able to create the table, but the underlying issue remains: <strong>the rows are still too large</strong>.</p>\n<p>It may not fail immediately, but at some point, an <code>INSERT</code> or <code>UPDATE</code> operation will trigger an error if it exceeds the maximum size. This shifts the failure from a controlled event (such as during deployment) to a random action that a user might perform — not ideal.</p>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20You%20can%27t%20change%20innodb_page_size%20on%20PXC%20clusters\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/pxc-cluster-innodb-page-size/","date_published":"2025-03-04T17:41:55Z","date_modified":"2025-03-04T18:14:32Z","language":"en"},{"id":"https://vic.demuzere.be/articles/matching-public-private-key/","content_html":"<p>Customers regularly send me x509 key pairs at work<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup>.\nMost of the time these have been forwarded a few times already, and people tend to mess up the files.\nThis means that the public and private keys don't match anymore, and I have to ask for a new pair.</p>\n<p>Luckily, we can use OpenSSL to check whether a public and private key match.</p>\n<h2 id=\"for-rsa-keys\">For RSA keys<a href=\"#for-rsa-keys\" aria-label=\"Link to heading 'For RSA keys'\" data-heading-content=\"For RSA keys\" class=\"anchor\"></a></h2>\n<p>The classic way to compare RSA keys is to check whether the <strong>modulus</strong> is the same, as this is the only part that is shared between the public and private keys.\nIf the modulus isn't the same, the keys can't form a pair.</p>\n<p>Read the modulus from the <strong>public key</strong>:</p>\n<pre><code>openssl rsa -pubin -in public.pem -modulus -noout | openssl sha1\n</code></pre>\n<p>Then do the same with the <strong>private RSA</strong> key:</p>\n<pre><code>openssl rsa -in private.pem -modulus -noout | openssl sha1\n</code></pre>\n<p>If the two values match, the key files form a pair.</p>\n<p>Note that we're piping the actual output of the <code>-modulus</code> command to <code>openssl sha1</code> to get a shorter hash of the modulus.\nThis makes it easier to manually compare the two values, but it's not strictly necessary.\nYou could also compare the output directly if you're feeling brave.</p>\n<h2 id=\"for-ec-keys\">For EC keys<a href=\"#for-ec-keys\" aria-label=\"Link to heading 'For EC keys'\" data-heading-content=\"For EC keys\" class=\"anchor\"></a></h2>\n<p>Elliptic curve keys don't have an easy-to-compare modulus like the RSA keys above had.\nHowever, it's always possible to generate the public key<sup class=\"footnote-ref\"><a href=\"#fn-2\" id=\"fnref-2\" data-footnote-ref>2</a></sup> part when we have the private key.</p>\n<p>Using the <strong>private key</strong>, generate (a hash of) the public key:</p>\n<pre><code>openssl pkey -pubout -in private.pem | openssl sha1\n</code></pre>\n<p>Then compare the output with the <strong>public key</strong> file you have.\nWe need to make <code>openssl</code> load it and print it out again to make sure we get the exact same format.</p>\n<pre><code>openssl x509 -pubkey -in public.pem -noout | openssl sha1\n</code></pre>\n<p>If the two values match, these keys form a pair.</p>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-1\">\n<p>Please don't do this. Ask the company that needs a certificate to generate a private key and provide you with a <a href=\"https://en.wikipedia.org/wiki/Certificate_signing_request\">Certificate Signing Request (CSR)</a>. This way, you don't have to handle the private key at all. <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n<li id=\"fn-2\">\n<p>When people say &quot;public key&quot; in the context of a x509 pair they usually refer to the certificate. The public key we're extracting here is the actual cryptographic public key, it doesn't contain the extra data like domain names that a certificate usually holds. You can't recover the full certificate when you only have the private key. <a href=\"#fnref-2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n</ol>\n</section>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Check%20whether%20public%20and%20private%20key%20match\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/matching-public-private-key/","date_published":"2025-02-06T21:00:00Z","date_modified":"2025-02-07T10:59:22Z","language":"en"},{"id":"https://vic.demuzere.be/articles/hiding-outdated-articles/","content_html":"<p><a href=\"https://kevquirk.com\" title=\"Kev Quirk\">Kev Quirk</a> recently wrote about <a href=\"https://kevquirk.com/blog/on-removing-content\" title=\"On Removing Content; Kev Quirk\">removing content from public blogs</a>:</p>\n<blockquote>\n<p>Generally speaking, I don't delete content from this site.\nHaving said that, if I posted something that I later feel is particularly egregious, I think I probably would.\nI personally don't think that a website should be a permanent record - how can it be?\nNothing last forever.</p>\n</blockquote>\n<p>This got me thinking about what to do with some of my older articles.\nI believe that URLs should be stable.\nThe internet is a large place, there’s no way to know who might be linking to any of my webpages.\nRemoving a page may also <em>break other websites</em>: someone may have written about it.\nDeleting the page would remove the context of their reference.</p>\n<p>However, I’ve been working on this website in some form or another since 2007.\nTechnical articles tend to get outdated, and people change their minds about things they’ve written.\nSome of the articles on this website no longer reflect what I stand for.</p>\n<p>Articles older than two years already include a banner noting their age, but that’s not always enough.\nNowadays, people often form an opinion based on the title alone — especially on social media.\nIt’s unlikely they’d notice a note about how old the article is.</p>\n<p><strong>So I decided to <em>hide</em> certain articles.</strong>\nHidden articles are no longer linked on my website and don't appear in RSS feeds.\nYou can’t find them by clicking around.\nBut if you already have the link, the content is still accessible.\nBecause, as they say, <a href=\"https://www.w3.org/Provider/Style/URI\" title=\"Cool URIs don't change\">cool URIs don't change</a>.</p>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Cool%20URIs%20don%27t%20change%2C%20but%20don%27t%20need%20spotlights\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/hiding-outdated-articles/","date_published":"2024-11-10T10:04:30Z","date_modified":"2024-11-11T18:18:07Z","language":"en"},{"id":"https://vic.demuzere.be/articles/pwgen-not-secure-by-default/","content_html":"<p>Command-line tool <a href=\"https://sourceforge.net/projects/pwgen/\" title=\"Official pwgen pages on Sourceforge\"><code>pwgen</code></a> is a well known tool to generate a list of passwords.\nI use it almost daily: when creating new accounts on remote servers, or when I want to encrypt sensitive data with a temporary password to send to a customer.\nSometimes opening my terminal and typing <code>pwgen 30 1</code> is faster than getting to the generator in my password manager.<sup class=\"footnote-ref\"><a href=\"#fn-manager\" id=\"fnref-manager\" data-footnote-ref>1</a></sup></p>\n<p>I had noticed before that the passwords generated by <code>pwgen</code> seemed to contain patterns: repeating vowels is something that stands out.\nI hadn't thought about it much, until I saw a colleague type <code>pwgen -s</code> at his terminal.</p>\n<h2 id=\"what-is-this--s\">What is this <code>-s</code>?<a href=\"#what-is-this--s\" aria-label=\"Link to heading 'What is this -s?'\" data-heading-content=\"What is this -s?\" class=\"anchor\"></a></h2>\n<p>As with most unix-y tools, a quick glance at <a href=\"https://man.archlinux.org/man/pwgen.1\" title=\"pwgen man page\">the <em>man page</em></a> tells us everything we want to know.\nThe <code>-s</code> is shorthand for <code>--secure</code>:</p>\n<blockquote>\n<p>Generate completely random, hard-to-memorize passwords.\nThese should only be used for machine passwords, since otherwise it's almost guaranteed that users will simply write the password on a piece of paper taped to the monitor...</p>\n</blockquote>\n<p>As it turns out, <code>pwgen</code> actually doesn't generate secure passwords by default.\nThe passwords it generates are <em>designed to be easily memorized</em>.\nThat explains the patterns I noticed before!<sup class=\"footnote-ref\"><a href=\"#fn-man\" id=\"fnref-man\" data-footnote-ref>2</a></sup></p>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-manager\">\n<p><a href=\"https://keepassxc.org/\">KeepassXC</a> is a great password manager with local storage. <a href=\"#fnref-manager\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n<li id=\"fn-man\">\n<p>I should have checked the man page right away: patterns are a red flag in cryptography. <a href=\"#fnref-man\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n</ol>\n</section>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Today%20I%20learned%20pwgen%20is%20not%20secure%20by%20default\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/pwgen-not-secure-by-default/","date_published":"2022-05-29T14:23:58Z","date_modified":"2024-11-11T18:19:13Z","language":"en"},{"id":"https://vic.demuzere.be/articles/ecowitt-protocol-alecto-ws-5500/","content_html":"<p>Recently, I was looking for an outside temperature sensor. Preferably in <a href=\"https://en.wikipedia.org/wiki/Stevenson_screen\" title=\"Instrument shelter or Stevenson screen\">an instrument shelter</a>, as my whole garden has direct sunlight during most of the day. I ended up buying the <a href=\"https://alectohome.nl/collections/alecto_weerstations/products/alecto-ws-5500-professioneel-wifi-weerstation-met-app-zwart\" title=\"Alecto WS-5500 weather station\">Alecto WS-5500</a>, which is actually a rebranded <a href=\"http://www.foshk.com/Wifi_Weather_Station/WH2900.html\" title=\"WH2900 Professional Color display Weather Station with WIFI Connection\">Fine Offset WH2900</a>. It has a decent temperature sensor and also supports uploading data to a configurable HTTP endpoint.</p>\n<p><img src=\"https://vic.demuzere.be/wp-content/uploads/articles/images/DgqtPFnIaOyThLZaRMPkYUv7MXsowBfW.jpg\" alt=\"Alecto WS-5500\" /></p>\n<h2 id=\"uploading-data\">Uploading data<a href=\"#uploading-data\" aria-label=\"Link to heading 'Uploading data'\" data-heading-content=\"Uploading data\" class=\"anchor\"></a></h2>\n<p>The inside unit that comes with the WS-5500 can be configured using the <a href=\"https://play.google.com/store/apps/details?id=com.ost.wsview\" title=\"WS View Android application\">WS View</a> Android application. It's able to upload data to a custom HTTP endpoint, provided that you use the same protocol used by either <a href=\"https://www.wunderground.com/\" title=\"Weather Underground\">Weather Underground</a> or <a href=\"https://www.ecowitt.net/\" title=\"Ecowitt\">Ecowitt.net</a>.</p>\n<p>Weather Underground uses a HTTP GET request. This makes for rather long URLs since we have to put all the data into query parameters. In addition, the configured password is also visible in the URL and thus saved in my access logs. <em>I didn't want to go down this path.</em></p>\n<p>Ecowitt uses POST requests which seems more sane.</p>\n<h2 id=\"the-ecowitt-protocol\">The Ecowitt protocol<a href=\"#the-ecowitt-protocol\" aria-label=\"Link to heading 'The Ecowitt protocol'\" data-heading-content=\"The Ecowitt protocol\" class=\"anchor\"></a></h2>\n<p>Data is sent through a POST request in standard <code>application/x-www-form-urlencoded</code> encoding. Looks like the contents depends on what sensors are connected to your weather station. I've found examples that contained other fields than I found in the data sent by the WS-5500.</p>\n<p>For anyone else that wants to write a script to capture data from the all-in-one <a href=\"https://alectohome.nl/collections/alecto_weerstations/products/alecto-ws-5500-professioneel-wifi-weerstation-met-app-zwart\" title=\"Alecto WS-5500 weather station\">Alecto WS-5500</a> or <a href=\"http://www.foshk.com/Wifi_Weather_Station/WH2900.html\" title=\"WH2900 Professional Color display Weather Station with WIFI Connection\">Fine Offset WH2900</a>, the fields below are sent in the POST request when Ecowitt protocol is selected:</p>\n<pre><code>stationtype=EasyWeatherV1.5.9\nmodel=WS2900_V2.01.12\nfreq=868M\n\nwh65batt=0\n\ndateutc=2021-06-21 20:16:45\n\ntempinf=70.9\nhumidityin=59\nbaromrelin=29.944\nbaromabsin=29.841\ntempf=59.2\nhumidity=86\nwinddir=355\nwindspeedmph=2.9\nwindgustmph=4.5\nmaxdailygust=12.5\nrainratein=0.000\neventrainin=0.000\nhourlyrainin=0.000\ndailyrainin=0.000\nweeklyrainin=0.000\nmonthlyrainin=0.000\ntotalrainin=0.000\nsolarradiation=0.00\nuv=0\n</code></pre>\n<h2 id=\"converting-units\">Converting units<a href=\"#converting-units\" aria-label=\"Link to heading 'Converting units'\" data-heading-content=\"Converting units\" class=\"anchor\"></a></h2>\n<p>As we use different units in Belgium, I used these conversions:</p>\n<ul>\n<li>Temperature in °C: <code>(fahrenheit - 32.0) * (5.0 / 9.0)</code></li>\n<li>Pressure in hPa: <code>inches * 33.6585</code></li>\n<li>Speed in km/h: <code>mph * 1.60934</code></li>\n<li>Rain in mm (or l/m^2): <code>inches * 25.4</code></li>\n</ul>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Ecowitt%20protocol%20on%20the%20Alecto%20WS-5500\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/ecowitt-protocol-alecto-ws-5500/","date_published":"2021-07-10T22:41:02Z","date_modified":"2026-05-13T17:31:47Z","language":"en"},{"id":"https://vic.demuzere.be/articles/using-bash-variables-in-jq/","content_html":"<p>Due to the special characters used in json, the easiest way to use jq with inline scripts it by putting it between single quotes.\nThat <strong>makes it impossible to use bash variables inside your script</strong>.\nFortunately, jq has an <code>--arg</code> parameter to create a predefined variable from an external source.</p>\n<p>You can use it to define a <code>$foo</code> variable with the contents of bash variable <code>$FOO</code>, for example:</p>\n<pre><code>$ export FOO=&quot;bar&quot;\n$ jq -n --arg foo &quot;$FOO&quot; '{foo: $foo}'\n{\n  &quot;foo&quot;: &quot;bar&quot;\n}\n</code></pre>\n<p>You might notice the <code>-n</code> parameter: This tells <code>jq</code> to use <code>null</code> as input instead of reading json from stdin like it normally would.\nI'm using it in all examples on this page as it makes them a bit shorter.\nIn real world usage, you probably won't need it.</p>\n<p>As usual in bash scripts, you can also execute a command inline:</p>\n<pre><code>$ jq -n --arg hostname &quot;$(hostname)&quot; '{foo: $hostname}'\n{\n  &quot;foo&quot;: &quot;cypher&quot;\n}\n</code></pre>\n<p>Every variable created with <code>--arg</code> is <strong>treated as a string</strong>.\nThat might cause some unexpected behavior when you work with numbers, like when you attempt to add a number to an argument.\nComparisons between strings and numbers seem to work fine, but I wouldn't count on it (pun intended).</p>\n<pre><code>$ echo '{&quot;a&quot;: 3.0}' | jq --arg foo 5 '$foo + .a'\njq: error (at &lt;stdin&gt;:1): string (&quot;5&quot;) and number (3) cannot be added\n</code></pre>\n<p>There are two ways to solve this: either convert the variable to a number with <code>tonumber</code> or use <code>--argjson</code> instead.\nI prefer to use <code>tonumber</code> because that way I'm sure jq forces it to be a number.\nAs my argument may be the result of an earlier command, I may be using an error message as input instead of the expected numeric result.</p>\n<pre><code>$ jq -n --arg foo 5 '($foo|tonumber) + 1'\n6\n\n$ jq -n --argjson foo 5 '$foo + 1'\n6\n</code></pre>\n<h2 id=\"variables-from-files\">Variables from files<a href=\"#variables-from-files\" aria-label=\"Link to heading 'Variables from files'\" data-heading-content=\"Variables from files\" class=\"anchor\"></a></h2>\n<p>It's also possible to load variables from files.\nWith <code>--slurpfile</code> we can read a file containing json objects.\nAn array containing the separate objects is then made available as a predefined variable.\nNote that even if your file contains only a single json object, as the variable is still an array, the <code>[0]</code> is required to access it.</p>\n<pre><code>$ cat foo.json\n{&quot;foo&quot;: &quot;bar&quot;}\n{&quot;bar&quot;: &quot;foo&quot;}\n\n$ jq -n --slurpfile foo foo.json '$foo[0].foo'\n&quot;bar&quot;\n</code></pre>\n<p>A second option is <code>--rawfile</code>, creating a string variable with the exact contents of the file.\nNote that as jq outputs json, reading and printing the file from the previous example results an escaped string.</p>\n<pre><code>$ jq -n --rawfile foo foo.json '$foo'\n&quot;{\\&quot;foo\\&quot;: \\&quot;bar\\&quot;}\\n{\\&quot;bar\\&quot;: \\&quot;foo\\&quot;}\\n&quot;\n</code></pre>\n<p>That's it!</p>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20Using%20bash%20variables%20in%20jq\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/using-bash-variables-in-jq/","date_published":"2021-02-10T20:01:03Z","date_modified":"2024-11-11T18:21:44Z","language":"en"},{"id":"https://vic.demuzere.be/articles/not-so-redesign/","content_html":"<p>Over the past few months I've been working on a <strong>new stylesheet</strong> for this website, and today I finally pushed the redesign branch to production!\nThe previous CSS code was largely written <em>almost 10 years ago</em> and only received small updates and enhancements.\nSince then, new features like <a href=\"https://developer.mozilla.org/en-US/docs/Glossary/Flexbox\" title=\"MDN Glossary: Flexbox\">flexbox</a> and <a href=\"https://developer.mozilla.org/en-US/docs/Glossary/Grid\" title=\"MDN Glossary: Grid\">grid</a>s were added and made creating responsive websites much easier.</p>\n<p>Turns out, the new design looks <strong>exactly the same as the old one</strong>!</p>\n<p>A few things did change though:</p>\n<ul>\n<li>Wrote plain CSS instead of <a href=\"http://lesscss.org/\" title=\"LESS: CSS, with just a little more\">LESS</a>.</li>\n<li>Stopped using <code>float</code> to position elements in favor of <a href=\"https://developer.mozilla.org/en-US/docs/Glossary/Flexbox\" title=\"MDN Glossary: Flexbox\">flexbox</a>.</li>\n<li>Followed the <a href=\"https://www.w3.org/WAI/standards-guidelines/wcag/\" title=\"Web Content Accessibility Guidelines (WCAG)\">Web Content Accessibility Guidelines</a> (WCAG).</li>\n<li>Made the stylesheet a lot smaller.</li>\n<li>Focused on readability throughout the website.</li>\n</ul>\n<p>I also tried to get some insight in how people used the website:</p>\n<h2 id=\"a-year-in-statistics\">A year in statistics<a href=\"#a-year-in-statistics\" aria-label=\"Link to heading 'A year in statistics'\" data-heading-content=\"A year in statistics\" class=\"anchor\"></a></h2>\n<p>Most of the traffic to this website is routed through <a href=\"https://www.cloudflare.com/cdn/\" title=\"Cloudflare CDN\">Cloudflare's CDN</a>.\nI've configured it to cache everything, which means the access logs on my web server record only part of the actual traffic.\nThis creates an interesting dataset: I can see which pages are most visited, but I can't see spikes due to pages being shared somewhere.\nFor the same reason, my statistics about browsers, operating systems, referring websites and returning visitors are completely skewed.</p>\n<h3 id=\"popular-pages\">Popular pages<a href=\"#popular-pages\" aria-label=\"Link to heading 'Popular pages'\" data-heading-content=\"Popular pages\" class=\"anchor\"></a></h3>\n<p>Articles are the most popular pages on this website.\nThat makes sense, as those tend to show up in searches and are shared more frequently than others.\nMy most popular articles are technical ones:</p>\n<ol>\n<li><a href=\"/articles/using-systemd-user-units/\">Managing services for non-root users with systemd</a></li>\n<li><a href=\"/articles/curriculum-vitae-cv-with-latex-moderncv/\">Creating a C.V. with LaTeX and moderncv</a></li>\n<li><a href=\"/articles/golang-makefile-crosscompile/\">Cross compiling Go applications with Make</a></li>\n</ol>\n<p>That means I need to write more articles!</p>\n<h3 id=\"popular-404-errors\">Popular 404 errors<a href=\"#popular-404-errors\" aria-label=\"Link to heading 'Popular 404 errors'\" data-heading-content=\"Popular 404 errors\" class=\"anchor\"></a></h3>\n<p>The most common 404s appear to be scanners looking for vulnerabilities.\nWordPress related pages like <code>wp-login.php</code>, <code>xmlrpc.php</code> and several files inside the <code>wp-includes</code> directory lead the race, followed by Joomla's <code>administrator</code> directory.</p>\n<p>My work to fix broken links (using data from the access logs and Google Search Console) seems to have paid off.\nI've also been trying not to delete or move pages for a few years now.\nWhen a page is no longer relevant, I replace it with a small message pointing people to alternatives, like I did with my <a href=\"/otr/\" title=\"My off-the-record fingerprints\">OTR fingerprint listing</a>.</p>\n<p>Another interesting 404 page is <code>/irc:irc.quakenet.org/sorcix,isnick</code> which seems to be caused by a spider that doesn't understand <code>irc:</code> URLs.</p>\n<h2 id=\"future-changes\">Future changes<a href=\"#future-changes\" aria-label=\"Link to heading 'Future changes'\" data-heading-content=\"Future changes\" class=\"anchor\"></a></h2>\n<p>I've been looking into <a href=\"https://indieweb.org\" title=\"The IndieWeb\">IndieWeb</a> after reading a few blog posts by <a href=\"https://jlelse.blog\" title=\"Jan-Lukas Else's weblog\">Jan-Lukas Else</a>.\nPeople can reply to your articles by linking back from their own website, or by using one of the provided IndieWeb-compatible services.\nAs I'm not active on mainstream social media, this may be a great way to connect with like-minded people.</p>\n<p>Features I want to roll out next:</p>\n<ul>\n<li>RSS feed for articles</li>\n<li>Overview pages for every tag, making it easier to find related articles</li>\n<li>Support for Webmentions</li>\n</ul>\n<div class=\"footer\"><hr><p>Thanks for subscribing to my feed!</p><p>Feel free to e-mail comments or corrections to <a href=\"mailto:vic@demuzere.be?subject=RE%3A%20The%20mayor%20CSS%20not-so-redesign\">vic@demuzere.be</a>.</p></div>","url":"https://vic.demuzere.be/articles/not-so-redesign/","date_published":"2020-06-14T00:12:27Z","date_modified":"2024-11-11T18:24:32Z","language":"en"}]}