You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/10/26 20:12:47 UTC

(opendal-yinyang) 02/02: feat: project artifact evidence and export conflict-local offline

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/managed-sync-cli
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git

commit 0262d3b6d442a5cd8b7bda78352bcdc7ef0274a3
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Tue Oct 27 04:12:39 2026 +0800

    feat: project artifact evidence and export conflict-local offline
    
    Use the domain artifact `path`/`exportable` directly instead of
    deriving the quarantine layout, and route `export --conflict --side local`
    through the authority-free `replica::export_conflict_local`. Only a
    conflict's remote side still needs the authority. Update the Linux
    Managed Sync specification to match.
    
    Co-authored-by: Chojan Shang <ps...@apache.org>
---
 specs/linux-sync.md | 12 ++++++------
 src/sync_cli.rs     | 48 ++++++++++++++++++++++++------------------------
 2 files changed, 30 insertions(+), 30 deletions(-)

diff --git a/specs/linux-sync.md b/specs/linux-sync.md
index 4ebcc56..2ba0d14 100644
--- a/specs/linux-sync.md
+++ b/specs/linux-sync.md
@@ -77,10 +77,10 @@ decide whether to retry.
 `status` and `export --artifact` work without remote connectivity. `status`
 classifies the local binding first and reports remote availability truthfully,
 marking it unavailable with the real connection error when the authority cannot
-be reached; it never fabricates success. `export --artifact` reads retained
-bytes from the local replica and never contacts the authority. `export
---conflict` currently connects to the authority, because the remote side reads
-a retained revision.
+be reached; it never fabricates success. `export --artifact` and `export
+--conflict --side local` read durable local bytes and never contact the
+authority. `export --conflict --side remote` reads a retained revision and
+needs the authority.
 
 ## Status document
 
@@ -165,5 +165,5 @@ yy --volume sync.json sync status --json
 
 `sync` is implemented only on Linux and drives an ordinary directory; it does
 not provide macOS File Provider integration, an automatic watcher or a daemon.
-Retained-artifact export is offline; conflict-side export currently needs the
-authority.
+A retained artifact's bytes and a conflict's local side are exported offline; a
+conflict's remote side needs the authority.
diff --git a/src/sync_cli.rs b/src/sync_cli.rs
index bd13be1..0b77bad 100644
--- a/src/sync_cli.rs
+++ b/src/sync_cli.rs
@@ -531,14 +531,12 @@ async fn export(
         // Retained bytes are local; export must not require remote connectivity.
         export_artifact_local(config, id, &mut file).await
     } else {
-        export_conflict(
-            config,
-            operator,
-            conflict.as_deref().expect("conflict selector present"),
-            side.expect("conflict side present").as_str(),
-            &mut file,
-        )
-        .await
+        let id = conflict.as_deref().expect("conflict selector present");
+        match side.expect("conflict side present") {
+            // The local side is durable local bytes; no authority is needed.
+            SyncSide::Local => export_conflict_local(config, id, &mut file),
+            SyncSide::Remote => export_conflict_remote(config, operator, id, &mut file).await,
+        }
     };
     drop(file);
     if let Err(error) = outcome {
@@ -568,13 +566,22 @@ async fn export_artifact_local(
     Ok(())
 }
 
-/// Exports one side of a conflict. The remote side reads the retained revision,
-/// so this path needs the publication authority.
-async fn export_conflict(
+/// Exports a conflict's local side through the authority-free local reader.
+fn export_conflict_local(
+    config: &VolumeConfig,
+    id: &str,
+    output: &mut dyn std::io::Write,
+) -> Result<(), Box<dyn std::error::Error>> {
+    yinyang::replica::export_conflict_local(&config.staging, id, output).map_err(fatal)?;
+    Ok(())
+}
+
+/// Exports a conflict's remote side. It reads a retained revision, so this path
+/// needs the publication authority.
+async fn export_conflict_remote(
     config: &VolumeConfig,
     operator: opendal::Operator,
     id: &str,
-    side: &str,
     output: &mut dyn std::io::Write,
 ) -> Result<(), Box<dyn std::error::Error>> {
     let authority = connect(config, operator).await?;
@@ -582,7 +589,7 @@ async fn export_conflict(
         .await
         .map_err(fatal)?;
     directory
-        .export_conflict_side(id, side, output)
+        .export_conflict_side(id, "remote", output)
         .await
         .map_err(fatal)?;
     Ok(())
@@ -666,7 +673,7 @@ fn project(inspection: &domain::Inspection) -> Result<SyncStatus, String> {
     let artifacts = inspection
         .artifacts
         .iter()
-        .map(|artifact| project_artifact(artifact, &inspection.binding.state_directory))
+        .map(project_artifact)
         .collect::<Result<Vec<_>, _>>()?;
     Ok(SyncStatus {
         schema: SyncStatus::SCHEMA.to_owned(),
@@ -869,7 +876,7 @@ fn conflict_side(side: &str) -> ConflictSide {
     }
 }
 
-fn project_artifact(artifact: &domain::Artifact, state: &Path) -> Result<ArtifactRecord, String> {
+fn project_artifact(artifact: &domain::Artifact) -> Result<ArtifactRecord, String> {
     let kind = match artifact.kind.as_str() {
         "replaced-file" => ArtifactKind::ReplacedFile,
         "rollback-displaced" => ArtifactKind::RollbackDisplaced,
@@ -892,15 +899,8 @@ fn project_artifact(artifact: &domain::Artifact, state: &Path) -> Result<Artifac
         origin: artifact.origin.clone(),
         kind,
         phase,
-        // Retained bytes live under the state directory's quarantine. The
-        // domain record exposes the file name; the adapter composes the public
-        // path the operator and export use.
-        path: state
-            .join("quarantine")
-            .join(&artifact.name)
-            .display()
-            .to_string(),
-        exportable: kind != ArtifactKind::QuarantinedDirectory,
+        path: artifact.path.display().to_string(),
+        exportable: artifact.exportable,
         error: artifact.error.clone(),
     })
 }