fixed a migration bug where ProtonDrive becomse unwritable

This commit is contained in:
nvrl committed 2026-07-30 02:11:23 +02:00
1 parent 77198ed261
commit 9bbd8eb982
4 files changed
+65 -9

No files matched your search

+23 -1
View File
@@ -13,6 +13,7 @@ jobs:
build-release: build-release:
name: Build and Release name: Build and Release
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: 'rpm signing'
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -30,7 +31,8 @@ jobs:
libsecret-1-dev \ libsecret-1-dev \
libdbus-1-dev \ libdbus-1-dev \
desktop-file-utils \ desktop-file-utils \
rpm rpm \
rpm-sign
- name: Set up Rust - name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
@@ -154,6 +156,26 @@ jobs:
# Copy built RPM to root directory # Copy built RPM to root directory
cp rpmbuild/RPMS/x86_64/*.rpm . cp rpmbuild/RPMS/x86_64/*.rpm .
- name: Sign Fedora package (.rpm)
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
if [ -n "$GPG_PRIVATE_KEY" ] && [ -n "$GPG_KEY_NAME" ]; then
echo "$GPG_PRIVATE_KEY" | gpg --import
cat <<EOF > ~/.rpmmacros
%_signature gpg
%_gpg_name ${GPG_KEY_NAME}
%__gpg_sign_cmd %{__gpg} gpg --batch --verbose --no-armor --pinentry-mode loopback --passphrase %{_gpg_passphrase} --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} --digest-algo sha256 %{__plaintext_filename}
EOF
rpm --addsign --define "_gpg_passphrase $GPG_PASSPHRASE" *.rpm
else
echo "Skipping RPM signing: GPG_PRIVATE_KEY or GPG_KEY_NAME secret is not set."
fi
- name: Upload Workflow Artifacts - name: Upload Workflow Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+9 -1
View File
@@ -992,7 +992,15 @@ impl ProtonFs {
warn!(uid = %root.uid, error = %e, "db upsert root failed"); warn!(uid = %root.uid, error = %e, "db upsert root failed");
} }
st.by_uid.insert(root.uid.clone(), ROOT_INO); st.by_uid.insert(root.uid.clone(), ROOT_INO);
let access = st.access_for_node(ROOT_INO, &root); // The filesystem root is always owned — it must never be
// classified as a shared root, even when the API returns
// membership data (which happens when the owner shares the
// folder with someone else). Clean up any stale share_access
// row that a previous downgrade_all_share_access may have
// written for this uid, so hydrate_access cannot pick it up.
let access = Access::Owner;
st.share_access.remove(&root.uid);
let _ = st.db.delete_share_access(&root.uid);
st.entries.insert( st.entries.insert(
ROOT_INO, ROOT_INO,
Entry { Entry {
+18 -7
View File
@@ -448,8 +448,13 @@ impl State {
let roots: Vec<u64> = self let roots: Vec<u64> = self
.entries .entries
.iter() .iter()
.filter(|entry| { .filter(|&(&ino, entry)| {
let (_, entry) = entry; // The filesystem root (parent == self) is always owned; never
// downgrade it even if it carries membership from the owner
// sharing this folder with someone else.
if entry.parent == ino {
return false;
}
entry.access != Access::Owner entry.access != Access::Owner
|| entry.node.membership.is_some() || entry.node.membership.is_some()
|| self.share_access.contains_key(&entry.uid) || self.share_access.contains_key(&entry.uid)
@@ -494,10 +499,12 @@ impl State {
return Access::Unknown; return Access::Unknown;
} }
let stored = state.share_access.get(&entry.uid).copied(); let stored = state.share_access.get(&entry.uid).copied();
let access = if let Some(stored) = stored { // The root of this mount is always owned, regardless of any
stored // stale share_access row that a global downgrade may have left.
} else if entry.parent == ino { let access = if entry.parent == ino {
Access::Owner Access::Owner
} else if let Some(stored) = stored {
stored
} else if !state.entries.contains_key(&entry.parent) { } else if !state.entries.contains_key(&entry.parent) {
entry entry
.node .node
@@ -538,8 +545,12 @@ impl State {
} }
let discovered: Vec<(NodeUid, Access)> = self let discovered: Vec<(NodeUid, Access)> = self
.entries .entries
.values() .iter()
.filter_map(|entry| { .filter_map(|(&ino, entry)| {
// The filesystem root is owned, not a discovered share root.
if entry.parent == ino {
return None;
}
let parent_access = self let parent_access = self
.entries .entries
.get(&entry.parent) .get(&entry.parent)
+15
View File
@@ -12,6 +12,21 @@ Conventions:
--- ---
## B78 — Profile backup fails: Cannot create file at the root of a device
**Status:** Open
**Found:** 2026-07-29, user reported `upload profile: proton api error NotEnoughPermissions (http 422): Cannot create file at the root of a device`
The background task that backs up the machine's profile (sync folder mappings, pins, cache budget) currently attempts to write `profile.json` directly to the device root node (`device.root_uid`) on the Drive backend. The Proton Drive API has started enforcing a restriction that files cannot be created directly at the root of a device; they must be placed inside a folder.
**Consequence:** The upload is rejected with HTTP 422. The daemon retries when changes happen but the profile is never successfully backed up. Local changes (pinning, adding a sync folder) still take effect locally, but they will not be restorable on a new machine.
**Planned Fix:** Match the original `RECOVERY.md` spec by putting it in a `.proton-drive-linux` folder within the device root.
- `pdfs-fuse/src/profile.rs:save_profile`: Look up (or create) the `.proton-drive-linux` folder under the device root, and upload `profile.json` there instead.
- `pdfs-fuse/src/profile.rs:load_profile`: Check for `PROFILE_FILE_NAME` inside `.proton-drive-linux` first, falling back to the device root to allow older, existing profiles to be restored until they are overwritten.
---
## B1 — FUSE rename loses the file (data loss) ## B1 — FUSE rename loses the file (data loss)
**Status:** Fixed (verified 2026-07-20) **Status:** Fixed (verified 2026-07-20)