Refuse a scan whose root has gone, instead of reporting it empty
FR-PLAT-AND-2, and a silent failure on both platforms. `dr_sync::scan` stepped over a NotFound or PermissionDenied the way it does for a child that vanished mid-walk -- correct for a child, wrong for the root, where it ended the walk, returned Ok with nothing in it, and reported a successful scan of a library that was no longer there. A lost root is now its own error. The images under it are marked Availability::Offline per FR-CAT-9 and no catalog row is deleted; `library::persist` clears the mark per file as each one is listed again, so a root that comes back needs no repair step. Partly satisfied rather than closed, and the gap is worth stating. The recovery half is real and reachable on Android today, because `map_status` turns Nextcloud's 403 and 404 into it and Nextcloud is how a phone actually gets a library in this build. The causes the requirement names -- revocation, reinstall, a removed card -- are properties of a persisted tree permission, and there is none: SAF does not exist here, `SourceRef::Document` is constructed only in test modules, and `LocalStorage` rejects the variant outright. When SAF lands it becomes a third producer of this error and nothing above it changes, which is why the discovery belongs in the connector. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,14 +61,18 @@ pub enum RemoteError {
|
||||
/// connector for.
|
||||
///
|
||||
/// **Not a network failure and not an auth failure**, which is why it is
|
||||
/// its own variant. A folder library whose directory has been unmounted,
|
||||
/// or an account naming a backend a cut-down build was not compiled with,
|
||||
/// produces a request that never leaves the process — reporting either as
|
||||
/// `Network` would put the app into offline mode and tell the user their
|
||||
/// connection is down, and reporting them as `AuthFailed` would send them
|
||||
/// to re-enter a credential that is fine. The message names what is wrong
|
||||
/// with the configuration, because that is the only thing that will fix
|
||||
/// it.
|
||||
/// its own variant. An account naming a backend a cut-down build was not
|
||||
/// compiled with, or a path that would leave the library folder, produces
|
||||
/// a request that never leaves the process — reporting either as `Network`
|
||||
/// would put the app into offline mode and tell the user their connection
|
||||
/// is down, and reporting them as `AuthFailed` would send them to re-enter
|
||||
/// a credential that is fine. The message names what is wrong with the
|
||||
/// configuration, because that is the only thing that will fix it.
|
||||
///
|
||||
/// A folder library whose directory is not there was once reported here
|
||||
/// too, and is now [`RootUnavailable`](Self::RootUnavailable): it is not
|
||||
/// something wrong with the configuration, it is the library being gone,
|
||||
/// and only the second of those has a catalog to protect.
|
||||
#[error("account misconfigured: {0}")]
|
||||
Configuration(String),
|
||||
|
||||
@@ -105,6 +109,43 @@ pub enum RemoteError {
|
||||
|
||||
#[error("operation cancelled")]
|
||||
Cancelled,
|
||||
|
||||
/// TRACES: FR-PLAT-AND-2 | FR-CAT-9
|
||||
/// The library root itself could not be opened.
|
||||
///
|
||||
/// **The one failure that is about the library rather than about a file in
|
||||
/// it**, and it is a separate variant because every other classification
|
||||
/// of it is wrong in a way that costs the user something:
|
||||
///
|
||||
/// - As [`NotFound`](Self::NotFound) it is indistinguishable from a folder
|
||||
/// deleted between listing its parent and reaching it, which the walk
|
||||
/// correctly steps over — so a whole library going away is reported as a
|
||||
/// successful scan that found nothing.
|
||||
/// - As [`PermissionDenied`](Self::PermissionDenied) it inherits a message
|
||||
/// about Nextcloud share permissions and sidecar writes, which is
|
||||
/// accurate for the case it was written for and nonsense for a tree
|
||||
/// grant the user revoked in system settings.
|
||||
/// - As [`Network`](Self::Network) it would claim the connection is down,
|
||||
/// which is a promise that waiting will fix it.
|
||||
///
|
||||
/// Today this is a Nextcloud root that answers 404 or 403 — deleted, or a
|
||||
/// share withdrawn — or a folder library whose directory is not there. It
|
||||
/// is also, exactly, the shape a revoked Android tree permission will have
|
||||
/// when the Storage Access Framework connector FR-PLAT-AND-1 asks for
|
||||
/// exists: the tree URI still stored, the permission behind it gone, every
|
||||
/// read failing at the root and nowhere else. **That connector is not
|
||||
/// built**, so no SAF grant can be lost yet; what this variant does is put
|
||||
/// the recovery FR-PLAT-AND-2 requires in the one place all three causes
|
||||
/// pass through, so the third needs no new handling above it.
|
||||
///
|
||||
/// The response is the same for all of them and is the point of the
|
||||
/// variant: mark what the catalog holds as offline, keep every row, and
|
||||
/// say which library and why (FR-CAT-9).
|
||||
///
|
||||
/// The string is the underlying failure, not a rewrite of it. What the
|
||||
/// user is told is composed where the library's name is known.
|
||||
#[error("the library folder could not be opened: {0}")]
|
||||
RootUnavailable(String),
|
||||
}
|
||||
|
||||
impl RemoteError {
|
||||
@@ -150,6 +191,19 @@ impl RemoteError {
|
||||
pub fn indicates_offline(&self) -> bool {
|
||||
matches!(self, RemoteError::Network(_))
|
||||
}
|
||||
|
||||
/// TRACES: FR-PLAT-AND-2
|
||||
/// Whether the *library* is gone, as opposed to the server or one file.
|
||||
///
|
||||
/// Kept beside [`Self::indicates_offline`] because the two answer the same
|
||||
/// shape of question and must not be confused. Both put the app into a
|
||||
/// degraded mode that keeps working from the catalog, but they differ in
|
||||
/// what the user is told and in what would end it: an offline library
|
||||
/// comes back when the network does, and an unavailable root comes back
|
||||
/// only when someone grants access again.
|
||||
pub fn indicates_lost_root(&self) -> bool {
|
||||
matches!(self, RemoteError::RootUnavailable(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user