mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
fixed some bugs
This commit is contained in:
1 parent
7766628098
commit
a90cf83ff5
39 files changed
+1573
-1285
No files matched your search
@@ -927,6 +927,10 @@ pub enum ErrorKind {
|
||||
/// The API was reached and refused, or the transfer broke. The one class
|
||||
/// where an unchanged retry can legitimately succeed.
|
||||
Remote,
|
||||
/// The account is out of storage. Distinct from [`Denied`](Self::Denied)
|
||||
/// because the user *can* fix it, and distinct from [`Remote`](Self::Remote)
|
||||
/// because retrying an upload that did not fit will not make it fit.
|
||||
Quota,
|
||||
/// Something on this machine failed: the database, the content cache, the
|
||||
/// filesystem. Not the user's doing and not theirs to fix.
|
||||
#[default]
|
||||
|
||||
@@ -63,5 +63,4 @@ impl Db {
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -122,5 +122,4 @@ impl Db {
|
||||
// keyed by uid display string. `recursive` marks a folder pin: its whole
|
||||
// subtree counts as pinned, resolved against `nodes` via a CTE so a new
|
||||
// descendant is covered the moment it lands in the node cache.
|
||||
|
||||
}
|
||||
@@ -44,5 +44,4 @@ impl Db {
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -160,4 +160,3 @@ pub(super) fn local_hit(row: &rusqlite::Row) -> rusqlite::Result<LocalFileHit> {
|
||||
mtime: row.get(4)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//! is already on disk somewhere, so a change here would silently diverge from
|
||||
//! what an existing install actually has. Add a new one instead.
|
||||
|
||||
|
||||
use super::Db;
|
||||
use crate::Result;
|
||||
|
||||
@@ -84,7 +83,6 @@ impl Db {
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Schema v1: nodes + FTS5 trigram index + cache LRU. `sync_state` is created
|
||||
@@ -329,4 +327,3 @@ const MIGRATION_V13: &str = "
|
||||
ALTER TABLE photos ADD COLUMN media_type TEXT;
|
||||
ALTER TABLE photos ADD COLUMN kind INTEGER NOT NULL DEFAULT 0;
|
||||
";
|
||||
|
||||
@@ -40,9 +40,8 @@ pub use devices::StoredDevice;
|
||||
pub use local::LocalFileHit;
|
||||
pub use nodes::{SearchHit, StoredNode};
|
||||
pub use ops::{
|
||||
LOCAL_VOLUME,
|
||||
AttachedBlob, OP_CREATE, OP_MKDIR, OP_RENAME, OP_REVISION, OP_TRASH, PendingCounts,
|
||||
PendingOp, op_supersedes,
|
||||
AttachedBlob, LOCAL_VOLUME, OP_CREATE, OP_MKDIR, OP_RENAME, OP_REVISION, OP_TRASH,
|
||||
PendingCounts, PendingOp, op_supersedes,
|
||||
};
|
||||
pub use photos::{StoredPhoto, THUMB_HAVE, THUMB_NONE, THUMB_UNKNOWN};
|
||||
pub use sync::{StoredSyncEntry, StoredSyncFolder};
|
||||
|
||||
@@ -223,10 +223,8 @@ impl Db {
|
||||
// separate. `last_accessed` (unix seconds) is the LRU key. The daemon owns
|
||||
// the on-disk cache and rebuilds this index from disk on open, then keeps it
|
||||
// in sync on every store/read/evict, so it is authoritative for eviction.
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Effective plaintext size of a node for the indexed `size` column: the
|
||||
/// claimed size when known, else the on-storage size; folders are 0.
|
||||
pub(super) fn node_size(node: &Node) -> i64 {
|
||||
|
||||
@@ -317,9 +317,9 @@ impl Db {
|
||||
Ok(PendingCounts { uploads, changes })
|
||||
}
|
||||
|
||||
/// Nothing bearing this volume may be handed to the API — it would 404. The
|
||||
/// drain replaces it with the uid the server assigns.
|
||||
pub const LOCAL_VOLUME: &str = "local";
|
||||
/// Nothing bearing this volume may be handed to the API — it would 404. The
|
||||
/// drain replaces it with the uid the server assigns.
|
||||
pub const LOCAL_VOLUME: &str = "local";
|
||||
|
||||
/// Drop a queued op, once its upload has actually landed.
|
||||
pub fn delete_op(&self, id: i64) -> Result<()> {
|
||||
@@ -340,5 +340,4 @@ pub const LOCAL_VOLUME: &str = "local";
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,8 +47,7 @@ impl Db {
|
||||
// any previously learned value so the Photos/Videos/Raw split survives a
|
||||
// refresh instead of collapsing back to name-extension guesses.
|
||||
let learned: HashMap<String, (Option<f64>, i64, Option<String>)> = {
|
||||
let mut stmt =
|
||||
tx.prepare("SELECT uid, ratio, thumb_state, media_type FROM photos")?;
|
||||
let mut stmt = tx.prepare("SELECT uid, ratio, thumb_state, media_type FROM photos")?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok((r.get::<_, String>(0)?, (r.get(1)?, r.get(2)?, r.get(3)?)))
|
||||
})?;
|
||||
@@ -63,18 +62,17 @@ impl Db {
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
)?;
|
||||
for (seq, (uid, capture_time, name, media_type)) in items.iter().enumerate() {
|
||||
let (ratio, thumb_state, learned_media) = learned
|
||||
.get(uid)
|
||||
.cloned()
|
||||
.unwrap_or((None, THUMB_UNKNOWN, None));
|
||||
let (ratio, thumb_state, learned_media) =
|
||||
learned
|
||||
.get(uid)
|
||||
.cloned()
|
||||
.unwrap_or((None, THUMB_UNKNOWN, None));
|
||||
let media_type = media_type.clone().or(learned_media);
|
||||
// The tab this photo lands in is derived here, once, so a page or
|
||||
// count query is a plain indexed `WHERE kind = ?` rather than a
|
||||
// reclassification of every row.
|
||||
let kind = crate::control::PhotoKind::classify(
|
||||
name.as_deref(),
|
||||
media_type.as_deref(),
|
||||
);
|
||||
let kind =
|
||||
crate::control::PhotoKind::classify(name.as_deref(), media_type.as_deref());
|
||||
stmt.execute(params![
|
||||
uid,
|
||||
capture_time,
|
||||
@@ -127,7 +125,9 @@ impl Db {
|
||||
let limit_pos = binds.len();
|
||||
binds.push(offset as i64);
|
||||
let offset_pos = binds.len();
|
||||
sql.push_str(&format!(" ORDER BY seq LIMIT ?{limit_pos} OFFSET ?{offset_pos}"));
|
||||
sql.push_str(&format!(
|
||||
" ORDER BY seq LIMIT ?{limit_pos} OFFSET ?{offset_pos}"
|
||||
));
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt
|
||||
@@ -168,7 +168,11 @@ impl Db {
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt
|
||||
.query_map(rusqlite::params_from_iter(binds), |r| {
|
||||
Ok((r.get::<_, i32>(0)?, r.get::<_, i32>(1)?, r.get::<_, i64>(2)? as usize))
|
||||
Ok((
|
||||
r.get::<_, i32>(0)?,
|
||||
r.get::<_, i32>(1)?,
|
||||
r.get::<_, i64>(2)? as usize,
|
||||
))
|
||||
})?
|
||||
.collect::<rusqlite::Result<_>>()?;
|
||||
Ok(rows)
|
||||
@@ -240,5 +244,4 @@ impl Db {
|
||||
let n: i64 = conn.query_row("SELECT COUNT(*) FROM photos", [], |r| r.get(0))?;
|
||||
Ok(n.max(0) as usize)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,8 +6,6 @@ use rusqlite::{OptionalExtension, params};
|
||||
use super::Db;
|
||||
use crate::Result;
|
||||
|
||||
|
||||
|
||||
impl Db {
|
||||
pub fn pin_add(&self, uid: &str, path: &str, recursive: bool) -> Result<()> {
|
||||
let conn = self.conn.lock();
|
||||
@@ -120,5 +118,4 @@ impl Db {
|
||||
}
|
||||
|
||||
// ---- local (non-Drive) file index (schema v6) ----
|
||||
|
||||
}
|
||||
@@ -94,5 +94,4 @@ impl Db {
|
||||
conn.execute("DELETE FROM sync_state WHERE key = ?1", params![key])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -222,5 +222,4 @@ impl Db {
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
+998
-996
File diff suppressed because it is too large.
Load diff
@@ -61,5 +61,4 @@ impl Db {
|
||||
}
|
||||
|
||||
// ---- device sync (devices.md) -----------------------------------------
|
||||
|
||||
}
|
||||
@@ -93,6 +93,27 @@ impl CoreError {
|
||||
Self::new(ErrorKind::Internal, what)
|
||||
}
|
||||
|
||||
/// Classify a failure from an SDK call, prefixing `what` for the log.
|
||||
///
|
||||
/// Takes `&dyn Error` rather than `&ProtonError` so it reads the same
|
||||
/// through the boxes the drain deals in, where the concrete type survives
|
||||
/// but the static one does not.
|
||||
///
|
||||
/// A failure that is not an API response at all — a transport error, a
|
||||
/// timeout, a TLS failure — is what being offline looks like from here, so
|
||||
/// it classifies as [`ErrorKind::Offline`] rather than
|
||||
/// [`ErrorKind::Remote`]: the request never reached Proton, and telling the
|
||||
/// user their connection is down beats telling them the server refused.
|
||||
pub fn from_api(e: &(dyn std::error::Error + 'static), what: &str) -> Self {
|
||||
let kind = match e.downcast_ref::<proton_sdk::ProtonError>() {
|
||||
Some(proton_sdk::ProtonError::Api(api)) => api_kind(api.code),
|
||||
// A `ProtonError` that is not an `Api` variant never reached the
|
||||
// API; so does anything that is not a `ProtonError` at all.
|
||||
_ => ErrorKind::Offline,
|
||||
};
|
||||
Self::new(kind, format!("{what}: {e}"))
|
||||
}
|
||||
|
||||
/// Prefix the message while keeping the classification.
|
||||
///
|
||||
/// For adding the caller's context to an already-classified failure — the
|
||||
@@ -134,12 +155,136 @@ impl From<Error> for CoreError {
|
||||
fn from(e: Error) -> Self {
|
||||
let kind = match &e {
|
||||
Error::NotLoggedIn | Error::ReloginRequired => ErrorKind::Denied,
|
||||
Error::Proton(_) => ErrorKind::Remote,
|
||||
Error::Proton(proton_sdk::ProtonError::Api(api)) => api_kind(api.code),
|
||||
// A `ProtonError` that never reached the API is a transport failure.
|
||||
Error::Proton(_) => ErrorKind::Offline,
|
||||
_ => ErrorKind::Internal,
|
||||
};
|
||||
Self::new(kind, e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// What an API response code means to a front-end.
|
||||
///
|
||||
/// The grouping is by *what the user can do about it*, which is what
|
||||
/// [`ErrorKind`] exists to express — not by HTTP-ish similarity. So a 403 and a
|
||||
/// `NotEnoughPermissions` land together despite being different numbers, while
|
||||
/// `AlreadyExists` and `DoesNotExist` stay apart despite both being 25xx.
|
||||
fn api_kind(code: proton_sdk::api::ResponseCode) -> ErrorKind {
|
||||
use proton_sdk::api::ResponseCode as C;
|
||||
match code {
|
||||
// The account may not do this, and repeating it will not help.
|
||||
C::Unauthorized
|
||||
| C::Forbidden
|
||||
| C::NotEnoughPermissions
|
||||
| C::NotEnoughPermissionsToGrantPermissions
|
||||
| C::InsufficientScope
|
||||
| C::IncorrectLoginCredentials
|
||||
| C::InvalidRefreshToken
|
||||
| C::AccountDeleted
|
||||
| C::AccountDisabled
|
||||
| C::NoActiveSubscription => ErrorKind::Denied,
|
||||
|
||||
C::DoesNotExist => ErrorKind::NotFound,
|
||||
|
||||
// The remote moved underneath the request; the caller has to decide.
|
||||
C::AlreadyExists | C::IncompatibleState => ErrorKind::Conflict,
|
||||
|
||||
// The request was malformed or asks for something structurally
|
||||
// impossible. A bug in the caller, not a condition to retry.
|
||||
C::InvalidRequirements
|
||||
| C::InvalidValue
|
||||
| C::InvalidEncryptedIdFormat
|
||||
| C::InvalidApp
|
||||
| C::TooManyChildren
|
||||
| C::NestingTooDeep => ErrorKind::Invalid,
|
||||
|
||||
C::InsufficientQuota | C::InsufficientSpace | C::InsufficientVolumeQuota => {
|
||||
ErrorKind::Quota
|
||||
}
|
||||
|
||||
// Reached Proton, but it could not answer *now*. Retrying is exactly
|
||||
// the right response, so these are Offline rather than Remote — the
|
||||
// front-end already words that as "not right now".
|
||||
C::RequestTimeout
|
||||
| C::Timeout
|
||||
| C::TooManyRequests
|
||||
| C::ServiceUnavailable
|
||||
| C::Offline => ErrorKind::Offline,
|
||||
|
||||
// Reached Proton and it refused for a reason we have no specific
|
||||
// handling for. Retryable, because an unchanged retry can legitimately
|
||||
// succeed against a transient server-side condition.
|
||||
_ => ErrorKind::Remote,
|
||||
}
|
||||
}
|
||||
|
||||
/// The result every request-serving method in the daemon returns.
|
||||
pub type CoreResult<T> = std::result::Result<T, CoreError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proton_sdk::api::ResponseCode as C;
|
||||
use proton_sdk::{ProtonApiError, ProtonError};
|
||||
|
||||
fn api(code: C) -> ProtonError {
|
||||
ProtonError::Api(ProtonApiError {
|
||||
code,
|
||||
http_status: 422,
|
||||
message: "nope".into(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_codes_classify_by_what_the_user_can_do() {
|
||||
assert_eq!(api_kind(C::Forbidden), ErrorKind::Denied);
|
||||
assert_eq!(api_kind(C::NotEnoughPermissions), ErrorKind::Denied);
|
||||
assert_eq!(api_kind(C::DoesNotExist), ErrorKind::NotFound);
|
||||
assert_eq!(api_kind(C::AlreadyExists), ErrorKind::Conflict);
|
||||
assert_eq!(api_kind(C::InvalidValue), ErrorKind::Invalid);
|
||||
assert_eq!(api_kind(C::InsufficientSpace), ErrorKind::Quota);
|
||||
assert_eq!(api_kind(C::TooManyRequests), ErrorKind::Offline);
|
||||
// Nothing specific known about it, but Proton did answer.
|
||||
assert_eq!(api_kind(C::ProtonDriveUnknown), ErrorKind::Remote);
|
||||
}
|
||||
|
||||
/// The regression this whole pass exists to prevent: before it, every SDK
|
||||
/// failure was hardcoded to `Remote`, so a 403 asked the user to retry a
|
||||
/// request that could never succeed.
|
||||
#[test]
|
||||
fn a_forbidden_response_is_not_offered_as_retryable() {
|
||||
let e = CoreError::from_api(&api(C::Forbidden), "rename");
|
||||
assert_eq!(e.kind, ErrorKind::Denied);
|
||||
assert!(!e.kind.retryable());
|
||||
assert!(e.message.starts_with("rename: "));
|
||||
}
|
||||
|
||||
/// Being full is not a transient condition, so retrying an upload that did
|
||||
/// not fit must not be offered as a fix.
|
||||
#[test]
|
||||
fn running_out_of_space_is_not_retryable() {
|
||||
assert!(
|
||||
!CoreError::from_api(&api(C::InsufficientQuota), "upload")
|
||||
.kind
|
||||
.retryable()
|
||||
);
|
||||
}
|
||||
|
||||
/// A failure that never reached the API is the user's connection, not
|
||||
/// Proton refusing — the front-end words those differently.
|
||||
#[test]
|
||||
fn a_transport_failure_reads_as_offline() {
|
||||
let io = std::io::Error::other("connection reset");
|
||||
assert_eq!(CoreError::from_api(&io, "list").kind, ErrorKind::Offline);
|
||||
}
|
||||
|
||||
/// `Error` is the plumbing type; converting it must classify as sharply as
|
||||
/// the direct path, or the two disagree about the same failure.
|
||||
#[test]
|
||||
fn converting_from_the_plumbing_error_classifies_the_api_code() {
|
||||
let e: CoreError = Error::Proton(api(C::DoesNotExist)).into();
|
||||
assert_eq!(e.kind, ErrorKind::NotFound);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user