g1t/services/packages/src/maven_http.rs

681 lines31,629 bytesCodeBlame

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

This file's history is long; its oldest lines are credited to the oldest commit read.