mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40aa8016ea | ||
|
|
d1b7535696 | ||
|
|
7957e1ed59 | ||
|
|
53543c91f9 | ||
|
|
fccb3d9561 |
No files matched your search
Generated
+4
-4
@@ -3779,9 +3779,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proton-drive-rs"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f70318394b2815b009c52f804d75747086db93a338ceb6eae62541f2b25d5bf"
|
||||
checksum = "a8fa2a88130883010b32035e208aaa36fb275f6d7d78ed44a1069b1d33d24481"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.23.1",
|
||||
@@ -3806,9 +3806,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proton-sdk"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4196392032f56b3bbaac0bb1f24eea62fa7e2a0af72004de8d08e518d14720fb"
|
||||
checksum = "af0bf8d50fa829d1a65b09c6855b606578d02734d4089874c971d1063c23c353"
|
||||
dependencies = [
|
||||
"aes-gcm 0.11.1",
|
||||
"async-trait",
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ keywords = ["proton", "drive", "fuse", "sync", "linux"]
|
||||
categories = ["filesystem", "command-line-utilities"]
|
||||
|
||||
[workspace.dependencies]
|
||||
proton-sdk = { version = "0.10.0" }
|
||||
proton-drive-rs = { version = "0.10.0" }
|
||||
proton-sdk = { version = "0.10.1" }
|
||||
proton-drive-rs = { version = "0.10.1" }
|
||||
|
||||
# Internal crates.
|
||||
pdfs-core = { path = "crates/pdfs-core" }
|
||||
|
||||
@@ -1434,6 +1434,14 @@ impl ProtonFs {
|
||||
// (`ContentCache::clone_or_copy`).
|
||||
if let Err(e) = ContentCache::clone_or_copy(&pending.path, &path) {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
// The drain lets go of the pending entry before it
|
||||
// discards the blob, so a blob that went away under the
|
||||
// copy is a revision that landed or was replaced: the
|
||||
// same race the re-check below catches, only earlier.
|
||||
if self.core.pending_blob(&uid).as_ref() != Some(&pending.path) {
|
||||
warn!(%uid, attempt, "queued revision went away while opening for write; retrying");
|
||||
continue;
|
||||
}
|
||||
error!(%uid, source = %pending.path.display(), error = %e,
|
||||
"copy queued revision into write scratch failed");
|
||||
break 'install Err(Errno::EIO);
|
||||
|
||||
@@ -592,14 +592,22 @@ mod tests {
|
||||
daemon.pending(),
|
||||
drive.tree()
|
||||
);
|
||||
// The bytes kept for the handle went once the trash landed with it closed.
|
||||
let cached: Vec<_> = std::fs::read_dir(dir.join("cache"))
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
|
||||
.map(|e| e.file_name())
|
||||
.collect();
|
||||
assert!(cached.is_empty(), "{cached:?}");
|
||||
// The bytes kept for the handle went once the trash landed with it
|
||||
// closed. The queue empties as the trash's row goes, a moment before
|
||||
// the eviction, so this waits too.
|
||||
let cached = || -> Vec<_> {
|
||||
std::fs::read_dir(dir.join("cache"))
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
|
||||
.map(|e| e.file_name())
|
||||
.collect()
|
||||
};
|
||||
assert!(
|
||||
wait_until(Duration::from_secs(30), || cached().is_empty()),
|
||||
"{:?}",
|
||||
cached()
|
||||
);
|
||||
|
||||
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
@@ -1998,6 +2006,21 @@ mod tests {
|
||||
daemon.write_new(&path, b"first").unwrap();
|
||||
assert!(wait_until(Duration::from_secs(30), || held.reached()));
|
||||
std::fs::write(&path, b"second").unwrap();
|
||||
// The kernel sends the release after `close` returns. Attaching the
|
||||
// bytes resets the create's attempts, so a release that came after the
|
||||
// lost reply had it retried at once, and the retry landed before
|
||||
// anything here saw it fail. The bytes it replaced go only once they
|
||||
// are attached.
|
||||
let staged = dir.join("cache").join("staging");
|
||||
assert!(wait_until(Duration::from_secs(30), || {
|
||||
let blobs: Vec<Vec<u8>> = std::fs::read_dir(&staged)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| std::fs::read(e.path()).ok())
|
||||
.collect();
|
||||
blobs.iter().any(|b| b == b"second") && !blobs.iter().any(|b| b == b"first")
|
||||
}));
|
||||
drop(held);
|
||||
assert!(wait_until(Duration::from_secs(30), || {
|
||||
daemon
|
||||
|
||||
+65
-4
@@ -68,10 +68,10 @@ original wording.
|
||||
|
||||
| Entry | Title | What is left |
|
||||
|---|---|---|
|
||||
| [B275](#b275--on-a-slow-link-a-files-last-write-can-land-only-in-a-conflict-copy) | On a slow link, a file's last write can land only in a conflict copy | Cause not found; seen once in CI |
|
||||
| [B273](#b273--a-restore-proposes-another-computers-paths-when-the-two-share-a-hostname) | A restore proposes another computer's paths when the two share a hostname | Not fixed |
|
||||
| [B270](#b270--one-file-left-in-a-mirror-folder-clears-the-wipe-guard-for-all-the-others) | One file left in a mirror folder clears the wipe guard for all the others | Not fixed |
|
||||
| [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed |
|
||||
| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced |
|
||||
| [B134](#b134--a-file-deleted-online-while-the-drain-checks-its-write-is-kept-as-a-conflict-copy) | A file deleted online while the drain checks its write is kept as a conflict copy | Only with `local_first` off |
|
||||
| [B91](#b91--the-daemon-hung-overnight-and-nothing-could-say-what-it-was-waiting-on) | The daemon hung overnight, and nothing could say what it was waiting on | The cause is not diagnosed; the daemon now reports where it is stuck and is restarted |
|
||||
| [B67](#b67--fresh-state-restore-can-silently-select-a-new-device-high-22) | Fresh-state restore can silently select a new device (HIGH-22) | An ambiguous device after a loss of local state is not asked about |
|
||||
@@ -109,6 +109,8 @@ a live account since.
|
||||
|
||||
| Entry | Title |
|
||||
|---|---|
|
||||
| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open |
|
||||
| [B274](#b274--opening-a-file-for-writing-fails-with-eio-while-its-queued-revision-lands) | Opening a file for writing fails with EIO while its queued revision lands |
|
||||
| [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing |
|
||||
| [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" |
|
||||
| [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running |
|
||||
@@ -375,6 +377,56 @@ a live account since.
|
||||
|
||||
Newest first.
|
||||
|
||||
### B275 — On a slow link, a file's last write can land only in a conflict copy
|
||||
|
||||
**Status:** Open
|
||||
**Found:** 2026-10-31, CI on #60: `one_client_on_a_slow_link`, profile `one-client-wifi` seed 2,
|
||||
failed with `Drive does not hold what the clients wrote, 120s after the links came up`. The
|
||||
same seed passed 4 times locally, and #60 changes no daemon logic, so the failure depends on
|
||||
timing.
|
||||
**Where:** not located; the drain's conflict check in `crates/pdfs-fuse/src/drain.rs` is the
|
||||
first suspect
|
||||
|
||||
**Repro:** Only the CI run above. The last steps created `x/z/y/a.txt` (37 bytes), wrote 42
|
||||
bytes into it at offset 20, and brought every link up. Drive then held the file at its first
|
||||
37 bytes, and the full content was in `x/z/y/a (sync-conflict …).txt`. No other client wrote
|
||||
to it.
|
||||
|
||||
**Cause.** Not known. Nothing was lost, since the conflict copy holds the bytes, but the file
|
||||
conflicted with its own create. B183 and B184 found the same symptom after a lost create
|
||||
reply.
|
||||
|
||||
**Fix.** Not done. Needs a reproduction: run the seed in a loop under load and keep the state
|
||||
directory of a failing run.
|
||||
|
||||
---
|
||||
|
||||
### B274 — Opening a file for writing fails with EIO while its queued revision lands
|
||||
|
||||
**Status:** Fixed, not verified live
|
||||
**Found:** 2026-10-31, CI: the simulation
|
||||
`a_file_whose_upload_was_not_read_back_takes_two_writes_at_once` failed once with
|
||||
`Input/output error` on a write, after `copy queued revision into write scratch failed … No
|
||||
such file or directory`. It did not fail in 54 local runs.
|
||||
**Where:** the write open in `crates/pdfs-fuse/src/filesystem.rs`
|
||||
|
||||
**Repro:** Write a file, and open it for writing again at the moment its queued revision
|
||||
finishes uploading. The open fails with EIO, and the program sees its save fail.
|
||||
|
||||
**Cause.** The open samples the queued revision, then copies its blob into the new handle's
|
||||
scratch file. The drain lets go of the pending entry and then deletes the blob when the upload
|
||||
lands. A blob deleted between the sample and the copy failed the open, although the re-check
|
||||
just after the copy already retries a revision that changed during it.
|
||||
|
||||
**Fix.** A failed copy checks whether the pending entry still names that blob. When it does
|
||||
not, the revision landed or was replaced, and the open goes around again with the base the
|
||||
file has now, as the re-check does. Since the drain drops the entry before the blob, a blob
|
||||
that is gone and still named is a real failure and still returns EIO.
|
||||
|
||||
**Test:** none deterministic; the simulation above covers the window.
|
||||
|
||||
---
|
||||
|
||||
### B273 — A restore proposes another computer's paths when the two share a hostname
|
||||
|
||||
**Status:** Open
|
||||
@@ -566,7 +618,7 @@ error, reads as offline. That covers every caller at once, the database ones inc
|
||||
|
||||
### B265 — Address keys whose token is for an inactive account key never open
|
||||
|
||||
**Status:** Open
|
||||
**Status:** Fixed, not verified live
|
||||
**Found:** 2026-10-31, code audit after issue #40: a Duo account whose mount failed on every
|
||||
start with `fetch My Files root: cryptography error: failed to decrypt: missing key`, after three
|
||||
`failed to decrypt address key token` warnings. The web app worked. B210's fix does not apply,
|
||||
@@ -583,8 +635,17 @@ already reports "token is for inactive account key", so the case is known. Separ
|
||||
`addresses()` fails as a whole through `?` when one address has no usable primary key, which
|
||||
takes the working addresses down with it.
|
||||
|
||||
**Fix.** Not done, and it belongs in the SDK. Use every account key that opens for decrypting
|
||||
tokens, and only the active ones for signing. Skip and log an address that cannot be opened.
|
||||
**Fix.** In `proton-sdk` 0.10.1 (narrrl/proton-sdk-rs#3). Tokens are tried with every account
|
||||
key that opens, active ones first, and signing uses active keys only. An address that cannot be
|
||||
opened is left out with a warning, and the others work; `addresses()` fails only when none
|
||||
opens. When the left-out address is the default, `default_address` fails with the reason
|
||||
rather than signing with another address. `key_report` walks the same chain, so `pdfs diagnose`
|
||||
reports such a key as opening. Not verified against an account with an inactive key, since
|
||||
none is at hand; issue #40's reporter can confirm.
|
||||
|
||||
**Test:** `an_address_key_whose_token_is_for_an_inactive_account_key_opens`,
|
||||
`an_address_whose_keys_do_not_open_leaves_the_others_working` and
|
||||
`an_account_whose_addresses_all_fail_to_open_is_an_error` in `proton-sdk`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user