1use std::path::Path;
10
11use bynk_project::manifest::{DependencySource, package_config_from};
12
13use super::archive;
14use super::{Client, TokenSource};
15use crate::fetch::{FetchError, fail};
16
17pub(crate) const PUBLISH_REFUSED: &str = "bynk.registry.publish_refused";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Published {
22 pub name: String,
23 pub version: String,
24 pub checksum: String,
25 pub registry: String,
26 pub dry_run: bool,
28}
29
30pub fn publish(project: &Path, dry_run: bool) -> Result<Published, FetchError> {
32 let doc = crate::fetch::read_manifest(project)
33 .ok_or_else(|| fail(PUBLISH_REFUSED, "there is no readable `bynk.toml` here"))?;
34 let package = doc
35 .get("package")
36 .and_then(|v| v.as_table())
37 .and_then(|t| package_config_from(t).ok())
38 .ok_or_else(|| {
39 fail(
40 PUBLISH_REFUSED,
41 "this project isn't a package: `bynk.toml` has no `[package]`",
42 )
43 })?;
44 let version = package.version.clone().ok_or_else(|| {
45 fail(
46 PUBLISH_REFUSED,
47 "`[package]` has no `version`, which publishing needs",
48 )
49 })?;
50 for spec in crate::fetch::specs(&doc, "dependencies") {
51 if !matches!(spec.source, DependencySource::Version(_)) {
52 return Err(fail(
53 PUBLISH_REFUSED,
54 format!(
55 "the dependency `{}` is a path or git dependency, which a registry refuses — publish it, and depend on it by `version`",
56 spec.alias
57 ),
58 ));
59 }
60 }
61 let name = format!("{}/{}", package.org, package.package);
62 let (bytes, checksum) = archive::pack(project)
63 .map_err(|e| fail(PUBLISH_REFUSED, format!("could not pack the package: {e}")))?;
64 let (mut client, registry) = client_for(project, &package.org);
65 client.writable(®istry)?;
67 let mut out = Published {
68 name: name.clone(),
69 version: version.clone(),
70 checksum: checksum.clone(),
71 registry: registry.clone(),
72 dry_run,
73 };
74 if dry_run {
75 return Ok(out);
76 }
77 let (status, body) =
78 client.api_write(®istry, "PUT", &format!("{name}/{version}"), Some(&bytes))?;
79 if status != 201 && status != 200 {
80 return Err(refused(&client, ®istry, &package.org, status, &body));
81 }
82 client.forget(®istry, &name);
84 let lines = client.lines(®istry, &name)?;
85 let line = lines.iter().find(|l| l.version == version).ok_or_else(|| {
86 fail(
87 PUBLISH_REFUSED,
88 format!(
89 "registry `{registry}` accepted `{name}` {version}, but its index doesn't list it"
90 ),
91 )
92 })?;
93 if line.checksum != checksum {
94 return Err(fail(
95 crate::fetch::CHECKSUM_MISMATCH,
96 format!(
97 "registry `{registry}` signed `{name}` {version} as the tree {}, but bynk sent {checksum}",
98 line.checksum
99 ),
100 ));
101 }
102 out.checksum = line.checksum.clone();
103 Ok(out)
104}
105
106pub fn yank(
109 project: &Path,
110 package: &str,
111 version: &str,
112 undo: bool,
113) -> Result<String, FetchError> {
114 let org = package.split('/').next().unwrap_or_default();
115 let (mut client, registry) = client_for(project, org);
116 let verb = if undo { "unyank" } else { "yank" };
117 let (status, body) = client.api_write(
118 ®istry,
119 "POST",
120 &format!("{package}/{version}/{verb}"),
121 None,
122 )?;
123 if !(200..300).contains(&status) {
124 return Err(refused(&client, ®istry, org, status, &body));
125 }
126 client.forget(®istry, package);
127 let lines = client.lines(®istry, package)?;
128 match lines.iter().find(|l| l.version == version) {
129 Some(l) if l.yanked == undo => Err(fail(
130 PUBLISH_REFUSED,
131 format!("registry `{registry}` accepted the {verb}, but its index doesn't show it"),
132 )),
133 Some(_) => Ok(registry),
134 None => Err(fail(
135 PUBLISH_REFUSED,
136 format!("registry `{registry}` has no `{package}` {version}"),
137 )),
138 }
139}
140
141fn client_for(project: &Path, org: &str) -> (Client, String) {
144 let root = bynk_project::lock::lock_root(project);
145 let lock = std::fs::read_to_string(root.join(bynk_project::lock::LOCK_FILE))
146 .ok()
147 .and_then(|t| bynk_project::lock::parse_lock(&t).ok());
148 let client = Client::new(super::commands::registries_of(&root), lock.as_ref()).acting_for(org);
149 let registry = client.route(org);
150 (client, registry)
151}
152
153fn refused(client: &Client, registry: &str, org: &str, status: u16, body: &[u8]) -> FetchError {
156 let parsed: Option<(String, String)> = serde_json::from_slice::<serde_json::Value>(body)
157 .ok()
158 .and_then(|v| {
159 Some((
160 v.get("error")?.as_str()?.to_owned(),
161 v.get("message")?.as_str()?.to_owned(),
162 ))
163 });
164 let source = client.token_source(registry);
165 let hint = token_hint(registry, org, status, source);
166 let why = match parsed {
167 Some((code, message)) => {
168 format!("registry `{registry}` refused it (HTTP {status}, `{code}`): {message}")
169 }
170 None if status == 401 => format!("registry `{registry}` needs a publish token (HTTP 401)"),
171 None => format!("registry `{registry}` refused it (HTTP {status})"),
172 };
173 fail(PUBLISH_REFUSED, format!("{why}{hint}"))
174}
175
176fn token_hint(registry: &str, org: &str, status: u16, source: Option<&TokenSource>) -> String {
179 let save = format!("save one for `{org}` with `bynk org token mint {org} --save`");
180 match (status, source) {
181 (401, None) => format!(
182 "\n = note: no token was found: set `{}`, or {save}",
183 bynk_project::manifest::registry_token_var(registry)
184 ),
185 (401, Some(source @ TokenSource::Organisation { .. })) => format!(
186 "\n = note: the token came from {source}; if it expired or was revoked, replace it with `bynk org token mint {org} --save --force`"
187 ),
188 (403, Some(source @ TokenSource::Organisation { .. })) => {
189 format!("\n = note: the token came from {source}")
190 }
191 (401 | 403, Some(source @ TokenSource::Var(_))) => format!(
192 "\n = note: the token came from {source}, which is used instead of any saved token"
193 ),
194 (401 | 403, Some(source @ TokenSource::Registry(_))) => {
195 format!("\n = note: the token came from {source}; {save}")
196 }
197 _ => String::new(),
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn a_refused_token_s_source_is_named() {
207 let var = TokenSource::Var("BYNK_REGISTRY_BYNK_TOKEN".into());
208 let wide = TokenSource::Registry("bynk".into());
209 let own = TokenSource::Organisation {
210 registry: "bynk".into(),
211 org: "acme".into(),
212 };
213 assert_eq!(
214 token_hint("bynk", "acme", 401, None),
215 "\n = note: no token was found: set `BYNK_REGISTRY_BYNK_TOKEN`, or save one for `acme` with `bynk org token mint acme --save`"
216 );
217 assert_eq!(
218 token_hint("bynk", "acme", 403, Some(&wide)),
219 "\n = note: the token came from `[registries.bynk] token`; save one for `acme` with `bynk org token mint acme --save`"
220 );
221 assert_eq!(
222 token_hint("bynk", "acme", 403, Some(&own)),
223 "\n = note: the token came from `[registries.bynk.organisations.acme] token`"
224 );
225 assert!(token_hint("bynk", "acme", 401, Some(&var)).contains("instead of any saved token"));
226 assert!(
227 token_hint("bynk", "acme", 401, Some(&own))
228 .ends_with("`bynk org token mint acme --save --force`")
229 );
230 assert_eq!(token_hint("bynk", "acme", 409, Some(&own)), "");
231 }
232}