implemented revisions

This commit is contained in:
nvrl committed 2026-07-25 12:48:41 +02:00
1 parent b683e73c23
commit ea59994250
6 files changed
+58 -4

No files matched your search

Generated
+2 -2
View File
@@ -2030,7 +2030,7 @@ dependencies = [
[[package]] [[package]]
name = "proton-drive-rs" name = "proton-drive-rs"
version = "0.2.1" version = "0.2.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
@@ -2055,7 +2055,7 @@ dependencies = [
[[package]] [[package]]
name = "proton-sdk" name = "proton-sdk"
version = "0.2.1" version = "0.2.2"
dependencies = [ dependencies = [
"aes-gcm 0.11.0", "aes-gcm 0.11.0",
"async-trait", "async-trait",
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/proton-sdk", "crates/proton-drive-rs"] members = ["crates/proton-sdk", "crates/proton-drive-rs"]
[workspace.package] [workspace.package]
version = "0.2.1" version = "0.2.2"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://github.com/narrrl/proton-sdk-rs" repository = "https://github.com/narrrl/proton-sdk-rs"
+1 -1
View File
@@ -14,7 +14,7 @@ readme = "../../README.md"
documentation = "https://docs.rs/proton-drive-rs" documentation = "https://docs.rs/proton-drive-rs"
[dependencies] [dependencies]
proton-sdk = { path = "../proton-sdk", version = "0.2.0" } proton-sdk = { path = "../proton-sdk", version = "0.2.2" }
tokio.workspace = true tokio.workspace = true
reqwest.workspace = true reqwest.workspace = true
serde.workspace = true serde.workspace = true
+4
View File
@@ -5770,6 +5770,7 @@ impl ProtonDriveClient {
// leaves the claimed metadata absent rather than failing the node. // leaves the claimed metadata absent rather than failing the node.
let mut claimed_size = None; let mut claimed_size = None;
let mut claimed_modification_time = None; let mut claimed_modification_time = None;
let mut content_sha1 = None;
if let Some(node_key) = node_key.as_ref() if let Some(node_key) = node_key.as_ref()
&& let Some(rev) = file.active_revision.as_ref() && let Some(rev) = file.active_revision.as_ref()
&& let Some(xattr) = rev.extended_attributes.as_deref() && let Some(xattr) = rev.extended_attributes.as_deref()
@@ -5787,6 +5788,7 @@ impl ProtonDriveClient {
if let Some(common) = attrs.common { if let Some(common) = attrs.common {
claimed_size = common.size; claimed_size = common.size;
claimed_modification_time = common.modification_time; claimed_modification_time = common.modification_time;
content_sha1 = common.digests.and_then(|d| d.sha1);
} }
} }
Err(e) => { Err(e) => {
@@ -5801,8 +5803,10 @@ impl ProtonDriveClient {
.active_revision .active_revision
.as_ref() .as_ref()
.map(|rev| RevisionState::from_raw(rev.state)), .map(|rev| RevisionState::from_raw(rev.state)),
active_revision_id: file.active_revision.as_ref().map(|rev| rev.id.clone()),
claimed_size, claimed_size,
claimed_modification_time, claimed_modification_time,
content_sha1,
} }
} }
LinkType::Unknown => { LinkType::Unknown => {
+46
View File
@@ -111,6 +111,14 @@ pub enum NodeKind {
/// the file has no active revision (an unsealed draft). /// the file has no active revision (an unsealed draft).
#[serde(default)] #[serde(default)]
active_revision_state: Option<RevisionState>, active_revision_state: Option<RevisionState>,
/// Server-assigned identifier of the file's active revision
/// (`ActiveRevision.RevisionID`); `None` for an unsealed draft, or when
/// read through a surface that does not carry it. Unlike
/// `modification_time` this is a stable revision identity: it advances iff
/// a *new* revision was sealed, so a consumer can tell "the same revision,
/// re-stamped" from "someone wrote a new revision" without a download.
#[serde(default)]
active_revision_id: Option<String>,
/// Authoritative plaintext size from the active revision's decrypted /// Authoritative plaintext size from the active revision's decrypted
/// extended attributes (C# `ClaimedSize`). `None` when the revision has /// extended attributes (C# `ClaimedSize`). `None` when the revision has
/// no `XAttr` or it failed to decrypt. /// no `XAttr` or it failed to decrypt.
@@ -118,6 +126,12 @@ pub enum NodeKind {
/// ISO-8601 modification time from the decrypted extended attributes /// ISO-8601 modification time from the decrypted extended attributes
/// (C# `ClaimedModificationTime`), verbatim as written by the uploader. /// (C# `ClaimedModificationTime`), verbatim as written by the uploader.
claimed_modification_time: Option<String>, claimed_modification_time: Option<String>,
/// Lowercase-hex SHA-1 of the full plaintext, from the active revision's
/// decrypted extended attributes (`Digests.SHA1`). A download-free content
/// fingerprint: two files with the same size *and* the same digest hold
/// the same bytes. `None` when the revision carries no digest (some
/// clients omit it) or the `XAttr` failed to decrypt.
content_sha1: Option<String>,
}, },
} }
@@ -222,6 +236,38 @@ impl FileThumbnail {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn file_node_carries_active_revision_id_and_defaults_it() {
// A File node round-trips its revision id.
let kind = NodeKind::File {
media_type: "text/plain".into(),
total_size_on_storage: 10,
active_revision_state: Some(RevisionState::Active),
active_revision_id: Some("rev-abc".into()),
claimed_size: Some(4),
claimed_modification_time: None,
content_sha1: Some("da39a3ee".into()),
};
let json = serde_json::to_string(&kind).unwrap();
let back: NodeKind = serde_json::from_str(&json).unwrap();
match back {
NodeKind::File {
active_revision_id, ..
} => assert_eq!(active_revision_id.as_deref(), Some("rev-abc")),
NodeKind::Folder => panic!("expected a file"),
}
// A sidecar written before the field existed deserializes to `None`.
let legacy = r#"{"File":{"media_type":"text/plain","total_size_on_storage":10,"claimed_size":null,"claimed_modification_time":null}}"#;
let back: NodeKind = serde_json::from_str(legacy).unwrap();
match back {
NodeKind::File {
active_revision_id, ..
} => assert!(active_revision_id.is_none()),
NodeKind::Folder => panic!("expected a file"),
}
}
#[test] #[test]
fn revision_state_maps_the_wire_value() { fn revision_state_maps_the_wire_value() {
assert_eq!(RevisionState::from_raw(Some(2)), RevisionState::Superseded); assert_eq!(RevisionState::from_raw(Some(2)), RevisionState::Superseded);
@@ -528,10 +528,14 @@ impl ProtonDrivePublicLinkClient {
active_revision_state: file active_revision_state: file
.and_then(|f| f.active_revision.as_ref()) .and_then(|f| f.active_revision.as_ref())
.map(|revision| RevisionState::from_raw(revision.state)), .map(|revision| RevisionState::from_raw(revision.state)),
active_revision_id: file
.and_then(|f| f.active_revision.as_ref())
.map(|revision| revision.id.clone()),
// Claimed metadata lives in the revision's extended attributes, // Claimed metadata lives in the revision's extended attributes,
// which this read surface does not fetch. // which this read surface does not fetch.
claimed_size: None, claimed_size: None,
claimed_modification_time: None, claimed_modification_time: None,
content_sha1: None,
} }
}; };