mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 08:56:59 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b81b76cb7 | ||
|
|
7470bd89f3 | ||
|
|
e6cb52339d | ||
|
|
912acc760a |
No files matched your search
Generated
+4
-4
@@ -3449,7 +3449,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-cli"
|
name = "pdfs-cli"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3464,7 +3464,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-core"
|
name = "pdfs-core"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dbus-secret-service-keyring-store",
|
"dbus-secret-service-keyring-store",
|
||||||
@@ -3488,7 +3488,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-fuse"
|
name = "pdfs-fuse"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"fuser",
|
"fuser",
|
||||||
@@ -3508,7 +3508,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-gui"
|
name = "pdfs-gui"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "2"
|
|||||||
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
|
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Nils Pukropp <contact@narl.io>"]
|
authors = ["Nils Pukropp <contact@narl.io>"]
|
||||||
|
|||||||
@@ -429,10 +429,12 @@ pub enum Request {
|
|||||||
/// Retry a backed-off queued op now instead of waiting out its backoff;
|
/// Retry a backed-off queued op now instead of waiting out its backoff;
|
||||||
/// every failed op when `id` is `None`. Replies with [`Response::Ok`].
|
/// every failed op when `id` is `None`. Replies with [`Response::Ok`].
|
||||||
RetryPendingOp { id: Option<i64> },
|
RetryPendingOp { id: Option<i64> },
|
||||||
/// List the `(sync-conflict …)` copies under My Files. Replies with
|
/// List the `(sync-conflict …)` copies under My Files and in synced
|
||||||
|
/// folders. Replies with
|
||||||
/// [`Response::Conflicts`].
|
/// [`Response::Conflicts`].
|
||||||
ListConflicts,
|
ListConflicts,
|
||||||
/// Resolve one conflict copy (`path` mountpoint-relative or absolute).
|
/// Resolve one conflict copy (`path` mountpoint-relative, or absolute for a
|
||||||
|
/// copy in a synced folder).
|
||||||
/// Everything removed goes to the Proton trash. Replies with
|
/// Everything removed goes to the Proton trash. Replies with
|
||||||
/// [`Response::Ok`].
|
/// [`Response::Ok`].
|
||||||
ResolveConflict { path: String, keep: ConflictKeep },
|
ResolveConflict { path: String, keep: ConflictKeep },
|
||||||
@@ -701,9 +703,10 @@ pub struct DeviceInfo {
|
|||||||
/// [`Response::Conflicts`]).
|
/// [`Response::Conflicts`]).
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct ConflictInfo {
|
pub struct ConflictInfo {
|
||||||
/// Mountpoint-relative path of the copy.
|
/// Mountpoint-relative path of the copy, or its absolute local path when it
|
||||||
|
/// sits in a synced folder.
|
||||||
pub path: String,
|
pub path: String,
|
||||||
/// Mountpoint-relative path of the file it is a copy of.
|
/// Path of the file it is a copy of, in the same form as `path`.
|
||||||
pub original_path: String,
|
pub original_path: String,
|
||||||
/// False when the original is gone and only the copy is left.
|
/// False when the original is gone and only the copy is left.
|
||||||
pub original_exists: bool,
|
pub original_exists: bool,
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ impl Db {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the kept log already holds an entry of `kind` for `target`. Lets
|
||||||
|
/// the daemon report something once across restarts rather than once per
|
||||||
|
/// run, without keeping a second table in step with the log.
|
||||||
|
pub fn activity_has(&self, kind: ActivityKind, target: &str) -> Result<bool> {
|
||||||
|
let kind = serde_json::to_string(&kind)?;
|
||||||
|
let conn = self.read();
|
||||||
|
let mut stmt =
|
||||||
|
conn.prepare("SELECT 1 FROM activity WHERE kind = ?1 AND target = ?2 LIMIT 1")?;
|
||||||
|
Ok(stmt.exists(params![kind, target])?)
|
||||||
|
}
|
||||||
|
|
||||||
/// The most recent activity, newest first, capped at `limit` entries. Rows
|
/// The most recent activity, newest first, capped at `limit` entries. Rows
|
||||||
/// whose stored `kind` no longer parses (written by an older build) are
|
/// whose stored `kind` no longer parses (written by an older build) are
|
||||||
/// skipped rather than failing the whole read.
|
/// skipped rather than failing the whole read.
|
||||||
|
|||||||
@@ -1414,6 +1414,29 @@ fn activity_reads_back_newest_first() {
|
|||||||
assert_eq!(db.activity_list(1).unwrap().len(), 1);
|
assert_eq!(db.activity_list(1).unwrap().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn activity_has_finds_only_matching_kind_and_target() {
|
||||||
|
let db = Db::open_in_memory().unwrap();
|
||||||
|
db.activity_add(&activity(
|
||||||
|
"a (sync-conflict 1).txt",
|
||||||
|
ActivityKind::Conflict,
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
db.activity_add(&activity("b.txt", ActivityKind::Upload, true))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
db.activity_has(ActivityKind::Conflict, "a (sync-conflict 1).txt")
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!db.activity_has(ActivityKind::Upload, "a (sync-conflict 1).txt")
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(!db.activity_has(ActivityKind::Conflict, "b.txt").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn activity_prunes_to_the_keep_limit() {
|
fn activity_prunes_to_the_keep_limit() {
|
||||||
let db = Db::open_in_memory().unwrap();
|
let db = Db::open_in_memory().unwrap();
|
||||||
|
|||||||
@@ -14,15 +14,38 @@
|
|||||||
//! that is open, being written, or still owed an upload, for the same reason the
|
//! that is open, being written, or still owed an upload, for the same reason the
|
||||||
//! sweep does: the node is about to change under the decision.
|
//! sweep does: the node is about to change under the decision.
|
||||||
//!
|
//!
|
||||||
//! Only copies under My Files are handled here. A copy inside a mirror folder is
|
//! Copies under My Files are named by their mountpoint-relative path. Copies
|
||||||
//! a local file first; the sync engine owns it.
|
//! inside a synced folder are named by their absolute local path, so a resolve
|
||||||
|
//! request routes to the on-demand mount that serves them. A copy inside a
|
||||||
|
//! mirror folder is listed too, but it is a local file first: the sync engine
|
||||||
|
//! owns it, so it is resolved by editing the folder, not through here.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use pdfs_core::control::{ConflictInfo, ConflictKeep};
|
use pdfs_core::control::{ConflictInfo, ConflictKeep};
|
||||||
use sweep::conflict_base_name;
|
use sweep::conflict_base_name;
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
/// Every conflict copy under My Files, with the file it is a copy of.
|
/// Where the user finds a conflict copy: its mountpoint-relative path under
|
||||||
|
/// My Files, or its absolute local path inside a synced folder. `None` for a
|
||||||
|
/// node no location of this machine shows, such as one in another
|
||||||
|
/// computer's device tree.
|
||||||
|
///
|
||||||
|
/// The sweep only flags what this can name, and the listing uses it too. A
|
||||||
|
/// flagged copy the listing cannot see reads as resolved in the activity
|
||||||
|
/// feed while it still sits in the folder (`docs/BUGS.md` B99).
|
||||||
|
pub(crate) fn conflict_path(&self, roots: &SearchRoots, uid: &NodeUid) -> Option<String> {
|
||||||
|
let uid = uid.to_string();
|
||||||
|
if let Ok(Some(path)) = self
|
||||||
|
.db
|
||||||
|
.path_relative_to(&self.primary_root_uid.to_string(), &uid)
|
||||||
|
{
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
let drive_path = self.db.node_path(&uid).ok().flatten()?;
|
||||||
|
roots.resolve(&drive_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every conflict copy this machine shows, with the file it is a copy of.
|
||||||
pub(crate) fn list_conflicts(&self) -> CoreResult<Vec<ConflictInfo>> {
|
pub(crate) fn list_conflicts(&self) -> CoreResult<Vec<ConflictInfo>> {
|
||||||
let stored = self
|
let stored = self
|
||||||
.db
|
.db
|
||||||
@@ -37,13 +60,13 @@ impl Core {
|
|||||||
for node in &nodes {
|
for node in &nodes {
|
||||||
by_parent_name.insert((node.parent_uid.as_ref(), node.name.as_str()), node);
|
by_parent_name.insert((node.parent_uid.as_ref(), node.name.as_str()), node);
|
||||||
}
|
}
|
||||||
let root = self.primary_root_uid.to_string();
|
let roots = self.search_roots();
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
for node in &nodes {
|
for node in &nodes {
|
||||||
let Some(base) = conflict_base_name(&node.name) else {
|
let Some(base) = conflict_base_name(&node.name) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Ok(Some(path)) = self.db.path_relative_to(&root, &node.uid.to_string()) else {
|
let Some(path) = self.conflict_path(&roots, &node.uid) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let original = by_parent_name
|
let original = by_parent_name
|
||||||
|
|||||||
@@ -185,6 +185,38 @@ fn route_to_mount(core: &Core, mountpoint: &Path, path: &str) -> CoreResult<(Cor
|
|||||||
rel_to_mount(mountpoint, path).map(|rel| (core.clone(), rel))
|
rel_to_mount(mountpoint, path).map(|rel| (core.clone(), rel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`route_to_mount`] for a conflict copy. The listing names a copy inside a
|
||||||
|
/// synced folder by its absolute local path (`docs/BUGS.md` B99), so the
|
||||||
|
/// resolve has to run against the on-demand mount serving it. A mirror folder
|
||||||
|
/// has no mount to run against: its copy is an ordinary local file.
|
||||||
|
fn route_conflict(core: &Core, mountpoint: &Path, path: &str) -> CoreResult<(Core, PathBuf)> {
|
||||||
|
let routed = route_to_mount(core, mountpoint, path).map_err(|e| {
|
||||||
|
let p = Path::new(path);
|
||||||
|
let mirrored = core
|
||||||
|
.db
|
||||||
|
.sync_folder_list()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.any(|folder| folder.mode == "mirror" && p.starts_with(&folder.local_path));
|
||||||
|
if mirrored {
|
||||||
|
CoreError::invalid(format!(
|
||||||
|
"{path} is in a mirrored folder; rename or delete the local file there"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
e
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if routed
|
||||||
|
.1
|
||||||
|
.components()
|
||||||
|
.all(|component| matches!(component, Component::Normal(_) | Component::CurDir))
|
||||||
|
{
|
||||||
|
Ok(routed)
|
||||||
|
} else {
|
||||||
|
Err(CoreError::invalid(format!("{path} escapes the mountpoint")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Turn a CLI-supplied path into a mountpoint-relative path. An absolute path
|
/// Turn a CLI-supplied path into a mountpoint-relative path. An absolute path
|
||||||
/// must live under `mountpoint`; a relative path is taken as already relative to
|
/// must live under `mountpoint`; a relative path is taken as already relative to
|
||||||
/// the mount root.
|
/// the mount root.
|
||||||
@@ -1123,13 +1155,15 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
Ok(items) => CtlResponse::Conflicts { items },
|
Ok(items) => CtlResponse::Conflicts { items },
|
||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
},
|
},
|
||||||
Ok(CtlRequest::ResolveConflict { path, keep }) => match rel_to_mount(mountpoint, &path) {
|
Ok(CtlRequest::ResolveConflict { path, keep }) => {
|
||||||
Ok(rel) => match core.resolve_conflict(&rel, &keep) {
|
match route_conflict(core, mountpoint, &path) {
|
||||||
Ok(message) => CtlResponse::Ok { message },
|
Ok((core, rel)) => match core.resolve_conflict(&rel, &keep) {
|
||||||
|
Ok(message) => CtlResponse::Ok { message },
|
||||||
|
Err(e) => CtlResponse::error(e),
|
||||||
|
},
|
||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
},
|
}
|
||||||
Err(e) => CtlResponse::error(e),
|
}
|
||||||
},
|
|
||||||
Ok(CtlRequest::ListPendingOps) => match core.pending_op_infos() {
|
Ok(CtlRequest::ListPendingOps) => match core.pending_op_infos() {
|
||||||
Ok(items) => CtlResponse::PendingOps { items },
|
Ok(items) => CtlResponse::PendingOps { items },
|
||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
|
|||||||
@@ -558,9 +558,10 @@ struct Core {
|
|||||||
/// instead of polling or blocking for the whole run. See
|
/// instead of polling or blocking for the whole run. See
|
||||||
/// [`Core::await_trash_refresh`].
|
/// [`Core::await_trash_refresh`].
|
||||||
trash_progress: Arc<tokio::sync::Notify>,
|
trash_progress: Arc<tokio::sync::Notify>,
|
||||||
/// Conflict copies the sweep has already flagged as needing attention this
|
/// Conflict copies the sweep has already checked against the activity log
|
||||||
/// run, so a divergent `(sync-conflict …)` file is logged once rather than
|
/// this run, so each pass does not ask the database again. The log itself
|
||||||
/// on every sweep pass. See [`Core::run_conflict_sweep_loop`].
|
/// is what keeps a copy from being flagged again after a restart. See
|
||||||
|
/// [`Core::run_conflict_sweep_loop`] and `docs/BUGS.md` B99.
|
||||||
conflict_notified: Arc<Mutex<HashSet<NodeUid>>>,
|
conflict_notified: Arc<Mutex<HashSet<NodeUid>>>,
|
||||||
/// Whether the conflict sweep may actually trash the duplicates it finds, or
|
/// Whether the conflict sweep may actually trash the duplicates it finds, or
|
||||||
/// only report them. Resolved once at mount from config + environment
|
/// only report them. Resolved once at mount from config + environment
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ impl Core {
|
|||||||
by_parent_name.insert((parent, node.name.as_str()), node);
|
by_parent_name.insert((parent, node.name.as_str()), node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let roots = self.search_roots();
|
||||||
let online = self.online.load(std::sync::atomic::Ordering::Relaxed);
|
let online = self.online.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
for node in &nodes {
|
for node in &nodes {
|
||||||
if node.trashed || node.is_folder() {
|
if node.trashed || node.is_folder() {
|
||||||
@@ -132,7 +133,16 @@ impl Core {
|
|||||||
self.remove_conflict_copy(node, &base);
|
self.remove_conflict_copy(node, &base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SweepAction::Flag(detail) => self.flag_conflict(&node.uid, &node.name, detail),
|
// Only flag what the conflict listing can show. Anything else
|
||||||
|
// would sit in the feed as "resolved" with no way to act on
|
||||||
|
// it (`docs/BUGS.md` B99).
|
||||||
|
SweepAction::Flag(detail) => {
|
||||||
|
if self.conflict_path(&roots, &node.uid).is_some() {
|
||||||
|
self.flag_conflict(&node.uid, &node.name, detail);
|
||||||
|
} else {
|
||||||
|
debug!(uid = %node.uid, name = %node.name, "conflict sweep: copy is outside every location; not flagged");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,12 +283,21 @@ impl Core {
|
|||||||
busy
|
busy
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surface a conflict copy that needs the user's attention, at most once per
|
/// Surface a conflict copy that needs the user's attention, once. The
|
||||||
/// daemon run so the activity feed is not spammed each pass.
|
/// in-memory set spares the database each pass; the activity log itself is
|
||||||
|
/// what keeps a restart from logging the same copy again (`docs/BUGS.md`
|
||||||
|
/// B99). A copy's name carries its own timestamp, so the name identifies it.
|
||||||
fn flag_conflict(&self, uid: &NodeUid, name: &str, detail: String) {
|
fn flag_conflict(&self, uid: &NodeUid, name: &str, detail: String) {
|
||||||
if !self.conflict_notified.lock().insert(uid.clone()) {
|
if !self.conflict_notified.lock().insert(uid.clone()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
match self.db.activity_has(ActivityKind::Conflict, name) {
|
||||||
|
Ok(false) => {}
|
||||||
|
Ok(true) => return,
|
||||||
|
// Cannot tell: a repeat entry is harmless, a missing one hides a
|
||||||
|
// conflict.
|
||||||
|
Err(e) => warn!(%uid, name, error = ?e, "conflict sweep: activity check failed"),
|
||||||
|
}
|
||||||
debug!(%uid, name, detail, "conflict sweep: flagged divergent conflict copy");
|
debug!(%uid, name, detail, "conflict sweep: flagged divergent conflict copy");
|
||||||
self.log_activity(ActivityKind::Conflict, name, detail, false);
|
self.log_activity(ActivityKind::Conflict, name, detail, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=resources/pdfs.gresource.xml");
|
println!("cargo:rerun-if-changed=resources/pdfs.gresource.xml");
|
||||||
println!("cargo:rerun-if-changed=resources/icons");
|
println!("cargo:rerun-if-changed=resources/icons");
|
||||||
println!("cargo:rerun-if-changed=resources/style.css");
|
println!("cargo:rerun-if-changed=resources/style.css");
|
||||||
|
println!("cargo:rerun-if-changed=../../packaging/io.narl.proton-drive-linux.svg");
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
<file preprocess="xml-stripblanks">scalable/actions/pdfs-upload-symbolic.svg</file>
|
<file preprocess="xml-stripblanks">scalable/actions/pdfs-upload-symbolic.svg</file>
|
||||||
<file preprocess="xml-stripblanks">scalable/actions/pdfs-offline-symbolic.svg</file>
|
<file preprocess="xml-stripblanks">scalable/actions/pdfs-offline-symbolic.svg</file>
|
||||||
<file preprocess="xml-stripblanks">scalable/actions/pdfs-online-only-symbolic.svg</file>
|
<file preprocess="xml-stripblanks">scalable/actions/pdfs-online-only-symbolic.svg</file>
|
||||||
|
<!-- The app icon, from the one file packaging installs, so the in-app brand
|
||||||
|
matches the launcher and resolves even when nothing is installed. -->
|
||||||
|
<file alias="scalable/apps/io.narl.proton-drive-linux.svg" preprocess="xml-stripblanks">../../../packaging/io.narl.proton-drive-linux.svg</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
<!-- The app stylesheet, loaded once at startup. Colours come from the
|
<!-- The app stylesheet, loaded once at startup. Colours come from the
|
||||||
libadwaita palette so the app follows the system accent, light/dark and
|
libadwaita palette so the app follows the system accent, light/dark and
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
/* Brand */
|
/* Brand */
|
||||||
.brand-title { font-size: 1.6rem; font-weight: 800; }
|
.brand-title { font-size: 1.6rem; font-weight: 800; }
|
||||||
.brand-icon { color: #6d4aff; }
|
|
||||||
|
|
||||||
/* Sidebar footer */
|
/* Sidebar footer */
|
||||||
.sidebar-footer { padding: 6px 12px 12px 12px; }
|
.sidebar-footer { padding: 6px 12px 12px 12px; }
|
||||||
|
|||||||
@@ -884,8 +884,8 @@ fn build_sidebar(footer: >k4::Box) -> (adw::NavigationPage, gtk4::ListBox) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let brand = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
let brand = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||||
let icon = gtk4::Image::from_icon_name("folder-remote-symbolic");
|
let icon = gtk4::Image::from_icon_name(APP_ID);
|
||||||
icon.add_css_class("brand-icon");
|
icon.set_pixel_size(24);
|
||||||
brand.append(&icon);
|
brand.append(&icon);
|
||||||
let name = gtk4::Label::new(Some("Proton Drive"));
|
let name = gtk4::Label::new(Some("Proton Drive"));
|
||||||
name.add_css_class("heading");
|
name.add_css_class("heading");
|
||||||
|
|||||||
@@ -52,9 +52,8 @@ pub(crate) fn build_login_page() -> (gtk4::Widget, LoginState) {
|
|||||||
login_status.add_css_class("dim-label");
|
login_status.add_css_class("dim-label");
|
||||||
|
|
||||||
let header = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
let header = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
||||||
let logo = gtk4::Image::from_icon_name("folder-remote-symbolic");
|
let logo = gtk4::Image::from_icon_name(APP_ID);
|
||||||
logo.set_pixel_size(64);
|
logo.set_pixel_size(96);
|
||||||
logo.add_css_class("brand-icon");
|
|
||||||
let title = gtk4::Label::new(Some("Proton Drive"));
|
let title = gtk4::Label::new(Some("Proton Drive"));
|
||||||
title.add_css_class("brand-title");
|
title.add_css_class("brand-title");
|
||||||
header.append(&logo);
|
header.append(&logo);
|
||||||
|
|||||||
@@ -12,6 +12,42 @@ Conventions:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## B99 — Conflict copies in synced folders are logged again on every start and shown as "Resolved"
|
||||||
|
|
||||||
|
**Status:** Fixed (unverified). No daemon carrying the change has been restarted against the
|
||||||
|
affected copies yet.
|
||||||
|
**Found:** 2026-09-27. The Activity page listed the same six conflict copies every day, e.g.
|
||||||
|
`Conflict [EMBER] Oshi no Ko S2 - 12 (sync-conflict 1786815864).mkv` with `Differs from … · 24 times
|
||||||
|
since Sep 20, 13:13 · Resolved`. `find` showed all six copies still on disk, in `~/Videos` and
|
||||||
|
`~/Documents`. Both are on-demand sync folders. `pdfs conflicts` answered `No conflicts.`. The
|
||||||
|
journal showed the daemon mounting at 17:29:33, and the entries were stamped 17:30. That is the
|
||||||
|
sweep's 30-second warmup.
|
||||||
|
|
||||||
|
**Where:** `crates/pdfs-fuse/src/sweep.rs` (`flag_conflict`, `sweep_conflicts_once`),
|
||||||
|
`crates/pdfs-fuse/src/conflicts.rs` (`list_conflicts`), `crates/pdfs-fuse/src/control.rs`
|
||||||
|
(`ResolveConflict`).
|
||||||
|
|
||||||
|
**Cause.** There were two faults.
|
||||||
|
|
||||||
|
- The sweep kept its "already flagged" set only in memory. Every daemon start forgot it, so the
|
||||||
|
first pass logged every open conflict again.
|
||||||
|
- The sweep flags copies anywhere in the account's own tree. `list_conflicts` only reported copies
|
||||||
|
below My Files, and resolving only accepted paths under the primary mount. A copy in a synced
|
||||||
|
folder was logged but never listed, so the GUI read it as resolved. It also could not be
|
||||||
|
resolved from the GUI or the CLI.
|
||||||
|
|
||||||
|
**Fix.** `Core::conflict_path` names a copy by its mountpoint-relative path under My Files, or
|
||||||
|
by its absolute local path inside a synced folder. It reuses the roots that search already
|
||||||
|
resolves hits through. The listing uses it, and the sweep flags only copies it can name. So the
|
||||||
|
feed never holds a conflict the listing cannot answer for. `ResolveConflict` routes an absolute
|
||||||
|
path to the on-demand mount that serves it. For a copy in a mirror folder, it says to edit the
|
||||||
|
local file instead. Before logging, `flag_conflict` checks the activity log
|
||||||
|
(`Db::activity_has`), so a restart does not log the same copy again. The copy's name carries its
|
||||||
|
own timestamp, so the name identifies the copy. No schema change.
|
||||||
|
Regression test: `activity_has_finds_only_matching_kind_and_target`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## B98 — A superseded upload spins its drain thread forever and shows as "Uploading" at 0 bytes
|
## B98 — A superseded upload spins its drain thread forever and shows as "Uploading" at 0 bytes
|
||||||
|
|
||||||
**Status:** Fixed (unverified) — the daemon carrying the change has not been driven against a
|
**Status:** Fixed (unverified) — the daemon carrying the change has not been driven against a
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.2.2] — 2026-09-27
|
||||||
|
|
||||||
|
Schema: **32** (unchanged).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Conflicts in synced folders.** A conflict copy inside a synced folder is now listed under
|
||||||
|
Needs attention and can be resolved there or with `pdfs conflicts`. It used to be logged again
|
||||||
|
after every restart and marked "Resolved" while it was still on disk (B99).
|
||||||
|
|
||||||
## [2.2.1] — 2026-09-24
|
## [2.2.1] — 2026-09-24
|
||||||
|
|
||||||
Schema: **32** (unchanged).
|
Schema: **32** (unchanged).
|
||||||
@@ -1038,6 +1047,7 @@ First stable release: FUSE files-on-demand mount, sync daemon under `proton-driv
|
|||||||
- The outstanding FUSE defects tracked in `docs/BUGS.md`, plus a truncate defect, validated
|
- The outstanding FUSE defects tracked in `docs/BUGS.md`, plus a truncate defect, validated
|
||||||
by a new POSIX compliance suite for the filesystem.
|
by a new POSIX compliance suite for the filesystem.
|
||||||
|
|
||||||
|
[2.2.2]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.2.2
|
||||||
[2.2.1]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.2.1
|
[2.2.1]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.2.1
|
||||||
[2.1.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.1.0
|
[2.1.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.1.0
|
||||||
[2.0.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.0.0
|
[2.0.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.0.0
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
# Build + install: cd packaging && makepkg -fi
|
# Build + install: cd packaging && makepkg -fi
|
||||||
|
|
||||||
pkgname=proton-drive-linux
|
pkgname=proton-drive-linux
|
||||||
pkgver=2.2.1
|
pkgver=2.2.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray"
|
pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = proton-drive-for-linux-bin
|
pkgbase = proton-drive-for-linux-bin
|
||||||
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)
|
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)
|
||||||
pkgver = 2.2.0
|
pkgver = 2.2.1
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/narrrl/proton-drive-linux
|
url = https://github.com/narrrl/proton-drive-linux
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -20,21 +20,21 @@ pkgbase = proton-drive-for-linux-bin
|
|||||||
optdepends = xdg-utils: "Open folder" action in the tray menu
|
optdepends = xdg-utils: "Open folder" action in the tray menu
|
||||||
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
||||||
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
||||||
provides = proton-drive-for-linux=2.2.0
|
provides = proton-drive-for-linux=2.2.1
|
||||||
provides = proton-drive-linux=2.2.0
|
provides = proton-drive-linux=2.2.1
|
||||||
provides = pdfs
|
provides = pdfs
|
||||||
conflicts = proton-drive-linux
|
conflicts = proton-drive-linux
|
||||||
conflicts = proton-drive-for-linux
|
conflicts = proton-drive-for-linux
|
||||||
conflicts = proton-drive-for-linux-git
|
conflicts = proton-drive-for-linux-git
|
||||||
options = !strip
|
options = !strip
|
||||||
options = !debug
|
options = !debug
|
||||||
source = proton-drive-linux-2.2.0-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v2.2.0/proton-drive-linux-2.2.0-x86_64.tar.gz
|
source = proton-drive-linux-2.2.1-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v2.2.1/proton-drive-linux-2.2.1-x86_64.tar.gz
|
||||||
source = io.narl.proton-drive-linux.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.0/packaging/io.narl.proton-drive-linux.desktop
|
source = io.narl.proton-drive-linux.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.1/packaging/io.narl.proton-drive-linux.desktop
|
||||||
source = io.narl.proton-drive-linux-tray.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.0/packaging/io.narl.proton-drive-linux-tray.desktop
|
source = io.narl.proton-drive-linux-tray.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.1/packaging/io.narl.proton-drive-linux-tray.desktop
|
||||||
source = io.narl.proton-drive-linux.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.0/packaging/io.narl.proton-drive-linux.svg
|
source = io.narl.proton-drive-linux.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.1/packaging/io.narl.proton-drive-linux.svg
|
||||||
source = proton-drive.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.0/packaging/proton-drive.service
|
source = proton-drive.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.1/packaging/proton-drive.service
|
||||||
source = LICENSE-2.2.0::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.0/LICENSE
|
source = LICENSE-2.2.1::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.2.1/LICENSE
|
||||||
sha256sums = 4d746d83abd5174acb324236b6146c083a43b8c25e8d30e31c2a6d5b594ba479
|
sha256sums = 69f0d6996feb95b18d81bb96e9373c61fddb64ab17b50c94cd83ef8d5e0a1425
|
||||||
sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123
|
sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123
|
||||||
sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0
|
sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0
|
||||||
sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828
|
sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Nils Pukropp <contact@narl.io>
|
# Maintainer: Nils Pukropp <contact@narl.io>
|
||||||
|
|
||||||
pkgname=proton-drive-for-linux-bin
|
pkgname=proton-drive-for-linux-bin
|
||||||
pkgver=2.2.0
|
pkgver=2.2.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)"
|
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -30,7 +30,7 @@ source=("proton-drive-linux-$pkgver-x86_64.tar.gz::$url/releases/download/$_tag/
|
|||||||
"io.narl.proton-drive-linux.svg::$_raw/packaging/io.narl.proton-drive-linux.svg"
|
"io.narl.proton-drive-linux.svg::$_raw/packaging/io.narl.proton-drive-linux.svg"
|
||||||
"proton-drive.service::$_raw/packaging/proton-drive.service"
|
"proton-drive.service::$_raw/packaging/proton-drive.service"
|
||||||
"LICENSE-$pkgver::$_raw/LICENSE")
|
"LICENSE-$pkgver::$_raw/LICENSE")
|
||||||
sha256sums=('4d746d83abd5174acb324236b6146c083a43b8c25e8d30e31c2a6d5b594ba479'
|
sha256sums=('69f0d6996feb95b18d81bb96e9373c61fddb64ab17b50c94cd83ef8d5e0a1425'
|
||||||
'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123'
|
'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123'
|
||||||
'1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0'
|
'1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0'
|
||||||
'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828'
|
'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = proton-drive-for-linux-git
|
pkgbase = proton-drive-for-linux-git
|
||||||
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)
|
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)
|
||||||
pkgver = 2.2.0.r1.g82e9654
|
pkgver = 2.2.1.r0.g5109427
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/narrrl/proton-drive-linux
|
url = https://github.com/narrrl/proton-drive-linux
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -23,8 +23,8 @@ pkgbase = proton-drive-for-linux-git
|
|||||||
optdepends = xdg-utils: "Open folder" action in the tray menu
|
optdepends = xdg-utils: "Open folder" action in the tray menu
|
||||||
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
||||||
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
||||||
provides = proton-drive-for-linux=2.2.0.r1.g82e9654
|
provides = proton-drive-for-linux=2.2.1.r0.g5109427
|
||||||
provides = proton-drive-linux=2.2.0.r1.g82e9654
|
provides = proton-drive-linux=2.2.1.r0.g5109427
|
||||||
provides = pdfs
|
provides = pdfs
|
||||||
conflicts = proton-drive-linux
|
conflicts = proton-drive-linux
|
||||||
conflicts = proton-drive-for-linux
|
conflicts = proton-drive-for-linux
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Nils Pukropp <contact@narl.io>
|
# Maintainer: Nils Pukropp <contact@narl.io>
|
||||||
|
|
||||||
pkgname=proton-drive-for-linux-git
|
pkgname=proton-drive-for-linux-git
|
||||||
pkgver=2.2.0.r1.g82e9654
|
pkgver=2.2.1.r0.g5109427
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)"
|
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = proton-drive-for-linux
|
pkgbase = proton-drive-for-linux
|
||||||
pkgdesc = Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray
|
pkgdesc = Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray
|
||||||
pkgver = 2.2.0
|
pkgver = 2.2.1
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/narrrl/proton-drive-linux
|
url = https://github.com/narrrl/proton-drive-linux
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -22,13 +22,13 @@ pkgbase = proton-drive-for-linux
|
|||||||
optdepends = xdg-utils: "Open folder" action in the tray menu
|
optdepends = xdg-utils: "Open folder" action in the tray menu
|
||||||
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
||||||
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
||||||
provides = proton-drive-linux=2.2.0
|
provides = proton-drive-linux=2.2.1
|
||||||
provides = pdfs
|
provides = pdfs
|
||||||
conflicts = proton-drive-linux
|
conflicts = proton-drive-linux
|
||||||
conflicts = proton-drive-for-linux-bin
|
conflicts = proton-drive-for-linux-bin
|
||||||
conflicts = proton-drive-for-linux-git
|
conflicts = proton-drive-for-linux-git
|
||||||
options = !lto
|
options = !lto
|
||||||
source = proton-drive-for-linux-2.2.0.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v2.2.0.tar.gz
|
source = proton-drive-for-linux-2.2.1.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v2.2.1.tar.gz
|
||||||
sha256sums = 73f68211d008894c3254b5adca0acca7f9e1af90e89209c22ae0213aa75add4b
|
sha256sums = 4635716908232896bfb0bf16d0f10300dc2eb59c27c7693607a21eabcac2edff
|
||||||
|
|
||||||
pkgname = proton-drive-for-linux
|
pkgname = proton-drive-for-linux
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Nils Pukropp <contact@narl.io>
|
# Maintainer: Nils Pukropp <contact@narl.io>
|
||||||
|
|
||||||
pkgname=proton-drive-for-linux
|
pkgname=proton-drive-for-linux
|
||||||
pkgver=2.2.0
|
pkgver=2.2.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray"
|
pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -22,7 +22,7 @@ conflicts=('proton-drive-linux' 'proton-drive-for-linux-bin' 'proton-drive-for-l
|
|||||||
# LTO has broken GTK/Rust links for this workspace; the release profile keeps it off.
|
# LTO has broken GTK/Rust links for this workspace; the release profile keeps it off.
|
||||||
options=('!lto')
|
options=('!lto')
|
||||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
|
||||||
sha256sums=('73f68211d008894c3254b5adca0acca7f9e1af90e89209c22ae0213aa75add4b')
|
sha256sums=('4635716908232896bfb0bf16d0f10300dc2eb59c27c7693607a21eabcac2edff')
|
||||||
|
|
||||||
_srcname="proton-drive-linux-$pkgver"
|
_srcname="proton-drive-linux-$pkgver"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user