1use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use anyhow::{Context, Result, bail};
11use sha2::{Digest, Sha256};
12
13use crate::artifact::{Identity, Kind, Meta};
14
15pub struct Store {
17 access_key: String,
18 secret_key: String,
19 bucket: String,
20 endpoint: String,
21}
22
23impl Store {
24 pub fn from_env() -> Result<Self> {
30 let get = |key: &str| -> Result<String> {
31 let value = std::env::var(key).unwrap_or_default();
32 if value.is_empty() {
33 bail!("{key} is required to publish artifacts");
34 }
35 Ok(value)
36 };
37 Ok(Self {
38 access_key: get("ARTIFACTS_ACCESS_KEY_ID")?,
39 secret_key: get("ARTIFACTS_SECRET_ACCESS_KEY")?,
40 bucket: get("ARTIFACTS_BUCKET")?,
41 endpoint: get("ARTIFACTS_ENDPOINT")?,
42 })
43 }
44
45 fn s3(&self, args: &[&str]) -> Result<()> {
46 let status = Command::new("s3cmd")
47 .arg(format!("--access_key={}", self.access_key))
48 .arg(format!("--secret_key={}", self.secret_key))
49 .arg(format!("--host={}", self.endpoint))
50 .arg(format!("--host-bucket=%(bucket)s.{}", self.endpoint))
51 .args(args)
52 .status()
53 .context("failed to run s3cmd")?;
54 if !status.success() {
55 bail!("s3cmd {} exited with {status}", args.join(" "));
56 }
57 Ok(())
58 }
59
60 pub fn publish(&self, kind: Kind, tarball: &Path, identity: &Identity) -> Result<Meta> {
64 let meta = Meta {
65 kind,
66 sha256: sha256_of(tarball)?,
67 bytes: std::fs::metadata(tarball)
68 .with_context(|| format!("cannot stat {}", tarball.display()))?
69 .len(),
70 identity: identity.clone(),
71 };
72
73 let prefix = identity.prefix(&self.bucket);
74 let object = format!("{prefix}/{}", meta.object_key());
75 println!(">> publishing {} -> {object}", tarball.display());
76 self.s3(&["put", &tarball.to_string_lossy(), &object])?;
77
78 let meta_path = tarball.with_extension("meta");
79 std::fs::write(&meta_path, meta.to_text())
80 .with_context(|| format!("cannot write {}", meta_path.display()))?;
81 self.s3(&[
82 "put",
83 &meta_path.to_string_lossy(),
84 &format!("{prefix}/{}", Meta::meta_key(kind)),
85 ])?;
86
87 Ok(meta)
88 }
89}
90
91pub fn sha256_of(path: &Path) -> Result<String> {
97 use std::io::Read;
98
99 let mut file =
100 std::fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
101 let mut hasher = Sha256::new();
102 let mut buffer = vec![0u8; 64 * 1024];
103 loop {
104 let read = file
105 .read(&mut buffer)
106 .with_context(|| format!("cannot read {}", path.display()))?;
107 if read == 0 {
108 break;
109 }
110 hasher.update(&buffer[..read]);
111 }
112 Ok(hasher
113 .finalize()
114 .iter()
115 .fold(String::with_capacity(64), |mut hex, byte| {
116 use std::fmt::Write;
117 let _ = write!(hex, "{byte:02x}");
118 hex
119 }))
120}
121
122impl Store {
123 fn s3_get(&self, remote: &str, local: &Path) -> Result<()> {
124 self.s3(&["--force", "get", remote, &local.to_string_lossy()])
125 }
126
127 pub fn fetch_meta(&self, kind: Kind, identity: &Identity, into: &Path) -> Result<Meta> {
129 let remote = format!("{}/{}", identity.prefix(&self.bucket), Meta::meta_key(kind));
130 let local = into.join(Meta::meta_key(kind));
131 self.s3_get(&remote, &local)
132 .with_context(|| format!("no {kind} meta at {remote} — did that job publish?"))?;
133 let text = std::fs::read_to_string(&local)
134 .with_context(|| format!("cannot read {}", local.display()))?;
135 Meta::from_text(&text).with_context(|| format!("malformed {kind} meta"))
136 }
137
138 pub fn verify_object(&self, meta: &Meta, identity: &Identity, into: &Path) -> Result<PathBuf> {
143 let remote = format!("{}/{}", identity.prefix(&self.bucket), meta.object_key());
144 let local = into.join(meta.object_key());
145 self.s3_get(&remote, &local)?;
146
147 let bytes = std::fs::metadata(&local)
148 .with_context(|| format!("cannot stat {}", local.display()))?
149 .len();
150 if bytes != meta.bytes {
151 bail!(
152 "{} is {bytes} bytes, its meta says {}",
153 meta.object_key(),
154 meta.bytes
155 );
156 }
157 let sha256 = sha256_of(&local)?;
158 if sha256 != meta.sha256 {
159 bail!(
160 "{} hashes to {sha256}, its meta says {}",
161 meta.object_key(),
162 meta.sha256
163 );
164 }
165 Ok(local)
166 }
167
168 pub fn write_manifest(&self, metas: &[Meta], identity: &Identity, from: &Path) -> Result<()> {
172 let entries: Vec<serde_json::Value> = metas
173 .iter()
174 .map(|meta| {
175 serde_json::json!({
176 "kind": meta.kind.as_str(),
177 "object": meta.object_key(),
178 "sha256": meta.sha256,
179 "bytes": meta.bytes,
180 "run_attempt": meta.identity.run_attempt,
181 })
182 })
183 .collect();
184 let manifest = serde_json::json!({
185 "commit": identity.commit,
186 "run_id": identity.run_id,
187 "builder": identity.builder,
188 "artifacts": entries,
189 });
190
191 let local = from.join(crate::artifact::MANIFEST_NAME);
192 std::fs::write(&local, serde_json::to_vec_pretty(&manifest)?)
193 .with_context(|| format!("cannot write {}", local.display()))?;
194 self.s3(&[
195 "put",
196 &local.to_string_lossy(),
197 &format!(
198 "{}/{}",
199 identity.prefix(&self.bucket),
200 crate::artifact::MANIFEST_NAME
201 ),
202 ])
203 }
204}