Skip to content

Commit a1dc9af

Browse files
authored
Merge branch 'main' into changes-stream
2 parents 7366888 + 1fbe2ce commit a1dc9af

10 files changed

+471
-16
lines changed

api/.sqlx/query-1d1876dcffdc0d3d1eead78a1514f3cac87830214e21abdeb5be583c1f6804b1.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
alter table package_version_dependencies
2+
drop constraint package_version_dependencies_package_scope_package_name_fkey;
3+
4+
alter table package_version_dependencies
5+
add foreign key (package_scope, package_name) references packages ON UPDATE CASCADE ON DELETE CASCADE;
6+
7+
8+
alter table package_version_dependencies
9+
drop constraint package_version_dependencies_package_scope_package_name_pa_fkey;
10+
11+
alter table package_version_dependencies
12+
add constraint package_version_dependencies_package_scope_package_name_pa_fkey
13+
foreign key (package_scope, package_name, package_version) references package_versions ON UPDATE CASCADE ON DELETE CASCADE;
14+
15+
alter table package_files
16+
drop constraint package_files_scope_name_version_fkey;
17+
18+
alter table package_files
19+
add foreign key (scope, name, version) references package_versions
20+
ON UPDATE CASCADE ON DELETE CASCADE;
21+
22+
alter table npm_tarballs
23+
drop constraint npm_tarballs_scope_name_version_fkey;
24+
25+
alter table npm_tarballs
26+
add foreign key (scope, name, version) references package_versions
27+
on UPDATE CASCADE ON DELETE CASCADE;

api/src/api/errors.rs

+4
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ errors!(
237237
status: BAD_REQUEST,
238238
"The requested package is archived. Unarchive it to modify settings or publish to it.",
239239
},
240+
DeleteVersionHasDependents {
241+
status: BAD_REQUEST,
242+
"The requested package version has dependents. Only a version without dependents can be deleted.",
243+
},
240244
);
241245

242246
pub fn map_unique_violation(err: sqlx::Error, new_err: ApiError) -> ApiError {

api/src/api/package.rs

+170-2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ pub fn package_router() -> Router<Body, ApiError> {
145145
"/:package/versions/:version",
146146
util::auth(version_update_handler),
147147
)
148+
.delete(
149+
"/:package/versions/:version",
150+
util::auth(version_delete_handler),
151+
)
148152
.post(
149153
"/:package/versions/:version/provenance",
150154
util::auth(version_provenance_statements_handler),
@@ -942,8 +946,98 @@ pub async fn version_update_handler(
942946
gzip_encoded: false,
943947
},
944948
)
945-
.await
946-
.unwrap();
949+
.await?;
950+
951+
let npm_version_manifest_path =
952+
crate::gcs_paths::npm_version_manifest_path(&scope, &package);
953+
let npm_version_manifest =
954+
generate_npm_version_manifest(db, npm_url, &scope, &package).await?;
955+
let content = serde_json::to_vec_pretty(&npm_version_manifest)?;
956+
buckets
957+
.npm_bucket
958+
.upload(
959+
npm_version_manifest_path.into(),
960+
UploadTaskBody::Bytes(content.into()),
961+
GcsUploadOptions {
962+
content_type: Some("application/json".into()),
963+
cache_control: Some(CACHE_CONTROL_DO_NOT_CACHE.into()),
964+
gzip_encoded: false,
965+
},
966+
)
967+
.await?;
968+
969+
Ok(
970+
Response::builder()
971+
.status(StatusCode::NO_CONTENT)
972+
.body(Body::empty())
973+
.unwrap(),
974+
)
975+
}
976+
977+
#[instrument(
978+
name = "DELETE /api/scopes/:scope/packages/:package/versions/:version",
979+
skip(req),
980+
err,
981+
fields(scope, package, version)
982+
)]
983+
pub async fn version_delete_handler(
984+
req: Request<Body>,
985+
) -> ApiResult<Response<Body>> {
986+
let scope = req.param_scope()?;
987+
let package = req.param_package()?;
988+
let version = req.param_version()?;
989+
Span::current().record("scope", field::display(&scope));
990+
Span::current().record("package", field::display(&package));
991+
Span::current().record("version", field::display(&version));
992+
993+
let db = req.data::<Database>().unwrap();
994+
let buckets = req.data::<Buckets>().unwrap().clone();
995+
let npm_url = &req.data::<NpmUrl>().unwrap().0;
996+
997+
let iam = req.iam();
998+
iam.check_admin_access()?;
999+
1000+
let count = db
1001+
.count_package_dependents(
1002+
crate::db::DependencyKind::Jsr,
1003+
&format!("@{}/{}", scope, package),
1004+
)
1005+
.await?;
1006+
1007+
if count > 0 {
1008+
return Err(ApiError::DeleteVersionHasDependents);
1009+
}
1010+
1011+
db.delete_package_version(&scope, &package, &version)
1012+
.await?;
1013+
1014+
let path = crate::gcs_paths::docs_v1_path(&scope, &package, &version);
1015+
buckets.docs_bucket.delete_file(path.into()).await?;
1016+
1017+
let path = crate::gcs_paths::version_metadata(&scope, &package, &version);
1018+
buckets.modules_bucket.delete_file(path.into()).await?;
1019+
1020+
let path =
1021+
crate::gcs_paths::file_path_root_directory(&scope, &package, &version);
1022+
buckets.modules_bucket.delete_directory(path.into()).await?;
1023+
1024+
let package_metadata_path =
1025+
crate::gcs_paths::package_metadata(&scope, &package);
1026+
let package_metadata = PackageMetadata::create(db, &scope, &package).await?;
1027+
1028+
let content = serde_json::to_vec_pretty(&package_metadata)?;
1029+
buckets
1030+
.modules_bucket
1031+
.upload(
1032+
package_metadata_path.into(),
1033+
UploadTaskBody::Bytes(content.into()),
1034+
GcsUploadOptions {
1035+
content_type: Some("application/json".into()),
1036+
cache_control: Some(CACHE_CONTROL_DO_NOT_CACHE.into()),
1037+
gzip_encoded: false,
1038+
},
1039+
)
1040+
.await?;
9471041

9481042
let npm_version_manifest_path =
9491043
crate::gcs_paths::npm_version_manifest_path(&scope, &package);
@@ -4093,4 +4187,78 @@ ggHohNAjhbzDaY2iBW/m3NC5dehGUP4T2GBo/cwGhg==
40934187
assert_eq!(tasks.len(), 1);
40944188
assert_eq!(tasks[0].id, task2.id);
40954189
}
4190+
4191+
#[tokio::test]
4192+
async fn delete_version() {
4193+
let mut t = TestSetup::new().await;
4194+
let staff_token = t.staff_user.token.clone();
4195+
4196+
// unpublished package
4197+
let mut resp = t
4198+
.http()
4199+
.get("/api/scopes/scope/packages/foo/versions/0.0.1/dependencies/graph")
4200+
.call()
4201+
.await
4202+
.unwrap();
4203+
resp
4204+
.expect_err_code(StatusCode::NOT_FOUND, "packageVersionNotFound")
4205+
.await;
4206+
4207+
let task = process_tarball_setup(&t, create_mock_tarball("ok")).await;
4208+
assert_eq!(task.status, PublishingTaskStatus::Success, "{:?}", task);
4209+
4210+
// Now publish a package that has a few deps
4211+
let package_name = PackageName::try_from("bar").unwrap();
4212+
let version = Version::try_from("1.2.3").unwrap();
4213+
let task = process_tarball_setup2(
4214+
&t,
4215+
create_mock_tarball("depends_on_ok"),
4216+
&package_name,
4217+
&version,
4218+
false,
4219+
)
4220+
.await;
4221+
assert_eq!(task.status, PublishingTaskStatus::Success, "{:?}", task);
4222+
4223+
let mut resp = t
4224+
.http()
4225+
.delete("/api/scopes/scope/packages/foo/versions/0.0.1")
4226+
.token(Some(&staff_token))
4227+
.call()
4228+
.await
4229+
.unwrap();
4230+
resp
4231+
.expect_err_code(StatusCode::BAD_REQUEST, "deleteVersionHasDependents")
4232+
.await;
4233+
4234+
let mut resp = t
4235+
.http()
4236+
.delete("/api/scopes/scope/packages/bar/versions/1.2.3")
4237+
.token(Some(&staff_token))
4238+
.call()
4239+
.await
4240+
.unwrap();
4241+
resp.expect_ok_no_content().await;
4242+
4243+
let mut resp = t
4244+
.http()
4245+
.delete("/api/scopes/scope/packages/foo/versions/0.0.1")
4246+
.token(Some(&staff_token))
4247+
.call()
4248+
.await
4249+
.unwrap();
4250+
resp.expect_ok_no_content().await;
4251+
4252+
let package_name = PackageName::try_from("foo").unwrap();
4253+
let version = Version::try_from("0.0.1").unwrap();
4254+
let task = process_tarball_setup2(
4255+
&t,
4256+
create_mock_tarball("ok"),
4257+
&package_name,
4258+
&version,
4259+
false,
4260+
)
4261+
.await;
4262+
assert_eq!(task.status, PublishingTaskStatus::Failure, "{:?}", task);
4263+
}
40964264
}

api/src/buckets.rs

+97
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use futures::Future;
88
use futures::FutureExt;
99
use futures::Stream;
1010
use futures::StreamExt;
11+
use futures::TryStreamExt;
1112
use tokio::sync::mpsc;
1213
use tokio_stream::wrappers::UnboundedReceiverStream;
1314
use tracing::instrument;
@@ -24,6 +25,8 @@ pub struct BucketWithQueue {
2425
pub bucket: gcp::Bucket,
2526
upload_queue: DynamicBackgroundTaskQueue<UploadTask>,
2627
download_queue: DynamicBackgroundTaskQueue<DownloadTask>,
28+
delete_queue: DynamicBackgroundTaskQueue<DeleteFileTask>,
29+
list_queue: DynamicBackgroundTaskQueue<ListDirectoryTask>,
2730
}
2831

2932
impl BucketWithQueue {
@@ -32,6 +35,8 @@ impl BucketWithQueue {
3235
bucket,
3336
upload_queue: DynamicBackgroundTaskQueue::default(),
3437
download_queue: DynamicBackgroundTaskQueue::default(),
38+
delete_queue: DynamicBackgroundTaskQueue::default(),
39+
list_queue: DynamicBackgroundTaskQueue::default(),
3540
}
3641
}
3742

@@ -72,6 +77,40 @@ impl BucketWithQueue {
7277
.await
7378
.unwrap()
7479
}
80+
81+
#[instrument(name = "BucketWithQueue::delete_file", skip(self), err)]
82+
pub async fn delete_file(&self, path: Arc<str>) -> Result<bool, GcsError> {
83+
self
84+
.delete_queue
85+
.run(DeleteFileTask {
86+
bucket: self.bucket.clone(),
87+
path,
88+
})
89+
.await
90+
.unwrap()
91+
}
92+
93+
#[instrument(name = "BucketWithQueue::delete_directory", skip(self), err)]
94+
pub async fn delete_directory(&self, path: Arc<str>) -> Result<(), GcsError> {
95+
let list = self
96+
.list_queue
97+
.run(ListDirectoryTask {
98+
bucket: self.bucket.clone(),
99+
path,
100+
})
101+
.await
102+
.unwrap()?;
103+
104+
if let Some(list) = list {
105+
let stream = futures::stream::iter(list.items)
106+
.map(|item| self.delete_file(item.name.into()))
107+
.buffer_unordered(64);
108+
109+
let _ = stream.try_collect::<Vec<_>>().await?;
110+
}
111+
112+
Ok(())
113+
}
75114
}
76115

77116
#[derive(Clone)]
@@ -191,3 +230,61 @@ impl RestartableTask for DownloadTask {
191230
.boxed()
192231
}
193232
}
233+
234+
struct DeleteFileTask {
235+
bucket: gcp::Bucket,
236+
path: Arc<str>,
237+
}
238+
239+
impl RestartableTask for DeleteFileTask {
240+
type Ok = bool;
241+
type Err = gcp::GcsError;
242+
type Fut =
243+
Pin<Box<dyn Future<Output = RestartableTaskResult<Self>> + Send + 'static>>;
244+
245+
fn run(self) -> Self::Fut {
246+
async move {
247+
let res = self.bucket.delete_file(&self.path).await;
248+
match res {
249+
Ok(data) => RestartableTaskResult::Ok(data),
250+
Err(e) if e.is_retryable() => {
251+
RestartableTaskResult::Backoff(DeleteFileTask {
252+
bucket: self.bucket,
253+
path: self.path,
254+
})
255+
}
256+
Err(e) => RestartableTaskResult::Error(e),
257+
}
258+
}
259+
.boxed()
260+
}
261+
}
262+
263+
struct ListDirectoryTask {
264+
bucket: gcp::Bucket,
265+
path: Arc<str>,
266+
}
267+
268+
impl RestartableTask for ListDirectoryTask {
269+
type Ok = Option<gcp::List>;
270+
type Err = gcp::GcsError;
271+
type Fut =
272+
Pin<Box<dyn Future<Output = RestartableTaskResult<Self>> + Send + 'static>>;
273+
274+
fn run(self) -> Self::Fut {
275+
async move {
276+
let res = self.bucket.list(&self.path).await;
277+
match res {
278+
Ok(data) => RestartableTaskResult::Ok(data),
279+
Err(e) if e.is_retryable() => {
280+
RestartableTaskResult::Backoff(ListDirectoryTask {
281+
bucket: self.bucket,
282+
path: self.path,
283+
})
284+
}
285+
Err(e) => RestartableTaskResult::Error(e),
286+
}
287+
}
288+
.boxed()
289+
}
290+
}

0 commit comments

Comments
 (0)