use std::collections::BTreeSet; use crank_core::WorkspaceId; use crank_registry::{ArtifactSourceId, DetachArtifactSourceRequest, ImportJobSourceEnvelope}; use time::OffsetDateTime; use crate::{error::ApiError, service::OpenApiUploadLocale}; use super::canonical_external_document_uri; const MAX_IMPORT_JOB_DOCUMENTS: usize = 32; pub(super) fn import_job_source( payload: &serde_json::Value, locale: OpenApiUploadLocale, ) -> Result { let source = payload .get("source") .ok_or_else(|| ApiError::openapi_upload(locale, "source_unavailable"))?; let source_id = source .get("source_id") .and_then(serde_json::Value::as_str) .filter(|value| value.len() <= 132) .ok_or_else(|| ApiError::openapi_upload(locale, "source_unavailable"))?; let digest = source .get("digest") .and_then(serde_json::Value::as_str) .and_then(|value| value.parse().ok()) .ok_or_else(|| ApiError::openapi_upload(locale, "source_integrity"))?; Ok(ImportJobSourceEnvelope { source_id: ArtifactSourceId::new(source_id), digest, }) } pub(super) fn import_job_dependencies( payload: &serde_json::Value, locale: OpenApiUploadLocale, ) -> Result, ApiError> { let empty = Vec::new(); let items = payload .get("dependency_snapshots") .and_then(serde_json::Value::as_array) .unwrap_or(&empty); if items.len() > MAX_IMPORT_JOB_DOCUMENTS { return Err(ApiError::openapi_upload(locale, "source_integrity")); } let mut canonical_uris = BTreeSet::new(); items .iter() .map(|item| { let canonical_uri = item .get("canonical_uri") .and_then(serde_json::Value::as_str) .filter(|value| { canonical_external_document_uri(None, value).as_deref() == Some(*value) }) .ok_or_else(|| ApiError::openapi_upload(locale, "source_integrity"))?; if !canonical_uris.insert(canonical_uri.to_owned()) { return Err(ApiError::openapi_upload(locale, "source_integrity")); } let source_id = item .get("source_id") .and_then(serde_json::Value::as_str) .filter(|value| value.len() <= 132) .ok_or_else(|| ApiError::openapi_upload(locale, "source_integrity"))?; let digest = item .get("digest") .and_then(serde_json::Value::as_str) .and_then(|value| value.parse().ok()) .ok_or_else(|| ApiError::openapi_upload(locale, "source_integrity"))?; Ok(( canonical_uri.to_owned(), ImportJobSourceEnvelope { source_id: ArtifactSourceId::new(source_id), digest, }, )) }) .collect() } pub(super) fn import_job_normalization_config( payload: &serde_json::Value, current: &crank_import::rest::NormalizationConfig, legacy_v1: bool, locale: OpenApiUploadLocale, ) -> Result { if legacy_v1 { return Ok(current.clone()); } payload .pointer("/normalization/config") .cloned() .ok_or_else(|| ApiError::openapi_upload(locale, "source_integrity")) .and_then(|value| { serde_json::from_value(value) .map_err(|_| ApiError::openapi_upload(locale, "source_integrity")) }) } pub(super) struct SourceDetachGuard { registry: crank_registry::PostgresRegistry, workspace_id: WorkspaceId, source_id: ArtifactSourceId, expected_updated_at: OffsetDateTime, armed: bool, } impl SourceDetachGuard { pub(super) fn new( registry: crank_registry::PostgresRegistry, workspace_id: WorkspaceId, source_id: ArtifactSourceId, expected_updated_at: OffsetDateTime, ) -> Self { Self { registry, workspace_id, source_id, expected_updated_at, armed: true, } } pub(super) fn disarm(&mut self) { self.armed = false; } pub(super) async fn detach_now(&mut self) { if !self.armed { return; } self.armed = false; let _ = self .registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &self.workspace_id, source_id: &self.source_id, expected_updated_at: Some(self.expected_updated_at), detached_at: OffsetDateTime::now_utc(), }) .await; } } impl Drop for SourceDetachGuard { fn drop(&mut self) { if !self.armed { return; } let registry = self.registry.clone(); let workspace_id = self.workspace_id.clone(); let source_id = self.source_id.clone(); let expected_updated_at = Some(self.expected_updated_at); if let Ok(handle) = tokio::runtime::Handle::try_current() { handle.spawn(async move { let _ = registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &workspace_id, source_id: &source_id, expected_updated_at, detached_at: OffsetDateTime::now_utc(), }) .await; }); } } }