mkit_cli/commands/
pack_shard.rs1use std::fs;
22use std::io::Write;
23use std::path::{Path, PathBuf};
24
25use clap::Parser;
26use mkit_core::hash::{Hash, from_hex, to_hex};
27use mkit_core::pack_shard::{SHARD_SIZE_THRESHOLD, encode_manifest, encode_pack_to_shards};
28use mkit_core::store::ObjectStore;
29
30use crate::clap_shim;
31use crate::exit;
32
33#[derive(Debug, Parser)]
34#[command(
35 name = "mkit pack-shard",
36 about = "Encode a stored pack into Reed-Solomon shards (+ manifest)."
37)]
38struct ShardOpts {
39 hash: String,
41
42 #[arg(long)]
45 out: Option<PathBuf>,
46
47 #[arg(long)]
51 force: bool,
52}
53
54#[must_use]
55pub fn run(args: &[String]) -> u8 {
56 let opts = match clap_shim::parse::<ShardOpts>("mkit pack-shard", args) {
57 Ok(o) => o,
58 Err(code) => return code,
59 };
60
61 let cwd = match std::env::current_dir() {
62 Ok(p) => p,
63 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
64 };
65
66 let hash: Hash = match from_hex(&opts.hash) {
67 Ok(h) => h,
68 Err(_) => {
69 return emit_err(
70 &format!("invalid hash '{}': expected 64 hex chars", opts.hash),
71 exit::USAGE,
72 );
73 }
74 };
75
76 let layout = match super::resolve_layout(&cwd) {
77 Ok(layout) => layout,
78 Err(code) => return code,
79 };
80 let store = match ObjectStore::open(&layout) {
81 Ok(s) => s,
82 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
83 };
84
85 let pack = match store.read(&hash) {
86 Ok(b) => b,
87 Err(e) => return emit_err(&format!("read pack {}: {e}", to_hex(&hash)), exit::NOINPUT),
88 };
89
90 if !opts.force && (pack.len() as u64) < SHARD_SIZE_THRESHOLD {
93 return emit_err(
94 &format!(
95 "pack is {} bytes, below the {} byte (1 MiB) shard threshold; \
96 pass --force to encode anyway",
97 pack.len(),
98 SHARD_SIZE_THRESHOLD
99 ),
100 exit::USAGE,
101 );
102 }
103
104 let out_root = opts.out.unwrap_or_else(|| layout.pack_shards_dir());
105
106 let (shards, manifest) =
107 match encode_pack_to_shards(&pack, mkit_core::pack_shard::default_config()) {
108 Ok(p) => p,
109 Err(e) => return emit_err(&format!("encode: {e}"), exit::DATAERR),
110 };
111
112 let hex = to_hex(&hash);
113 let pack_dir = out_root.join("packs").join(&hex);
114 let shards_dir = pack_dir.join("shards");
115
116 if let Err(e) = fs::create_dir_all(&shards_dir) {
117 return emit_err(
118 &format!("mkdir {}: {e}", shards_dir.display()),
119 exit::CANTCREAT,
120 );
121 }
122
123 let manifest_bytes = match encode_manifest(&manifest) {
131 Ok(b) => b,
132 Err(e) => return emit_err(&format!("encode manifest: {e}"), exit::DATAERR),
133 };
134
135 for shard in &shards {
136 let path = shards_dir.join(shard.index.to_string());
137 if let Err(e) = write_atomic(&path, &shard.bytes) {
138 return emit_err(&format!("write {}: {e}", path.display()), exit::CANTCREAT);
139 }
140 }
141
142 let manifest_path = pack_dir.join("shards.manifest");
143 if let Err(e) = write_atomic(&manifest_path, &manifest_bytes) {
144 return emit_err(
145 &format!("write {}: {e}", manifest_path.display()),
146 exit::CANTCREAT,
147 );
148 }
149
150 let mut stdout = std::io::stdout().lock();
151 let _ = writeln!(
152 stdout,
153 "wrote {} shards + manifest under {}",
154 shards.len(),
155 pack_dir.display()
156 );
157 exit::OK
158}
159
160fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
165 let parent = path.parent().unwrap_or_else(|| Path::new("."));
166 let mut tmp = path.to_path_buf();
167 let fname = path.file_name().and_then(|f| f.to_str()).unwrap_or("shard");
168 tmp.set_file_name(format!(".{fname}.tmp"));
169 {
170 let mut f = fs::OpenOptions::new()
171 .write(true)
172 .create(true)
173 .truncate(true)
174 .open(&tmp)?;
175 f.write_all(bytes)?;
176 }
177 fs::rename(&tmp, path).inspect_err(|_| {
178 let _ = fs::remove_file(&tmp);
180 let _ = parent; })
182}
183
184use super::error as emit_err;
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use std::env;
190
191 #[test]
192 fn invalid_hex_returns_usage_error() {
193 let dir = tempfile::tempdir().unwrap();
197 let saved_cwd = env::current_dir().ok();
201 env::set_current_dir(dir.path()).unwrap();
202 let code = run(&["not-hex".to_string()]);
203 assert_eq!(code, exit::USAGE);
204 if let Some(p) = saved_cwd {
205 env::set_current_dir(p).unwrap();
206 }
207 }
208}