g1t/services/packages/src/maven_http.rs

598 lines27,181 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};
28use crate::db::{Checksums, NewFile, NewVersion, PackageRow, VersionRow};
29use crate::digest::Digest;
30use crate::maven::{self, Checksum, MavenPath};
31use crate::npm;
32use crate::oci::{Credentials, published_by};
33use crate::store::BlobStore;
34use crate::{Caller, Packages, TargetOf};
35
36const MAVEN: &str = "maven";
37/// The most versions an artifact's metadata lists.
38const MAX_VERSIONS: u32 = 5000;
39/// The longest POM read for its description and source.
40const MAX_POM_BYTES: usize = 1024 * 1024;
41const DOCS: &str = "https://docs.g1t.sh/guides/maven/";
42const 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.
46fn 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
55fn created() -> Result<Response> {
56 Ok(Response::empty()?.with_status(201))
57}
58
59fn 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.
64fn 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
73impl 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.