fix: keep the mirror out of the device root

Proton refuses a file directly in a device's root folder — 422 "Cannot
create file at the root of a device" — so the first real backup died on
RESTORE.txt, before it had written anything. The mirror now lives in a
`data/` folder on the device, which is both what Proton permits and the
honest name for what it holds: the contents of DIARY_DATA_DIR.

Every remote path changed with it, so the bookkeeping describing the old
layout is dropped and rebuilt on the next run. A device root that
already collected an empty `uploads/` folder from the failed attempt
keeps it; it is not ours to guess about, and deleting it is one click.

Also quiet rPGP's packet-header warnings by default. They fire on every
key Proton wrote slightly differently to how rPGP re-serialises it —
cosmetic, once per unlock, and loud enough to bury the backup's own log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5Gk8FPkRXu1e6Nj2BCRE1
This commit is contained in:
nvrlandClaude Opus 5 committed 2026-09-05 13:39:41 +02:00
1 parent ca0039400e
commit 7646ecc397
5 files changed
+62 -24

No files matched your search

+13 -10
View File
@@ -3,10 +3,11 @@
//! The remote layout is deliberately the local one, not a rendering of it:
//!
//! ```text
//! narl-diary/ the device's sync root
//! RESTORE.txt what this is and how to put it back
//! diary.db a consistent snapshot, one revision per change
//! uploads/<uuid> every uploaded file, under the name the database knows
//! narl-diary/ the device's sync root — folders only, per Proton
//! data/ the diary's data directory, copied
//! RESTORE.txt what this is and how to put it back
//! diary.db a consistent snapshot, one revision per change
//! uploads/<uuid> every uploaded file, under the name the database knows
//! ```
//!
//! so restoring is copying two things into an empty data directory, with no
@@ -33,7 +34,9 @@ pub struct Summary {
}
const RESTORE: &str = "\
This is a Proton Drive mirror of a ~/diary server.
This is a Proton Drive mirror of a ~/diary server. It is the contents of the
server's data directory, one folder down from the device root because Proton
does not allow files directly in it.
diary.db the whole diary: entries, media metadata, share tokens
uploads/<uuid> every uploaded file; diary.db gives each one its real name
@@ -55,7 +58,7 @@ pub async fn run(
db: &SqlitePool,
config: &Config,
client: &ProtonDriveClient,
root: &NodeUid,
data_folder: &NodeUid,
uploads_folder: &NodeUid,
) -> Result<Summary> {
let mut mirror = Mirror {
@@ -67,10 +70,10 @@ pub async fn run(
// Written once, and only ever re-read by a human who has lost everything
// else — so it goes up before the data it explains.
mirror
.bytes(root, "RESTORE.txt", "RESTORE.txt", "text/plain", RESTORE.as_bytes())
.bytes(data_folder, "RESTORE.txt", "RESTORE.txt", "text/plain", RESTORE.as_bytes())
.await?;
mirror.database(config, root).await?;
mirror.database(config, data_folder).await?;
mirror.media(config, uploads_folder).await?;
if config.backup.prune {
@@ -94,7 +97,7 @@ impl Mirror<'_> {
/// write-ahead log already folded in. Copying the file by hand while the
/// diary is being written to would produce something that may or may not
/// open.
async fn database(&mut self, config: &Config, root: &NodeUid) -> Result<()> {
async fn database(&mut self, config: &Config, data_folder: &NodeUid) -> Result<()> {
let snapshot = config.data_dir.join(".diary-backup.db");
// A snapshot left behind by a killed run would fail the next one:
// VACUUM INTO refuses to write over an existing file.
@@ -107,7 +110,7 @@ impl Mirror<'_> {
.context("could not snapshot the database")?;
let result = self
.file(root, "diary.db", "diary.db", "application/vnd.sqlite3", &snapshot)
.file(data_folder, "diary.db", "diary.db", "application/vnd.sqlite3", &snapshot)
.await;
let _ = tokio::fs::remove_file(&snapshot).await;