hexz_cli/cmd/data/
push.rs1use anyhow::{Context, Result};
4use colored::Colorize;
5use hexz_core::format::header::Header;
6use hexz_core::format::magic::HEADER_SIZE;
7use hexz_store::StorageBackend;
8use hexz_store::local::MmapBackend;
9use hexz_store::remote::{self, RemoteTransport};
10use std::collections::HashSet;
11use std::path::{Path, PathBuf};
12
13use super::workspace::Workspace;
14
15pub fn run(remote_name: &str, archive: Option<PathBuf>) -> Result<()> {
17 let ws = Workspace::find(&std::env::current_dir()?)?
18 .context("Not in a hexz workspace (no .hexz found)")?;
19
20 let url = ws.config.remotes.get(remote_name).with_context(|| {
21 format!(
22 "Remote '{remote_name}' not found. Add it with `hexz remote add {remote_name} <url>`"
23 )
24 })?;
25
26 let target = if let Some(a) = archive {
27 a
28 } else if let Some(b) = &ws.config.base_archive {
29 b.clone()
30 } else {
31 anyhow::bail!("No archive specified and workspace has no base archive to push.");
32 };
33
34 if !target.exists() {
35 anyhow::bail!("Archive not found: {}", target.display());
36 }
37
38 println!(
39 "{} Pushing to {} {}",
40 "╭".dimmed(),
41 remote_name.magenta(),
42 url.bright_black()
43 );
44
45 let transport =
46 remote::connect(url).map_err(|e| anyhow::anyhow!("Failed to connect to remote: {e}"))?;
47
48 let mut pushed = HashSet::new();
49 push_archive(&target, transport.as_ref(), &mut pushed)?;
50
51 println!("\n {} Push complete.", "✓".green());
52 Ok(())
53}
54
55fn push_archive(
57 path: &Path,
58 transport: &dyn RemoteTransport,
59 pushed: &mut HashSet<PathBuf>,
60) -> Result<()> {
61 let canonical = std::fs::canonicalize(path)
62 .with_context(|| format!("Cannot resolve archive path: {}", path.display()))?;
63
64 if !pushed.insert(canonical.clone()) {
65 return Ok(());
66 }
67
68 let name = canonical
69 .file_name()
70 .context("Archive path has no file name")?
71 .to_string_lossy()
72 .to_string();
73
74 let already_exists = transport
76 .exists(&name)
77 .map_err(|e| anyhow::anyhow!("Failed to check remote: {e}"))?;
78
79 if already_exists {
80 println!(" {} {} (already on remote)", "·".dimmed(), name.dimmed());
81 } else {
82 let header = read_header(&canonical)?;
84
85 let archive_dir = canonical.parent().unwrap_or_else(|| Path::new("."));
87 for parent_path in &header.parent_paths {
88 let parent = resolve_parent(archive_dir, parent_path);
89 if parent.exists() {
90 push_archive(&parent, transport, pushed)?;
91 } else {
92 eprintln!(
93 " {} Parent not found locally: {} (skipping)",
94 "!".yellow(),
95 parent_path
96 );
97 }
98 }
99
100 println!(" {} Uploading {}...", "→".yellow(), name.cyan());
101 transport
102 .upload(&canonical, &name)
103 .map_err(|e| anyhow::anyhow!("Upload failed: {e}"))?;
104 println!(" {} {}", "✓".green(), name.green());
105 }
106
107 Ok(())
108}
109
110fn read_header(path: &Path) -> Result<Header> {
112 let backend = MmapBackend::new(path)
113 .map_err(|e| anyhow::anyhow!("Cannot open archive {}: {e}", path.display()))?;
114 let header_bytes = backend
115 .read_exact(0, HEADER_SIZE)
116 .map_err(|e| anyhow::anyhow!("Cannot read header: {e}"))?;
117 let header: Header = bincode::deserialize(&header_bytes).context("Invalid archive header")?;
118 Ok(header)
119}
120
121fn resolve_parent(archive_dir: &Path, parent_path: &str) -> PathBuf {
123 let p = Path::new(parent_path);
124 if p.is_absolute() && p.exists() {
125 return p.to_path_buf();
126 }
127 let rel = archive_dir.join(parent_path);
128 if rel.exists() {
129 return rel;
130 }
131 p.to_path_buf()
132}