Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'worktree-agent-a6a121745e81f639f' | 1 | //! The Maven repository: `g1t.sh/-/maven/<workspace>/`, one for each |
| 2 | //! workspace, in the standard layout. `mvn deploy` and Gradle's `publish` | |
| 3 | //! upload each file with a `PUT` and Basic credentials (any username, a g1t | |
| 4 | //! token as the password); a `Bearer` token works too, for Gradle's header | |
| 5 | //! credentials. | |
| 6 | //! | |
| 7 | //! Files are kept once, by their SHA-256, with their MD5, SHA-1 and SHA-512 | |
| 8 | //! worked out as they arrive, so the checksum files beside them are | |
| 9 | //! answered without reading them again; checksums uploaded are checked | |
| 10 | //! against them, not kept. `maven-metadata.xml` is made from the versions | |
| 11 | //! on each read: an uploaded one is taken and let go. | |
| 12 | //! | |
| 13 | //! A release's files are written once. A SNAPSHOT's builds arrive as | |
| 14 | //! timestamped files beside each other, and its metadata names the newest. | |
| 15 | //! The POM is the version's record. The artifact's `maven-metadata.xml`, which | |
| 16 | //! Maven and Gradle upload last, publishes what the deploy brought, as an | |
| 17 | //! event and an audit entry. | |
| 18 | ||
| 19 | use g1t_contracts::User; | |
| 20 | use g1t_contracts::audit::AuditActor; | |
| 21 | use g1t_contracts::events::PackageEvent; | |
| 22 | use g1t_contracts::new_id; | |
| 23 | use g1t_kit::now_ms; | |
| 24 | use serde_json::{Value, json}; | |
| 25 | use worker::{Context, Headers, Method, Request, Response, ResponseBody, Result}; | |
| 26 | ||
| 27 | use crate::access::{self, Action}; | |
| 28 | use crate::db::{Checksums, NewFile, NewVersion, PackageRow, VersionRow}; | |
| 29 | use crate::digest::Digest; | |
| 30 | use crate::maven::{self, Checksum, MavenPath}; | |
| 31 | use crate::npm; | |
| 32 | use crate::oci::{Credentials, published_by}; | |
| 33 | use crate::store::BlobStore; | |
| 34 | use crate::{Caller, Packages, TargetOf}; | |
| 35 | ||
| 36 | const MAVEN: &str = "maven"; | |
| 37 | /// The most versions an artifact's metadata lists. | |
| 38 | const MAX_VERSIONS: u32 = 5000; | |
| 39 | /// The longest POM read for its description and source. | |
| 40 | const MAX_POM_BYTES: usize = 1024 * 1024; | |
| 41 | const DOCS: &str = "https://docs.g1t.sh/guides/maven/"; | |
| 42 | const TOKENS: &str = "https://g1t.sh/settings/tokens"; | |
| 43 | ||
| 44 | /// A plain-text answer. Maven and Gradle print the status; the text says | |
| 45 | /// why for anyone reading the response. | |
| 46 | fn error(status: u16, message: impl Into<String>) -> Result<Response> { | |
| 47 | let mut response = Response::ok(format!("{}\n", message.into()))?.with_status(status); | |
| 48 | response.headers_mut().set("content-type", "text/plain; charset=utf-8")?; | |
| 49 | if status == 401 { | |
| 50 | response.headers_mut().set("www-authenticate", "Basic realm=\"g1t\"")?; | |
| 51 | } | |
| 52 | Ok(response) | |
| 53 | } | |
| 54 | ||
| 55 | fn created() -> Result<Response> { | |
| 56 | Ok(Response::empty()?.with_status(201)) | |
| 57 | } | |
| 58 | ||
| 59 | fn sign_in() -> String { | |
| 60 | format!("Sign in to use this repository: Basic credentials with a g1t access token from {TOKENS} as the password. See {DOCS}") | |
| 61 | } | |
| 62 | ||
| 63 | /// A file or document as Maven reads it; a `HEAD` gets its headers alone. | |
| 64 | fn serve(bytes: Vec<u8>, media_type: &str, head: bool, cache: &str) -> Result<Response> { | |
| 65 | let headers = Headers::new(); | |
| 66 | headers.set("content-type", media_type)?; | |
| 67 | headers.set("content-length", &bytes.len().to_string())?; | |
| 68 | headers.set("cache-control", cache)?; | |
| 69 | let body = if head { ResponseBody::Empty } else { ResponseBody::Body(bytes) }; | |
| 70 | Ok(Response::from_body(body)?.with_headers(headers)) | |
| 71 | } | |
| 72 | ||
| 73 | impl Packages { | |
| 74 | /// Answers a Maven request. | |
| 75 | pub async fn maven(&self, request: Request, ctx: &Context) -> Result<Response> { | |
| 76 | let url = request.url()?; | |
| 77 | let Some((workspace, path)) = maven::route(url.path()) else { | |
| 78 | return error(404, "There is nothing at this address."); | |
| 79 | }; | |
| 80 | match self.maven_route(request, &workspace, path, ctx).await { | |
| 81 | Ok(response) => Ok(response), | |
| 82 | Err(problem) => { | |
| 83 | worker::console_error!("packages: maven {}: {problem}", url.path()); | |
| 84 | error(500, "Something went wrong on our side. Try again in a moment.") | |
| 85 | } | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | async fn maven_route(&self, mut request: Request, workspace: &str, path: MavenPath, ctx: &Context) -> Result<Response> { | |
| 90 | let method = request.method(); | |
| 91 | let credentials = self.credentials(&request).await?; | |
| 92 | let read = matches!(method, Method::Get | Method::Head); | |
| 93 | if read && let Some(refused) = self.limited(&request, &credentials, "Basic credentials with a g1t token").await? { | |
| 94 | return Ok(refused); | |
| 95 | } | |
| 96 | let viewer = match credentials { | |
| 97 | Credentials::Viewer(viewer) => viewer, | |
| 98 | Credentials::None => None, | |
| 99 | Credentials::Token(_) | Credentials::Bad => { | |
| 100 | return error(401, format!("The token is not right, or has expired. Make an access token at {TOKENS}.")); | |
| 101 | } | |
| 102 | }; | |
| 103 | let viewer = viewer.as_ref(); | |
| 104 | let head = method == Method::Head; | |
| 105 | match (path, method) { | |
| 106 | (MavenPath::ArtifactMetadata { group, artifact, checksum }, Method::Get | Method::Head) => { | |
| 107 | self.maven_metadata(workspace, &group, &artifact, None, checksum, viewer, head).await | |
| 108 | } | |
| 109 | (MavenPath::VersionMetadata { group, artifact, version, checksum }, Method::Get | Method::Head) => { | |
| 110 | self.maven_metadata(workspace, &group, &artifact, Some(&version), checksum, viewer, head).await | |
| 111 | } | |
| 112 | (MavenPath::File { group, artifact, version, file, checksum }, Method::Get | Method::Head) => { | |
| 113 | self.maven_file(workspace, &maven::package_name(&group, &artifact), &version, &file, checksum, viewer, head, ctx) | |
| 114 | .await | |
| 115 | } | |
| 116 | (MavenPath::File { group, artifact, version, file, checksum: None }, Method::Put) => { | |
| 117 | self.maven_upload(&mut request, workspace, &group, &artifact, &version, &file, viewer).await | |
| 118 | } | |
| 119 | (MavenPath::File { group, artifact, version, file, checksum: Some(checksum) }, Method::Put) => { | |
| 120 | let name = maven::package_name(&group, &artifact); | |
| 121 | self.maven_checksum(&mut request, workspace, &name, &version, &file, checksum, viewer).await | |
| 122 | } | |
| 123 | (path @ (MavenPath::ArtifactMetadata { .. } | MavenPath::VersionMetadata { .. }), Method::Put) => { | |
| 124 | self.maven_metadata_upload(&mut request, workspace, &path, viewer).await | |
| 125 | } | |
| 126 | _ => error(405, "Not a method this address takes. Versions are deleted on the package's page."), | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | async fn maven_package(&self, workspace: &str, name: &str) -> Result<Option<PackageRow>> { | |
| 131 | Ok(self.db.package(workspace, MAVEN, name).await?.filter(|p| !p.hidden())) | |
| 132 | } | |
| 133 | ||
| 134 | /// The answer for something that is not there: to someone not signed | |
| 135 | /// in, a 401 when the workspace has private artifacts (so Maven sends | |
| 136 | /// its credentials and asks again, and a private artifact looks like a | |
| 137 | /// missing one), else a 404. | |
| 138 | async fn maven_absent(&self, workspace: &str, viewer: Option<&User>) -> Result<Response> { | |
| 139 | if viewer.is_none() && self.db.has_private(workspace, MAVEN).await? { | |
| 140 | return error(401, sign_in()); | |
| 141 | } | |
| 142 | error(404, "Not found: no such artifact or file, or you cannot see it.") | |
| 143 | } | |
| 144 | ||
| 145 | /// Whether `viewer` may `action` the artifact, as the answer when not. | |
| 146 | async fn maven_check(&self, viewer: Option<&User>, package: &PackageRow, action: Action) -> Result<Option<Response>> { | |
| 147 | let target = TargetOf::package(package); | |
| 148 | let decision = access::decide(viewer, &target.view(), action); | |
| 149 | if decision.allowed { | |
| 150 | return Ok(None); | |
| 151 | } | |
| 152 | let readable = action != Action::Pull && access::decide(viewer, &target.view(), Action::Pull).allowed; | |
| 153 | if !readable && viewer.is_none() { | |
| 154 | return Ok(Some(error(401, sign_in())?)); | |
| 155 | } | |
| 156 | if !readable { | |
| 157 | return Ok(Some(self.maven_absent(&package.workspace, viewer).await?)); | |
| 158 | } | |
| 159 | Ok(Some(error(403, decision.reason.unwrap_or_else(|| "Not allowed.".to_owned()))?)) | |
| 160 | } | |
| 161 | ||
| 162 | /// `maven-metadata.xml` of an artifact, or of one of its SNAPSHOTs, or | |
| 163 | /// a checksum of it. | |
| 164 | #[allow(clippy::too_many_arguments)] | |
| 165 | async fn maven_metadata( | |
| 166 | &self, | |
| 167 | workspace: &str, | |
| 168 | group: &str, | |
| 169 | artifact: &str, | |
| 170 | snapshot: Option<&str>, | |
| 171 | checksum: Option<Checksum>, | |
| 172 | viewer: Option<&User>, | |
| 173 | head: bool, | |
| 174 | ) -> Result<Response> { | |
| 175 | let Some(package) = self.maven_package(workspace, &maven::package_name(group, artifact)).await? else { | |
| 176 | return self.maven_absent(workspace, viewer).await; | |
| 177 | }; | |
| 178 | if let Some(refusal) = self.maven_check(viewer, &package, Action::Pull).await? { | |
| 179 | return Ok(refusal); | |
| 180 | } | |
| 181 | let xml = match snapshot { | |
| 182 | None => { | |
| 183 | let versions: Vec<String> = self.db.versions(&package.id, MAX_VERSIONS).await?.into_iter().map(|v| v.version).collect(); | |
| 184 | if versions.is_empty() { | |
| 185 | return self.maven_absent(workspace, viewer).await; | |
| 186 | } | |
| 187 | maven::artifact_metadata(group, artifact, &versions, &package.updated_at) | |
| 188 | } | |
| 189 | Some(version) => { | |
| 190 | let Some(row) = self.db.version_named(&package.id, version).await? else { | |
| 191 | return self.maven_absent(workspace, viewer).await; | |
| 192 | }; | |
| 193 | let files: Vec<String> = self.db.files(&row.id).await?.into_iter().map(|f| f.name).collect(); | |
| 194 | let Some(xml) = maven::snapshot_metadata(group, artifact, version, &files) else { | |
| 195 | return self.maven_absent(workspace, viewer).await; | |
| 196 | }; | |
| 197 | xml | |
| 198 | } | |
| 199 | }; | |
| 200 | match checksum { | |
| 201 | Some(checksum) => serve(checksum.of(xml.as_bytes()).into_bytes(), "text/plain", head, "no-cache"), | |
| 202 | None => serve(xml.into_bytes(), "application/xml", head, "no-cache"), | |
| 203 | } | |
| 204 | } | |
| 205 | ||
| 206 | /// One of a version's files, or a checksum of it. | |
| 207 | #[allow(clippy::too_many_arguments)] | |
| 208 | async fn maven_file( | |
| 209 | &self, | |
| 210 | workspace: &str, | |
| 211 | name: &str, | |
| 212 | version: &str, | |
| 213 | file: &str, | |
| 214 | checksum: Option<Checksum>, | |
| 215 | viewer: Option<&User>, | |
| 216 | head: bool, | |
| 217 | ctx: &Context, | |
| 218 | ) -> Result<Response> { | |
| 219 | let Some(package) = self.maven_package(workspace, name).await? else { | |
| 220 | return self.maven_absent(workspace, viewer).await; | |
| 221 | }; | |
| 222 | if let Some(refusal) = self.maven_check(viewer, &package, Action::Pull).await? { | |
| 223 | return Ok(refusal); | |
| 224 | } | |
| 225 | let Some(row) = self.db.version_named(&package.id, version).await? else { | |
| 226 | return self.maven_absent(workspace, viewer).await; | |
| 227 | }; | |
| 228 | let Some(kept) = self.db.file(&row.id, file).await? else { | |
| 229 | return self.maven_absent(workspace, viewer).await; | |
| 230 | }; | |
| 231 | let Some(digest) = Digest::parse(&kept.digest) else { | |
| 232 | return self.maven_absent(workspace, viewer).await; | |
| 233 | }; | |
| 234 | let Some(blob) = self.db.package_blob(&package.id, &digest).await? else { | |
| 235 | return self.maven_absent(workspace, viewer).await; | |
| 236 | }; | |
| 237 | // A release's files never change; a SNAPSHOT's are timestamped, so | |
| 238 | // each name is one file too. | |
| 239 | let cache = "max-age=31536000"; | |
| 240 | if let Some(checksum) = checksum { | |
| 241 | let sums = match self.db.checksums(&digest).await? { | |
| 242 | Some(sums) => sums, | |
| 243 | None => { | |
| 244 | let Some(bytes) = self.store.read(&blob.object_key).await? else { | |
| 245 | return self.maven_absent(workspace, viewer).await; | |
| 246 | }; | |
| 247 | let sums = Checksums::of(&bytes); | |
| 248 | self.db.set_checksums(&digest, &sums).await?; | |
| 249 | sums | |
| 250 | } | |
| 251 | }; | |
| 252 | return serve(checksum.pick(&sums, digest.hex()).into_bytes(), "text/plain", head, cache); | |
| 253 | } | |
| 254 | let headers = Headers::new(); | |
| 255 | headers.set("content-type", maven::media_type(file))?; | |
| 256 | headers.set("content-length", &blob.size.to_string())?; | |
| 257 | headers.set("cache-control", cache)?; | |
| 258 | if head { | |
| 259 | return Ok(Response::from_body(ResponseBody::Empty)?.with_headers(headers)); | |
| 260 | } | |
| 261 | let Some(got) = self.store.get(&blob.object_key, None).await? else { | |
| 262 | return self.maven_absent(workspace, viewer).await; | |
| 263 | }; | |
| 264 | // A download is the artifact itself, not its POM, signature or | |
| 265 | // Gradle module file, which are read beside it. | |
| 266 | if let Some(parsed) = maven::parse_file(name.rsplit(':').next().unwrap_or(""), version, file) | |
| 267 | && parsed.classifier.is_none() | |
| 268 | && !matches!(parsed.extension.as_str(), "pom" | "module" | "asc") | |
| 269 | && !parsed.extension.ends_with(".asc") | |
| 270 | { | |
| 271 | self.count_download(&package.id, ctx); | |
| 272 | } | |
| 273 | Ok(Response::from_body(got.body)?.with_headers(headers)) | |
| 274 | } | |
| 275 | ||
| 276 | /// Who may upload to a new artifact: the repository its artifactId | |
| 277 | /// names, else the workspace. | |
| 278 | async fn maven_target(&self, workspace: &str, candidates: &[String]) -> Result<TargetOf> { | |
| 279 | let mut repo = None; | |
| 280 | for candidate in candidates { | |
| 281 | if let Some(found) = self.repo_by_name(workspace, candidate).await? { | |
| 282 | repo = Some(found); | |
| 283 | break; | |
| 284 | } | |
| 285 | } | |
| 286 | Ok(TargetOf { workspace: workspace.to_owned(), repo: repo.map(|r| (r.id, r.name, r.is_private)), public: false }) | |
| 287 | } | |
| 288 | ||
| 289 | /// Whether `viewer` may upload to the artifact (made on its first | |
| 290 | /// file), as the answer when not. | |
| 291 | fn maven_refusal(&self, viewer: Option<&User>, target: &TargetOf, exists: bool) -> Option<Result<Response>> { | |
| 292 | let decision = access::decide(viewer, &target.view(), Action::Push); | |
| 293 | if decision.allowed { | |
| 294 | return None; | |
| 295 | } | |
| 296 | if viewer.is_none() { | |
| 297 | return Some(error(401, sign_in())); | |
| 298 | } | |
| 299 | if exists && !access::decide(viewer, &target.view(), Action::Pull).allowed { | |
| 300 | return Some(error(404, "Not found: no such artifact, or you cannot see it.")); | |
| 301 | } | |
| 302 | Some(error(403, decision.reason.unwrap_or_else(|| "Not allowed.".to_owned()))) | |
| 303 | } | |
| 304 | ||
| 305 | async fn maven_body(&self, request: &mut Request) -> Result<std::result::Result<Vec<u8>, Response>> { | |
| 306 | let declared = request.headers().get("content-length")?.and_then(|n| n.parse::<u64>().ok()); | |
| 307 | let too_large = || { | |
| 308 | let mb = self.max_request / 1_000_000; | |
| 309 | error(413, format!("A file may be at most {mb} MB. See {DOCS}#size")) | |
| 310 | }; | |
| 311 | if declared.is_some_and(|n| n > self.max_request) { | |
| 312 | return Ok(Err(too_large()?)); | |
| 313 | } | |
| 314 | let bytes = request.bytes().await?; | |
| 315 | if bytes.len() as u64 > self.max_request { | |
| 316 | return Ok(Err(too_large()?)); | |
| 317 | } | |
| 318 | Ok(Ok(bytes)) | |
| 319 | } | |
| 320 | ||
| 321 | /// A `PUT` of one of a version's files. | |
| 322 | #[allow(clippy::too_many_arguments)] | |
| 323 | async fn maven_upload( | |
| 324 | &self, | |
| 325 | request: &mut Request, | |
| 326 | workspace: &str, | |
| 327 | group: &str, | |
| 328 | artifact: &str, | |
| 329 | version: &str, | |
| 330 | file: &str, | |
| 331 | viewer: Option<&User>, | |
| 332 | ) -> Result<Response> { | |
| 333 | let bytes = match self.maven_body(request).await? { | |
| 334 | Ok(bytes) => bytes, | |
| 335 | Err(refused) => return Ok(refused), | |
| 336 | }; | |
| 337 | if bytes.is_empty() { | |
| 338 | return error(400, format!("{file} is empty.")); | |
| 339 | } | |
| 340 | let Some(parsed) = maven::parse_file(artifact, version, file) else { | |
| 341 | return error(400, format!("{file} is not a file of {artifact} {version}.")); | |
| 342 | }; | |
| 343 | let is_pom = parsed.classifier.is_none() && parsed.extension == "pom"; | |
| 344 | let pom = if is_pom { | |
| 345 | let pom = match maven::read_pom(&bytes[..bytes.len().min(MAX_POM_BYTES)]) { | |
| 346 | Ok(pom) => pom, | |
| 347 | Err(message) => return error(400, message), | |
| 348 | }; | |
| 349 | if pom.group != group || pom.artifact != artifact || pom.version != version { | |
| 350 | return error( | |
| 351 | 400, | |
| 352 | format!( | |
| 353 | "The POM says {}:{}:{}, but it was uploaded as {group}:{artifact}:{version}.", | |
| 354 | pom.group, pom.artifact, pom.version | |
| 355 | ), | |
| 356 | ); | |
| 357 | } | |
| 358 | Some(pom) | |
| 359 | } else { | |
| 360 | None | |
| 361 | }; | |
| 362 | ||
| 363 | let name = maven::package_name(group, artifact); | |
| 364 | let found = self.db.package(workspace, MAVEN, &name).await?; | |
| 365 | if found.as_ref().is_some_and(PackageRow::hidden) || (found.is_none() && self.db.workspace_hidden(workspace).await?) { | |
| 366 | return error(403, format!("The workspace {workspace} is deleted; nothing can be published to it.")); | |
| 367 | } | |
| 368 | let target = match &found { | |
| 369 | Some(package) => TargetOf::package(package), | |
| 370 | None => { | |
| 371 | let lower = artifact.to_ascii_lowercase(); | |
| 372 | self.maven_target(workspace, &[lower]).await? | |
| 373 | } | |
| 374 | }; | |
| 375 | if let Some(refusal) = self.maven_refusal(viewer, &target, found.is_some()) { | |
| 376 | return refusal; | |
| 377 | } | |
| 378 | let caller = Caller { actor: viewer.map(AuditActor::of) }; | |
| 379 | let now = now_ms(); | |
| 380 | let package = match found { | |
| 381 | Some(package) => package, | |
| 382 | None => { | |
| 383 | self.db | |
| 384 | .create_package( | |
| 385 | &new_id("pkg", now), | |
| 386 | workspace, | |
| 387 | MAVEN, | |
| 388 | &name, | |
| 389 | target.repo.as_ref().map(|(id, repo, private)| (id.as_str(), repo.as_str(), *private)), | |
| 390 | caller.actor.as_ref().map_or("", |actor| actor.actor_id.as_str()), | |
| 391 | now, | |
| 392 | ) | |
| 393 | .await? | |
| 394 | } | |
| 395 | }; | |
| 396 | ||
| 397 | let digest = Digest::of(&bytes); | |
| 398 | let size = bytes.len() as u64; | |
| 399 | let (row, _) = self | |
| 400 | .db | |
| 401 | .version_or_new( | |
| 402 | &NewVersion { | |
| 403 | id: new_id("ver", now), | |
| 404 | package_id: package.id.clone(), | |
| 405 | version: version.to_owned(), | |
| 406 | digest: digest.to_string(), | |
| 407 | size: 0, | |
| 408 | metadata: "{}".to_owned(), | |
| 409 | subject: None, | |
| 410 | published_by: published_by(&caller), | |
| 411 | files: Vec::new(), | |
| 412 | }, | |
| 413 | now, | |
| 414 | ) | |
| 415 | .await?; | |
| 416 | if let Some(kept) = self.db.file(&row.id, file).await? { | |
| 417 | if kept.digest == digest.to_string() { | |
| 418 | return created(); | |
| 419 | } | |
| 420 | if !maven::is_snapshot(version) { | |
| 421 | return error( | |
| 422 | 409, | |
| 423 | format!("{file} is already published in {name} {version}, and a release's files are published once. Bump the version."), | |
| 424 | ); | |
| 425 | } | |
| 426 | } | |
| 427 | if let Some(refusal) = self.storage_refusal(&package, &[(digest.to_string(), size)]).await? { | |
| 428 | return error(403, refusal); | |
| 429 | } | |
| 430 | let stored = match self.db.blob(&digest).await? { | |
| 431 | Some(blob) => self.store.head(&blob.object_key).await?.is_some(), | |
| 432 | None => false, | |
| 433 | }; | |
| 434 | let sums = Checksums::of(&bytes); | |
| 435 | if !stored { | |
| 436 | self.store.put(&digest.object_key(), bytes).await?; | |
| 437 | } | |
| 438 | let media_type = maven::media_type(file); | |
| 439 | self.db.keep_blob(&package.id, &digest, size, Some(media_type), &digest.object_key(), now).await?; | |
| 440 | self.db.set_checksums(&digest, &sums).await?; | |
| 441 | self.db | |
| 442 | .put_file(&package.id, &row.id, &NewFile { name: file.to_owned(), digest: digest.to_string(), size, media_type: Some(media_type.to_owned()) }, now) | |
| 443 | .await?; | |
| 444 | self.db.measure(&package.workspace).await?; | |
| 445 | ||
| 446 | if let Some(pom) = pom { | |
| 447 | self.maven_pom(&package, &row, &digest, &pom, viewer).await?; | |
| 448 | } | |
| 449 | created() | |
| 450 | } | |
| 451 | ||
| 452 | /// The POM arrived: it is the version's record. Its description is | |
| 453 | /// the package's when it is the highest release, and a new artifact | |
| 454 | /// whose POM names its source on g1t is linked to that repository. | |
| 455 | async fn maven_pom(&self, package: &PackageRow, row: &VersionRow, digest: &Digest, pom: &maven::Pom, viewer: Option<&User>) -> Result<()> { | |
| 456 | let now = now_ms(); | |
| 457 | let version = row.version.as_str(); | |
| 458 | let mut metadata = row.meta(); | |
| 459 | if !metadata.is_object() { | |
| 460 | metadata = json!({}); | |
| 461 | } | |
| 462 | metadata["pom"] = json!(true); | |
| 463 | metadata["name"] = json!(pom.name); | |
| 464 | metadata["description"] = json!(pom.description); | |
| 465 | metadata["source"] = json!(pom.source); | |
| 466 | self.db.set_version(&row.id, &digest.to_string(), &metadata.to_string()).await?; | |
| 467 | let versions = self.db.versions(&package.id, MAX_VERSIONS).await?; | |
| 468 | let releases: Vec<&str> = versions.iter().map(|v| v.version.as_str()).filter(|v| !maven::is_snapshot(v)).collect(); | |
| 469 | let highest = if maven::is_snapshot(version) { | |
| 470 | releases.is_empty() | |
| 471 | } else { | |
| 472 | releases.iter().all(|other| maven::compare(other, version) != std::cmp::Ordering::Greater) | |
| 473 | }; | |
| 474 | if highest { | |
| 475 | self.db.set_readme(&package.id, None, pom.description.as_deref().or(pom.name.as_deref()), now).await?; | |
| 476 | } | |
| 477 | if package.repo_id.is_none() | |
| 478 | && versions.len() == 1 | |
| 479 | && let Some((owner, repo)) = pom.source.as_ref().and_then(|source| npm::repository_of(&Value::String(source.clone()), &self.host)) | |
| 480 | && owner == package.workspace | |
| 481 | && let Some(repo) = self.repo_by_name(&package.workspace, &repo).await? | |
| 482 | { | |
| 483 | let linked = TargetOf { | |
| 484 | workspace: package.workspace.clone(), | |
| 485 | repo: Some((repo.id.clone(), repo.name.clone(), repo.is_private)), | |
| 486 | public: false, | |
| 487 | }; | |
| 488 | if access::decide(viewer, &linked.view(), Action::Push).allowed { | |
| 489 | let visibility = if repo.is_private { "private" } else { "public" }; | |
| 490 | self.db.set_link(&package.id, Some((&repo.id, &repo.name)), visibility, now).await?; | |
| 491 | self.db.measure(&package.workspace).await?; | |
| 492 | } | |
| 493 | } | |
| 494 | Ok(()) | |
| 495 | } | |
| 496 | ||
| 497 | /// The artifact's `maven-metadata.xml` is what Maven and Gradle upload | |
| 498 | /// last: each version (or SNAPSHOT build) whose POM arrived since the | |
| 499 | /// last one is published now, with all its files, as an event and an | |
| 500 | /// audit entry. | |
| 501 | async fn maven_announce(&self, package: &PackageRow, caller: &Caller) -> Result<()> { | |
| 502 | let artifact = package.name.rsplit(':').next().unwrap_or("").to_owned(); | |
| 503 | for row in self.db.versions(&package.id, MAX_VERSIONS).await? { | |
| 504 | let mut metadata = row.meta(); | |
| 505 | if metadata["pom"] != json!(true) { | |
| 506 | continue; | |
| 507 | } | |
| 508 | let mark = if maven::is_snapshot(&row.version) { | |
| 509 | let files: Vec<String> = self.db.files(&row.id).await?.into_iter().map(|f| f.name).collect(); | |
| 510 | match maven::newest_build(&artifact, &row.version, &files) { | |
| 511 | Some((timestamp, build)) => format!("{timestamp}-{build}"), | |
| 512 | None => "unique".to_owned(), | |
| 513 | } | |
| 514 | } else { | |
| 515 | "release".to_owned() | |
| 516 | }; | |
| 517 | if metadata["published"].as_str() == Some(mark.as_str()) { | |
| 518 | continue; | |
| 519 | } | |
| 520 | metadata["published"] = json!(mark); | |
| 521 | self.db.set_version(&row.id, &row.digest, &metadata.to_string()).await?; | |
| 522 | let event = PackageEvent { | |
| 523 | version: Some(row.version.clone()), | |
| 524 | digest: Some(row.digest.clone()), | |
| 525 | size: Some(row.size), | |
| 526 | ..self.event_of(package) | |
| 527 | }; | |
| 528 | self.announce("package.published", package, event, caller).await; | |
| 529 | self.audit(caller, "package.publish", package, Some(&format!("{}/{}@{}", package.workspace, package.name, row.version)), None).await; | |
| 530 | } | |
| 531 | Ok(()) | |
| 532 | } | |
| 533 | ||
| 534 | /// A checksum uploaded beside a file: checked against the file's. | |
| 535 | #[allow(clippy::too_many_arguments)] | |
| 536 | async fn maven_checksum( | |
| 537 | &self, | |
| 538 | request: &mut Request, | |
| 539 | workspace: &str, | |
| 540 | name: &str, | |
| 541 | version: &str, | |
| 542 | file: &str, | |
| 543 | checksum: Checksum, | |
| 544 | viewer: Option<&User>, | |
| 545 | ) -> Result<Response> { | |
| 546 | let bytes = match self.maven_body(request).await? { | |
| 547 | Ok(bytes) => bytes, | |
| 548 | Err(refused) => return Ok(refused), | |
| 549 | }; | |
| 550 | let Some(package) = self.maven_package(workspace, name).await? else { | |
| 551 | return self.maven_absent(workspace, viewer).await; | |
| 552 | }; | |
| 553 | if let Some(refusal) = self.maven_check(viewer, &package, Action::Push).await? { | |
| 554 | return Ok(refusal); | |
| 555 | } | |
| 556 | let Some(row) = self.db.version_named(&package.id, version).await? else { | |
| 557 | return error(404, format!("Upload {file} before its checksum.")); | |
| 558 | }; | |
| 559 | let Some(kept) = self.db.file(&row.id, file).await? else { | |
| 560 | return error(404, format!("Upload {file} before its checksum.")); | |
| 561 | }; | |
| 562 | let Some(digest) = Digest::parse(&kept.digest) else { | |
| 563 | return error(404, format!("Upload {file} before its checksum.")); | |
| 564 | }; | |
| 565 | let Some(sums) = self.db.checksums(&digest).await? else { | |
| 566 | return created(); | |
| 567 | }; | |
| 568 | if maven::sent_checksum(&bytes) != checksum.pick(&sums, digest.hex()) { | |
| 569 | return error(400, format!("The {} checksum sent is not {file}'s. Upload the file again.", &checksum.suffix()[1..])); | |
| 570 | } | |
| 571 | created() | |
| 572 | } | |
| 573 | ||
| 574 | /// An uploaded `maven-metadata.xml`, or its checksum: the repository | |
| 575 | /// makes its own, so it is taken from anyone who may upload and let go. | |
| 576 | /// The artifact's own (not a SNAPSHOT's, nor its checksum) ends a | |
| 577 | /// deploy, and publishes what it brought. | |
| 578 | async fn maven_metadata_upload(&self, request: &mut Request, workspace: &str, path: &MavenPath, viewer: Option<&User>) -> Result<Response> { | |
| 579 | if let Err(refused) = self.maven_body(request).await? { | |
| 580 | return Ok(refused); | |
| 581 | } | |
| 582 | let (MavenPath::ArtifactMetadata { group, artifact, .. } | MavenPath::VersionMetadata { group, artifact, .. } | MavenPath::File { group, artifact, .. }) = path; | |
| 583 | let found = self.maven_package(workspace, &maven::package_name(group, artifact)).await?; | |
| 584 | let target = match &found { | |
| 585 | Some(package) => TargetOf::package(package), | |
| 586 | // A plugin group's metadata names no artifact of its own. | |
| 587 | None => self.maven_target(workspace, &[]).await?, | |
| 588 | }; | |
| 589 | if let Some(refusal) = self.maven_refusal(viewer, &target, found.is_some()) { | |
| 590 | return refusal; | |
| 591 | } | |
| 592 | if let (Some(package), MavenPath::ArtifactMetadata { checksum: None, .. }) = (&found, path) { | |
| 593 | let caller = Caller { actor: viewer.map(AuditActor::of) }; | |
| 594 | self.maven_announce(package, &caller).await?; | |
| 595 | } | |
| 596 | created() | |
| 597 | } | |
| 598 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.