<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Backup on Max Bonnefin</title><link>https://bonnef.in/tags/backup/</link><description>Recent content in Backup on Max Bonnefin</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>max@bonnef.in (Max Bonnefin)</managingEditor><webMaster>max@bonnef.in (Max Bonnefin)</webMaster><copyright>Max Bonnefin</copyright><lastBuildDate>Sun, 04 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://bonnef.in/tags/backup/index.xml" rel="self" type="application/rss+xml"/><item><title>The Sync Script That Corrupted Its Own Backup</title><link>https://bonnef.in/posts/ipod-sync-corruption-reflash/</link><pubDate>Sun, 04 Oct 2026 00:00:00 +0000</pubDate><author>max@bonnef.in (Max Bonnefin)</author><guid>https://bonnef.in/posts/ipod-sync-corruption-reflash/</guid><description>&lt;p&gt;Back in 2019 I &lt;a href="https://bonnef.in/posts/modded-ipod-800gb/" &gt;built an 800GB iPod&lt;/a&gt; and, pleased with myself, published the little script I used to keep it in sync with my laptop. Seven years later that script had quietly eaten a chunk of my music library and I only noticed when the iPod refused to boot.&lt;/p&gt;
&lt;h2 id="how-a-sync-script-corrupts-the-thing-its-backing-up"&gt;
How a sync script corrupts the thing it&amp;rsquo;s backing up
&lt;a class="heading-link" href="#how-a-sync-script-corrupts-the-thing-its-backing-up"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;After years in my pocket, one of the microSD cards in the &lt;a href="https://www.iflash.xyz/store/iflash-quad/" class="external-link" target="_blank" rel="noopener"&gt;iFlash-Quad&lt;/a&gt; started returning bad bytes on reads: not errors that &lt;code&gt;rsync&lt;/code&gt; would notice, just garbage and zero-length files handed back as if they were real. The adapter was fine. Flash cards simply wear out, and this one had. A read error would have been fine, &lt;code&gt;rsync&lt;/code&gt; would have stopped. Silent corruption is the dangerous kind, because every tool in the chain treats the garbage as legitimate data.&lt;/p&gt;
&lt;p&gt;The original 2019 script ran &lt;a href="https://download.samba.org/pub/rsync/rsync.1#opt--ignore-existing" class="external-link" target="_blank" rel="noopener"&gt;&lt;code&gt;rsync --ignore-existing&lt;/code&gt;&lt;/a&gt; in both directions, library to iPod and iPod back to library. Once the card started handing back corrupt reads, the iPod-to-library pass copied that corruption straight over the good originals on my laptop. Every subsequent run propagated it further.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--ignore-existing&lt;/code&gt; is what made it invisible. The flag skips any file already present on the destination, which sounds safe until you realise a file full of zeroes is still present. Once a track was corrupted on the laptop, &lt;code&gt;rsync&lt;/code&gt; saw a filename that existed and skipped it from then on. The good copy was gone and nothing would ever overwrite it. The corruption had been seeping outward for a long time before the iPod finally wouldn&amp;rsquo;t boot, which meant the copies I thought of as backups had been absorbing it too.&lt;/p&gt;
&lt;h2 id="direction-isnt-the-fix-verification-is"&gt;
Direction isn&amp;rsquo;t the fix. Verification is.
&lt;a class="heading-link" href="#direction-isnt-the-fix-verification-is"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The tempting conclusion is to make the sync strictly one-way: library to iPod, never the reverse, with the laptop as the source of truth. That does stop a failing iPod from poisoning the master, and if all you want is a disposable mirror it&amp;rsquo;s enough.&lt;/p&gt;
&lt;p&gt;It also throws away something I actually use. A two-way sync lets either device hold tracks the other is missing, which is handy when I add music on the go or rip something on the laptop. The reason two-way was dangerous in 2019 wasn&amp;rsquo;t the second direction. It was that nothing checked whether the bytes being copied were real.&lt;/p&gt;
&lt;p&gt;So the script I run now is still two-way. It&amp;rsquo;s safe because every audio file is decode-verified before it&amp;rsquo;s allowed to move, in both directions. If a file doesn&amp;rsquo;t decode, it&amp;rsquo;s corrupt, and corrupt files are skipped and logged rather than copied over anything.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;verify_rel&lt;span style="color:#ff7b72;font-weight:bold"&gt;()&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local &lt;span style="color:#79c0ff"&gt;src&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$1&lt;/span&gt; &lt;span style="color:#79c0ff"&gt;rel&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; is_rockbox &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;then&lt;/span&gt; printf &lt;span style="color:#a5d6ff"&gt;&amp;#39;%s\0&amp;#39;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;fi&lt;/span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;# .rockbox exempt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; ! is_audio &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;then&lt;/span&gt; printf &lt;span style="color:#a5d6ff"&gt;&amp;#39;%s\0&amp;#39;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;fi&lt;/span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;# non-audio copied as-is&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;[&lt;/span&gt; -s &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$src&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;/&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;]&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#79c0ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ffmpeg -v error -i &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$src&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;/&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt; -map 0:a -c:a pcm_s16le -f null - &amp;gt;/dev/null 2&amp;gt;&amp;amp;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; printf &lt;span style="color:#a5d6ff"&gt;&amp;#39;%s\0&amp;#39;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; printf &lt;span style="color:#a5d6ff"&gt;&amp;#39;CORRUPT, skipped: %s\n&amp;#39;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$rel&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$CORRUPTLOG&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ffmpeg -f null&lt;/code&gt; decodes the audio stream and discards the output.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; It&amp;rsquo;s the cheapest way to ask &amp;ldquo;would this file actually play?&amp;rdquo; without writing anything. A zero-byte FLAC fails. A track read through a flaky contact fails. Only files that fully decode make it onto the approved list, and that list is the only thing &lt;code&gt;rsync&lt;/code&gt; is allowed to touch:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -rt --from0 --files-from&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$approved&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#79c0ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --ignore-existing --no-perms --no-owner --no-group &lt;span style="color:#79c0ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$src&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;/&amp;#34;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$dst&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What makes the two-way version safe where the 2019 one was lethal comes down to a few deliberate choices.&lt;/p&gt;
&lt;p&gt;Verification gates every copy. This is the one that matters. A corrupt read never reaches the other side, so neither copy can poison the other no matter which direction the sync runs. It&amp;rsquo;s the check that would have caught the degrading card straight away.&lt;/p&gt;
&lt;p&gt;To be precise about what that check buys you: a decode pass proves a file still plays end to end, not that it&amp;rsquo;s bit-for-bit what it was when you ripped it. It catches the failures that actually bit me, zero-byte files and garbled reads that fail to decode at all, but a subtle glitch buried mid-stream can still decode cleanly and slip through. It&amp;rsquo;s a liveness check, not a cryptographic one. If you want true integrity you keep checksums from the moment of ripping and compare against those. For catching a dying card handing back rubbish, decode-verification is enough.&lt;/p&gt;
&lt;p&gt;Nothing is ever overwritten or deleted. &lt;code&gt;--ignore-existing&lt;/code&gt; means a file that already exists is left alone, and there&amp;rsquo;s no &lt;code&gt;--delete&lt;/code&gt; anywhere. The sync only ever adds files that are missing from one side. That has a deliberate limitation: if I edit a track in place, re-tag it or re-encode it, the other side keeps the old copy, because the path still exists and existing files are never touched. The script doesn&amp;rsquo;t try to detect or reconcile that. Working out which of two same-path files is the &amp;ldquo;right&amp;rdquo; one would mean reading the whole device back on every run to catch a case I create by hand and already know about. An automatic resolver is also just another way to let bad data win silently. If I change a file in place I delete the stale copy and let the next sync re-add it.&lt;/p&gt;
&lt;p&gt;The removable device is mounted and unmounted by the script itself, located by label and falling back to UUID, and it refuses to run if something that isn&amp;rsquo;t the iPod is mounted at the target path. The 2019 script assumed the mount was already there and correct, which is how an unmounted target quietly becomes a write into a bare directory on your root filesystem.&lt;/p&gt;
&lt;p&gt;The verification pass runs in parallel across cores, because decoding an entire library serially is painfully slow. The full script, with the mounting, the parallel verification and the ownership fixes, is &lt;a href="https://gitlab.com/_Max/rockboxed-ipod-syncing-script" class="external-link" target="_blank" rel="noopener"&gt;on GitLab&lt;/a&gt;. I&amp;rsquo;m publishing it because the approach is worth stealing, not as something you should run unexamined against your only copy of anything. It&amp;rsquo;s wired to my hardware, my mount points and my tolerance for risk. Read it, understand the direction of trust, and adapt it. The snippets above are the parts that carry the argument.&lt;/p&gt;
&lt;h2 id="reflashing-the-ipod-firmware-on-linux-without-itunes"&gt;
Reflashing the iPod firmware on Linux, without iTunes
&lt;a class="heading-link" href="#reflashing-the-ipod-firmware-on-linux-without-itunes"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Rebuilding the iPod meant repartitioning the microSD cards from scratch, which wiped the firmware partition along with everything else. &lt;a href="https://www.rockbox.org/" class="external-link" target="_blank" rel="noopener"&gt;Rockbox&lt;/a&gt; then refused to install.&lt;/p&gt;
&lt;p&gt;Rockbox installs its bootloader alongside the Apple firmware, so it needs that firmware present. The usual answer is to restore the iPod with iTunes first. The entire point of this project, since 2019, has been never touching iTunes, plus is iTunes even still a thing in 2026? So the question was whether a 5.5G iPod can be reflashed on Linux alone.&lt;/p&gt;
&lt;p&gt;Fortunately it can. The firmware images Apple shipped are plain &lt;code&gt;.ipsw&lt;/code&gt; files, which are just ZIP archives, and the iPod will reflash itself from the right payload with no Apple software involved.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the correct image for the board. For the 5.5G family it&amp;rsquo;s &lt;code&gt;iPod_25.1.3.ipsw&lt;/code&gt;: the &lt;code&gt;25&lt;/code&gt; is Apple&amp;rsquo;s internal model identifier for this iPod rather than a version number, with the actual software version being 1.3.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unzip&lt;/code&gt; the archive and locate the &lt;code&gt;Firmware-*&lt;/code&gt; payload inside.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd&lt;/code&gt; that payload directly onto the iPod&amp;rsquo;s firmware partition.&lt;/li&gt;
&lt;li&gt;Unplug the iPod. It detects the fresh image and self-reflashes: Apple logo, progress bar, then a reboot into the Apple OS, which rewrites the firmware partition in the format it expects.&lt;/li&gt;
&lt;li&gt;Rockbox&amp;rsquo;s installer now sees a valid Apple firmware and installs the bootloader. Dual-boot restored.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The self-reflash in step four is the interesting bit. The iPod does the real work itself once it finds a valid image on the partition; iTunes was only ever the thing that put the image there. I assume this is roughly what the official restore does under the hood, though I haven&amp;rsquo;t disassembled the Apple updater to confirm it. Either way you don&amp;rsquo;t need iTunes to trigger it, which is a fitting ending for a project built on avoiding iTunes in the first place.&lt;/p&gt;
&lt;h2 id="what-it-cost-me"&gt;
What it cost me
&lt;a class="heading-link" href="#what-it-cost-me"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The music is back on the iPod. I rebuilt the library from the least-corrupted sources I could find, re-ripped what I still had on disc, and &lt;em&gt;reacquired&lt;/em&gt; &lt;strong&gt;cough cough&lt;/strong&gt; the rest. The one failed card went in the bin awaiting its replacement; the iFlash-Quad and the other three cards carried on fine.&lt;/p&gt;
&lt;p&gt;The lesson isn&amp;rsquo;t &amp;ldquo;test your backups&amp;rdquo;, though that&amp;rsquo;s true. It&amp;rsquo;s narrower: a copy your tooling trusts without checking isn&amp;rsquo;t a backup, it&amp;rsquo;s a liability waiting for the first bad read. Direction, retention, delete flags, all of that is secondary. If the bytes aren&amp;rsquo;t verified before they&amp;rsquo;re trusted, the rest is decoration. My 2019 self got that backwards and published the mistake. If you copied that old script you probably want to update it.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;The &lt;a href="https://ffmpeg.org/ffmpeg-formats.html#null" class="external-link" target="_blank" rel="noopener"&gt;&lt;code&gt;-f null&lt;/code&gt;&lt;/a&gt; muxer writes decoded frames to nowhere, so the whole file is read and decoded with the output thrown away. Pair it with &lt;code&gt;-v error&lt;/code&gt; and a non-zero exit means at least one frame failed to decode. It&amp;rsquo;s the standard trick for a quick &amp;ldquo;does this media file actually decode?&amp;rdquo; check without producing a file.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Confirmed against the &lt;a href="https://theapplewiki.com/wiki/Firmware/iPod" class="external-link" target="_blank" rel="noopener"&gt;Apple Wiki iPod firmware list&lt;/a&gt;. The &lt;code&gt;iPod_25.*&lt;/code&gt; naming runs across every 5.5G release, with the trailing numbers (&lt;code&gt;1.3&lt;/code&gt;) being the user-facing software version. The &lt;code&gt;25&lt;/code&gt; identifies the hardware, not the firmware revision, which is why it never changes between versions.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>