mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
implemented shared files, folder uploads, caching
This commit is contained in:
1 parent
cc744d56c7
commit
a38d0371ec
8 files changed
+4141
-129
No files matched your search
@@ -92,6 +92,18 @@ pub enum Request {
|
||||
name: String,
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
/// Bulk-upload local files and/or directory trees into the mountpoint-relative
|
||||
/// `parent` folder. `sources` are absolute paths on the daemon's own
|
||||
/// filesystem (the daemon is local): each file is uploaded, each directory is
|
||||
/// recreated remotely and walked recursively. The daemon acks immediately with
|
||||
/// [`Response::Ok`] and does the work in the background — a big tree far
|
||||
/// outlasts the socket read timeout — so progress is observed through
|
||||
/// [`Request::GetQueueStatus`] and completion through the transfer count
|
||||
/// falling to zero.
|
||||
UploadPaths {
|
||||
parent: String,
|
||||
sources: Vec<String>,
|
||||
},
|
||||
/// Delete all unpinned cached blobs and on-demand blocks, keeping pinned
|
||||
/// files intact. Replies with [`Response::Ok`] reporting the bytes freed.
|
||||
PurgeCache,
|
||||
@@ -117,6 +129,243 @@ pub enum Request {
|
||||
/// Permanently delete everything in the trash. Irreversible.
|
||||
/// Replies with [`Response::Ok`].
|
||||
EmptyTrash,
|
||||
|
||||
// ---- devices ----------------------------------------------------------
|
||||
/// List the account's registered devices. Replies with [`Response::Devices`].
|
||||
ListDevices,
|
||||
/// Rename a device by its uid. Replies with [`Response::Ok`].
|
||||
RenameDevice { uid: String, name: String },
|
||||
/// Delete (deregister) a device by its uid. Replies with [`Response::Ok`].
|
||||
DeleteDevice { uid: String },
|
||||
|
||||
// ---- sharing a node ---------------------------------------------------
|
||||
/// Invite `emails` (Proton and/or external addresses, auto-detected) to the
|
||||
/// node at mountpoint-relative `path` at `role` ("viewer"|"editor"|"admin"),
|
||||
/// with an optional email `message`. Replies with [`Response::Ok`].
|
||||
ShareNode {
|
||||
path: String,
|
||||
emails: Vec<String>,
|
||||
role: String,
|
||||
message: Option<String>,
|
||||
},
|
||||
/// List the members, pending invitations and public link of the node at
|
||||
/// mountpoint-relative `path`. Replies with [`Response::Share`].
|
||||
ListShare { path: String },
|
||||
/// Change the role of a share entry (member or pending invitation) on the node
|
||||
/// at `path`. `id` and `kind` identify the entry (from [`Response::Share`]).
|
||||
/// Replies with [`Response::Ok`].
|
||||
UpdateShareRole {
|
||||
path: String,
|
||||
id: String,
|
||||
kind: ShareEntryKind,
|
||||
role: String,
|
||||
},
|
||||
/// Remove a share entry (member, pending Proton invite, or external invite)
|
||||
/// from the node at `path`. Replies with [`Response::Ok`].
|
||||
RemoveShareEntry {
|
||||
path: String,
|
||||
id: String,
|
||||
kind: ShareEntryKind,
|
||||
},
|
||||
/// Create a public link on the node at `path`. `role` is "viewer" or "editor";
|
||||
/// `password` optionally adds a custom password; `expires` is an optional Unix
|
||||
/// expiry. Replies with [`Response::PublicLink`] (carrying the shareable URL).
|
||||
CreatePublicLink {
|
||||
path: String,
|
||||
role: String,
|
||||
password: Option<String>,
|
||||
expires: Option<i64>,
|
||||
},
|
||||
/// Remove the public link `id` from the node at `path`. Replies with
|
||||
/// [`Response::Ok`].
|
||||
RemovePublicLink { path: String, id: String },
|
||||
|
||||
// ---- shared by me -----------------------------------------------------
|
||||
/// List the nodes I have shared with others — collaborative shares that still
|
||||
/// have members, pending invitations or a public link. Replies with
|
||||
/// [`Response::SharedByMe`]. Each item carries the share's state so the
|
||||
/// front-end can render it without a follow-up per node.
|
||||
ListSharedByMe,
|
||||
|
||||
// ---- shared with me ---------------------------------------------------
|
||||
/// List nodes shared with me that I have accepted. Replies with
|
||||
/// [`Response::Entries`] (each entry carries its `uid`; `path` is empty since
|
||||
/// the item lives outside the mount tree).
|
||||
ListSharedWithMe,
|
||||
/// Leave a shared node by its `uid`, giving up my access. Replies with
|
||||
/// [`Response::Ok`].
|
||||
LeaveShared { uid: String },
|
||||
|
||||
// ---- incoming invitations ---------------------------------------------
|
||||
/// List invitations addressed to me, pending accept or reject. Replies with
|
||||
/// [`Response::Invitations`].
|
||||
ListInvitations,
|
||||
/// Accept the invitation `id`, gaining access to the shared node. Replies with
|
||||
/// [`Response::Ok`].
|
||||
AcceptInvitation { id: String },
|
||||
/// Reject the invitation `id`, declining access. Replies with [`Response::Ok`].
|
||||
RejectInvitation { id: String },
|
||||
|
||||
// ---- bookmarks --------------------------------------------------------
|
||||
/// List public links saved to my account. Replies with [`Response::Bookmarks`].
|
||||
ListBookmarks,
|
||||
/// Save a public link `url` (optionally password-protected) as a bookmark.
|
||||
/// Replies with [`Response::Ok`].
|
||||
CreateBookmark {
|
||||
url: String,
|
||||
password: Option<String>,
|
||||
},
|
||||
/// Remove a saved bookmark by its `token`. Replies with [`Response::Ok`].
|
||||
DeleteBookmark { token: String },
|
||||
|
||||
// ---- activity ---------------------------------------------------------
|
||||
/// Fetch the daemon's recent activity log, newest first, capped at `limit`
|
||||
/// entries. Replies with [`Response::Activity`]. The log lives in memory for
|
||||
/// the life of the daemon: it records the mutations and transfers the daemon
|
||||
/// performs (uploads, deletes, renames, shares, …), so a front-end can show a
|
||||
/// running "what happened" feed without re-deriving it from anywhere.
|
||||
ListActivity { limit: usize },
|
||||
}
|
||||
|
||||
/// Which kind of share entry a [`ShareEntry`] is, and which collection an
|
||||
/// [`Request::UpdateShareRole`]/[`Request::RemoveShareEntry`] targets.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShareEntryKind {
|
||||
/// An accepted member (identified by its membership id).
|
||||
Member,
|
||||
/// A pending invitation to a Proton user (identified by its invitation id).
|
||||
ProtonInvite,
|
||||
/// A pending invitation to a non-Proton email (identified by its invitation id).
|
||||
ExternalInvite,
|
||||
}
|
||||
|
||||
/// A registered device in a [`Response::Devices`] listing.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeviceInfo {
|
||||
/// Device uid — the handle for [`Request::RenameDevice`]/[`Request::DeleteDevice`].
|
||||
pub uid: String,
|
||||
/// Decrypted device name, or a placeholder when it could not be decrypted.
|
||||
pub name: String,
|
||||
/// Platform: "Windows", "MacOs" or "Linux".
|
||||
pub device_type: String,
|
||||
/// Last sync time, epoch seconds; `None` if it never synced.
|
||||
pub last_sync: Option<i64>,
|
||||
}
|
||||
|
||||
/// One member or pending invitation on a node's share (in [`Response::Share`]).
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ShareEntry {
|
||||
/// Membership id (members) or invitation id (invitations) — the handle for
|
||||
/// [`Request::UpdateShareRole`]/[`Request::RemoveShareEntry`].
|
||||
pub id: String,
|
||||
/// The member's / invitee's email address.
|
||||
pub email: String,
|
||||
/// Role: "viewer", "editor", "admin" or "inherited".
|
||||
pub role: String,
|
||||
/// Whether this is a member, a pending Proton invite, or an external invite.
|
||||
pub kind: ShareEntryKind,
|
||||
}
|
||||
|
||||
/// A node's public link (in [`Response::Share`] / [`Response::PublicLink`]).
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PublicLinkInfo {
|
||||
/// Public-link id — the handle for [`Request::RemovePublicLink`].
|
||||
pub id: String,
|
||||
/// The shareable URL including the `#password` fragment, when known (always on
|
||||
/// creation; absent when only listed).
|
||||
pub url: Option<String>,
|
||||
/// Role granted to anyone with the link ("viewer" or "editor").
|
||||
pub role: String,
|
||||
/// Expiry, epoch seconds, if set.
|
||||
pub expires: Option<i64>,
|
||||
/// Whether a custom password additionally protects the link.
|
||||
pub has_password: bool,
|
||||
}
|
||||
|
||||
/// One invitation addressed to me (in [`Response::Invitations`]).
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct InvitationInfo {
|
||||
/// Invitation id — the handle for [`Request::AcceptInvitation`]/[`Request::RejectInvitation`].
|
||||
pub id: String,
|
||||
/// The email of the user who shared the item.
|
||||
pub inviter_email: String,
|
||||
/// The shared item's decrypted name, when available.
|
||||
pub name: Option<String>,
|
||||
/// Whether the shared item is a folder.
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
/// One saved public link (in [`Response::Bookmarks`]).
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BookmarkInfo {
|
||||
/// Bookmark token — the handle for [`Request::DeleteBookmark`].
|
||||
pub token: String,
|
||||
/// The full public URL including the `#password` fragment.
|
||||
pub url: String,
|
||||
/// The bookmarked item's decrypted name, when available.
|
||||
pub name: Option<String>,
|
||||
/// Whether the bookmarked item is a folder.
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
/// One node I have shared with others (in [`Response::SharedByMe`]). Summarizes
|
||||
/// the share's state so the "Shared" view renders in one pass: how many people
|
||||
/// have access, how many invitations are still pending, and the public link if
|
||||
/// the node has one.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SharedItem {
|
||||
/// Node uid in `volume~link` form — the handle for opening the node's share
|
||||
/// (via its mount path) or its details.
|
||||
pub uid: String,
|
||||
/// The shared node's decrypted name.
|
||||
pub name: String,
|
||||
pub is_dir: bool,
|
||||
/// Mountpoint-relative path, when the daemon can resolve it (the node lives in
|
||||
/// my own tree). Empty when the path is unknown.
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
/// Accepted members (people who already have access), excluding me.
|
||||
pub member_count: usize,
|
||||
/// Pending invitations (Proton + external) not yet accepted.
|
||||
pub invite_count: usize,
|
||||
/// The node's public link, if it has one.
|
||||
pub link: Option<PublicLinkInfo>,
|
||||
}
|
||||
|
||||
/// What happened, in an [`ActivityEntry`]. Kept coarse: a front-end maps each to
|
||||
/// an icon and a verb, and the human detail lives in the entry's fields.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ActivityKind {
|
||||
Upload,
|
||||
Rename,
|
||||
Move,
|
||||
CreateFolder,
|
||||
Trash,
|
||||
Restore,
|
||||
DeleteForever,
|
||||
EmptyTrash,
|
||||
Share,
|
||||
PublicLink,
|
||||
Unshare,
|
||||
}
|
||||
|
||||
/// One line in the daemon's activity log (in [`Response::Activity`]). Newest
|
||||
/// first. Records a mutation or transfer the daemon performed, with enough
|
||||
/// context to read as a sentence: "Uploaded report.pdf to /docs".
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ActivityEntry {
|
||||
/// When it happened, epoch seconds.
|
||||
pub time: i64,
|
||||
pub kind: ActivityKind,
|
||||
/// The primary item's name (a file/folder name, or a count like "3 items").
|
||||
pub target: String,
|
||||
/// Extra context: a destination path, a new name, an invitee, an error
|
||||
/// message. Empty when there is nothing to add.
|
||||
#[serde(default)]
|
||||
pub detail: String,
|
||||
/// Whether the operation succeeded. A failed entry still appears, so the log
|
||||
/// shows what was attempted.
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// Which way an active transfer is moving, in a [`TransferItem`].
|
||||
@@ -285,6 +534,26 @@ pub enum Response {
|
||||
LocalResults { hits: Vec<LocalHit>, indexing: bool },
|
||||
/// A snapshot of in-flight transfers (reply to [`Request::GetQueueStatus`]).
|
||||
Transfers { items: Vec<TransferItem> },
|
||||
/// The account's devices (reply to [`Request::ListDevices`]).
|
||||
Devices { items: Vec<DeviceInfo> },
|
||||
/// A node's share: members + pending invitations, and its public link if any
|
||||
/// (reply to [`Request::ListShare`]).
|
||||
Share {
|
||||
entries: Vec<ShareEntry>,
|
||||
link: Option<PublicLinkInfo>,
|
||||
},
|
||||
/// A freshly created public link, carrying the shareable URL (reply to
|
||||
/// [`Request::CreatePublicLink`]).
|
||||
PublicLink { link: PublicLinkInfo },
|
||||
/// Invitations addressed to me (reply to [`Request::ListInvitations`]).
|
||||
Invitations { items: Vec<InvitationInfo> },
|
||||
/// Saved public links (reply to [`Request::ListBookmarks`]).
|
||||
Bookmarks { items: Vec<BookmarkInfo> },
|
||||
/// Nodes I have shared with others (reply to [`Request::ListSharedByMe`]).
|
||||
SharedByMe { items: Vec<SharedItem> },
|
||||
/// The daemon's recent activity, newest first (reply to
|
||||
/// [`Request::ListActivity`]).
|
||||
Activity { items: Vec<ActivityEntry> },
|
||||
/// The request failed.
|
||||
Error { message: String },
|
||||
}
|
||||
@@ -336,6 +605,10 @@ mod tests {
|
||||
name: "f.bin".into(),
|
||||
bytes: vec![0, 1, 2, 255],
|
||||
},
|
||||
Request::UploadPaths {
|
||||
parent: "a".into(),
|
||||
sources: vec!["/home/u/x.txt".into(), "/home/u/pics".into()],
|
||||
},
|
||||
];
|
||||
for req in reqs {
|
||||
let line = serde_json::to_string(&req).unwrap();
|
||||
@@ -368,4 +641,71 @@ mod tests {
|
||||
assert_eq!(line, serde_json::to_string(&back).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// The sharing and devices requests must survive the same line-delimited JSON
|
||||
/// round-trip: a mangled id or role would touch the wrong share or member.
|
||||
#[test]
|
||||
fn sharing_requests_roundtrip() {
|
||||
let reqs = [
|
||||
Request::ListDevices,
|
||||
Request::RenameDevice {
|
||||
uid: "dev-1".into(),
|
||||
name: "laptop".into(),
|
||||
},
|
||||
Request::DeleteDevice {
|
||||
uid: "dev-1".into(),
|
||||
},
|
||||
Request::ShareNode {
|
||||
path: "a/b".into(),
|
||||
emails: vec!["x@proton.me".into(), "y@example.com".into()],
|
||||
role: "editor".into(),
|
||||
message: Some("hi".into()),
|
||||
},
|
||||
Request::ListShare { path: "a/b".into() },
|
||||
Request::UpdateShareRole {
|
||||
path: "a/b".into(),
|
||||
id: "mid-1".into(),
|
||||
kind: ShareEntryKind::Member,
|
||||
role: "admin".into(),
|
||||
},
|
||||
Request::RemoveShareEntry {
|
||||
path: "a/b".into(),
|
||||
id: "iid-1".into(),
|
||||
kind: ShareEntryKind::ExternalInvite,
|
||||
},
|
||||
Request::CreatePublicLink {
|
||||
path: "a/b".into(),
|
||||
role: "viewer".into(),
|
||||
password: Some("pw".into()),
|
||||
expires: Some(1_700_000_000),
|
||||
},
|
||||
Request::RemovePublicLink {
|
||||
path: "a/b".into(),
|
||||
id: "url-1".into(),
|
||||
},
|
||||
Request::ListSharedByMe,
|
||||
Request::ListActivity { limit: 100 },
|
||||
Request::ListSharedWithMe,
|
||||
Request::LeaveShared {
|
||||
uid: "vol~link".into(),
|
||||
},
|
||||
Request::ListInvitations,
|
||||
Request::AcceptInvitation { id: "inv-1".into() },
|
||||
Request::RejectInvitation { id: "inv-1".into() },
|
||||
Request::ListBookmarks,
|
||||
Request::CreateBookmark {
|
||||
url: "https://drive.proton.me/urls/tok#pw".into(),
|
||||
password: None,
|
||||
},
|
||||
Request::DeleteBookmark {
|
||||
token: "tok".into(),
|
||||
},
|
||||
];
|
||||
for req in reqs {
|
||||
let line = serde_json::to_string(&req).unwrap();
|
||||
assert!(!line.contains('\n'), "wire form must be a single line");
|
||||
let back: Request = serde_json::from_str(&line).unwrap();
|
||||
assert_eq!(line, serde_json::to_string(&back).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,9 +381,8 @@ impl Db {
|
||||
let tx = conn.transaction()?;
|
||||
let learned: HashMap<String, (Option<f64>, i64)> = {
|
||||
let mut stmt = tx.prepare("SELECT uid, ratio, thumb_state FROM photos")?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok((r.get::<_, String>(0)?, (r.get(1)?, r.get(2)?)))
|
||||
})?;
|
||||
let rows =
|
||||
stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, (r.get(1)?, r.get(2)?))))?;
|
||||
rows.collect::<rusqlite::Result<_>>()?
|
||||
};
|
||||
|
||||
@@ -394,8 +393,16 @@ impl Db {
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
)?;
|
||||
for (seq, (uid, capture_time, name)) in items.iter().enumerate() {
|
||||
let (ratio, thumb_state) = learned.get(uid).copied().unwrap_or((None, THUMB_UNKNOWN));
|
||||
stmt.execute(params![uid, capture_time, name, ratio, thumb_state, seq as i64])?;
|
||||
let (ratio, thumb_state) =
|
||||
learned.get(uid).copied().unwrap_or((None, THUMB_UNKNOWN));
|
||||
stmt.execute(params![
|
||||
uid,
|
||||
capture_time,
|
||||
name,
|
||||
ratio,
|
||||
thumb_state,
|
||||
seq as i64
|
||||
])?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
|
||||
Reference in new issue
Block a user