Skip to content

fix(media): skip byte-identical writes so a synced vault stops forking - #134

Open
VGonPa wants to merge 1 commit into
developfrom
fix/media-skip-identical-write
Open

fix(media): skip byte-identical writes so a synced vault stops forking#134
VGonPa wants to merge 1 commit into
developfrom
fix/media-skip-identical-write

Conversation

@VGonPa

@VGonPa VGonPa commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Qué pasaba

learnings/x-knowledge/_media/ en el vault pesaba 9,1 GB. De eso, 8,41 GB eran copias de conflicto de iCloud — 50.020 ficheros nombre N.ext, todos byte-idénticos al fichero de al lado, ninguno referenciado por ninguna nota. El contenido real son 0,55 GB.

No era vídeo: digest-video descarta los bytes como debe. Era la misma imagen guardada hasta 17 veces.

Por qué

generate re-mirrorea el árbol de media entero al vault en cada ejecución, y el job nocturno lanza generate cada noche. Miles de reescrituras de bytes idénticos por noche. Para iCloud cada reescritura es una modificación que reconciliar, y si choca con una versión que aún no ha subido, conserva las dos: la perdedora como nombre N.ext.

Los duplicados se agrupan en 9 fechas — las de los refresh grandes (25-jun, 4/5/6-jul, 12-ago), no las noches sueltas. Un pase masivo satura el sync y bifurca en bloque.

El fix

Dos rutas de escritura, con firmas observables distintas:

mecanismo inode mtime ctime
generate._mirror_file shutil.copy2 (trunca en sitio) igual igual (copystat) cambia
media._write_bytes os.replace cambia cambia cambia

La primera es la que llenó el vault. Ambas comparan ahora antes de escribir, con el tamaño como corto-circuito para no materializar bytes que sólo viven en la nube. Un fallo de stat/lectura cae a escribir: una copia redundante es barata, un embed roto no.

download_all ya era idempotente a nivel de ítem, pero un full refresh reconstruye el estado y re-descarga todas las fotos — por eso la idempotencia de arriba no bastaba.

Tests

TDD, con el RED verificado en cada caso. El primer intento contra _mirror_file pasó sin el fix: asertaba sobre inode y mtime, que copy2 no altera. Corregido a st_ctime_ns, que es el único campo que registra la escritura — y el que un cliente de sync interpreta como "esto ha cambiado".

  • _mirror_file: no toca un destino idéntico · reescribe uno que divergió · reescribe cuando el tamaño coincide pero los bytes no
  • _write_bytes: no toca un fichero idéntico · reescribe si el contenido cambió · no se confunde con un .part huérfano al lado

Suite completa: 1633 passed, 1 xfailed. scripts/check.sh: las 11 puertas en verde, coverage 94%.

Verificación contra el vault real

300 ficheros reales del vault re-mirrorados con el código nuevo → 0 tocados. iCloud no vería nada que reconciliar.

Los 8,41 GB ya se han recuperado en local (el disco iba al 94%), verificando antes fichero a fichero: 50.020/50.020 confirmados byte-idénticos por md5 contra su base, 0 grupos sin fichero base, 0 referencias de notas rotas después.

🤖 Generated with Claude Code

`generate` re-mirrors the entire media tree into the vault on every run and
the nightly job runs it nightly, so the same bytes were rewritten thousands of
times a night. On iCloud Drive a rewrite is a modification to reconcile, and a
clash with a not-yet-uploaded version is resolved by keeping BOTH — the loser
as a `name N.ext` sibling. The x-knowledge vault had accumulated 8.41 GB across
50,020 such copies, every one byte-identical to the file next to it.

Two write paths, two distinct signatures:

- `generate._mirror_file` — `shutil.copy2` truncates in place, so the inode
  survives and `copystat` restores mtime; only ctime records the write. This is
  the path that filled the vault.
- `media._write_bytes` — `os.replace` installs a fresh inode on every call,
  including no-op rewrites during a full refresh (which rebuilds item state and
  so re-downloads every photo, defeating the item-level idempotency).

Both now compare before writing, size first so the common case settles from
metadata without faulting in cloud-only bytes. A stat/read failure falls
through to the write: a redundant copy is cheap, a missing embed is not.

Verified against the live vault — 300 real files re-mirrored, 0 touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant