Skip to content

Commit 3c58c87

Browse files
committed
feature: crate overwriting implementation in the backend (#41)
1 parent b4213e1 commit 3c58c87

8 files changed

+164
-61
lines changed

.sqlx/query-606b54deb2a8792ced5c2bdd6f2bf7792bdeb5c4ca110d33d2d97aecd03a3ec7.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.sqlx/query-64020941f6a6cf41e841dd40caca417c0511c3f0e384cdf0505b1d2178e09417.json

Lines changed: 0 additions & 20 deletions
This file was deleted.

.sqlx/query-d93dcaeade8c53483517e8d07cfaf8aa2fb47883f59d2ad152a6cf525b727a0d.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/application.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,14 +435,14 @@ impl Application {
435435
let package = CrateUploadData::new(content)?;
436436
let index_data = package.build_index_data();
437437

438-
let (user, result, targets, capabilities) = {
438+
let (user, result, targets, capabilities, is_overwriting) = {
439439
let package = &package;
440440
self.db_transaction_write("publish_crate_version", |app| async move {
441441
let authentication = app.authenticate(auth_data).await?;
442442
authentication.check_can_write()?;
443443
let user = app.database.get_user_profile(authentication.uid()?).await?;
444444
// publish
445-
let result = app.database.publish_crate_version(user.id, package).await?;
445+
let (result, is_overwriting) = app.database.publish_crate_version(user.id, package).await?;
446446
let mut targets = app.database.get_crate_targets(&package.metadata.name).await?;
447447
if targets.is_empty() {
448448
targets.push(CrateInfoTarget {
@@ -456,13 +456,13 @@ impl Application {
456456
.await?;
457457
}
458458
let capabilities = app.database.get_crate_required_capabilities(&package.metadata.name).await?;
459-
Ok::<_, ApiError>((user, result, targets, capabilities))
459+
Ok::<_, ApiError>((user, result, targets, capabilities, is_overwriting))
460460
})
461461
.await
462462
}?;
463463

464464
self.service_storage.store_crate(&package.metadata, package.content).await?;
465-
self.service_index.publish_crate_version(&index_data).await?;
465+
self.service_index.publish_crate_version(&index_data, is_overwriting).await?;
466466
for info in targets {
467467
self.service_docs_generator
468468
.queue(

src/services/database/packages.rs

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -172,35 +172,54 @@ impl Database {
172172

173173
/// Publish a crate
174174
#[allow(clippy::similar_names)]
175-
pub async fn publish_crate_version(&self, uid: i64, package: &CrateUploadData) -> Result<CrateUploadResult, ApiError> {
176-
let warnings = package.metadata.validate()?;
175+
pub async fn publish_crate_version(
176+
&self,
177+
uid: i64,
178+
package: &CrateUploadData,
179+
) -> Result<(CrateUploadResult, bool), ApiError> {
180+
let mut warnings = package.metadata.validate()?;
177181
let lowercase = package.metadata.name.to_ascii_lowercase();
178-
let row = sqlx::query!(
182+
183+
// get existing package and version data
184+
let package_row = sqlx::query!(
185+
"SELECT name, canOverwrite AS can_overwrite FROM Package WHERE lowercase = $1 LIMIT 1",
186+
lowercase
187+
)
188+
.fetch_optional(&mut *self.transaction.borrow().await)
189+
.await?;
190+
let version_row = sqlx::query!(
179191
"SELECT upload FROM PackageVersion WHERE package = $1 AND version = $2 LIMIT 1",
180192
package.metadata.name,
181193
package.metadata.vers
182194
)
183195
.fetch_optional(&mut *self.transaction.borrow().await)
184196
.await?;
185-
if let Some(row) = row {
186-
return Err(specialize(
187-
error_invalid_request(),
188-
format!(
189-
"Package {} already exists in version {}, uploaded on {}",
190-
&package.metadata.name, &package.metadata.vers, row.upload
191-
),
192-
));
193-
}
197+
198+
// check for previous version
199+
let is_overwriting = if let Some(version_row) = version_row {
200+
// has previous version
201+
if package_row.as_ref().is_some_and(|r| !r.can_overwrite) {
202+
// cannot overwrite
203+
return Err(specialize(
204+
error_invalid_request(),
205+
format!(
206+
"Package {} already exists in version {}, uploaded on {}",
207+
&package.metadata.name, &package.metadata.vers, version_row.upload
208+
),
209+
));
210+
}
211+
true
212+
} else {
213+
false
214+
};
215+
194216
// check whether the package already exists
195-
let row = sqlx::query!("SELECT name FROM Package WHERE lowercase = $1 LIMIT 1", lowercase)
196-
.fetch_optional(&mut *self.transaction.borrow().await)
197-
.await?;
198-
if let Some(row) = row {
217+
if let Some(package_row) = package_row {
199218
// check this is the same package
200-
if row.name != lowercase {
219+
if package_row.name != lowercase {
201220
return Err(specialize(
202221
error_invalid_request(),
203-
format!("A package named {} already exists", row.name),
222+
format!("A package named {} already exists", package_row.name),
204223
));
205224
}
206225
// check the ownership
@@ -223,20 +242,37 @@ impl Database {
223242
.execute(&mut *self.transaction.borrow().await)
224243
.await?;
225244
}
245+
226246
let now = Local::now().naive_local();
227-
// create the version
228247
let description = package.metadata.description.as_ref().map_or("", String::as_str);
229-
sqlx::query!(
230-
"INSERT INTO PackageVersion (package, version, description, upload, uploadedBy, yanked, downloadCount, downloads, depsLastCheck, depsHasOutdated, depsHasCVEs) VALUES ($1, $2, $3, $4, $5, false, 0, NULL, 0, false, false)",
231-
package.metadata.name,
232-
package.metadata.vers,
233-
description,
234-
now,
235-
uid,
236-
)
237-
.execute(&mut *self.transaction.borrow().await)
238-
.await?;
239-
Ok(warnings)
248+
if is_overwriting {
249+
// update the version
250+
sqlx::query!("UPDATE PackageVersion SET description = $3, upload = $4, uploadedBy = $5, yanked = FALSE, depsLastCheck = 0, depsHasOutdated = FALSE, depsHasCVEs = 0 WHERE package = $1 AND version = $2",
251+
package.metadata.name,
252+
package.metadata.vers,
253+
description,
254+
now,
255+
uid
256+
).execute(&mut *self.transaction.borrow().await)
257+
.await?;
258+
warnings
259+
.warnings
260+
.other
261+
.push(format!("overwriting existing version {}", package.metadata.vers));
262+
} else {
263+
// create the version
264+
sqlx::query!(
265+
"INSERT INTO PackageVersion (package, version, description, upload, uploadedBy, yanked, downloadCount, downloads, depsLastCheck, depsHasOutdated, depsHasCVEs) VALUES ($1, $2, $3, $4, $5, false, 0, NULL, 0, false, false)",
266+
package.metadata.name,
267+
package.metadata.vers,
268+
description,
269+
now,
270+
uid,
271+
)
272+
.execute(&mut *self.transaction.borrow().await)
273+
.await?;
274+
}
275+
Ok((warnings, is_overwriting))
240276
}
241277

242278
/// Yank a crate version

src/services/index/git.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ impl Index for GitIndex {
4545
Box::pin(async move { self.inner.lock().await.get_upload_pack_for(input).await })
4646
}
4747

48-
fn publish_crate_version<'a>(&'a self, metadata: &'a IndexCrateMetadata) -> FaillibleFuture<'a, ()> {
49-
Box::pin(async move { self.inner.lock().await.publish_crate_version(metadata).await })
48+
fn publish_crate_version<'a>(&'a self, metadata: &'a IndexCrateMetadata, is_overwriting: bool) -> FaillibleFuture<'a, ()> {
49+
Box::pin(async move { self.inner.lock().await.publish_crate_version(metadata, is_overwriting).await })
5050
}
5151

5252
fn get_crate_data<'a>(&'a self, package: &'a str) -> FaillibleFuture<'a, Vec<IndexCrateMetadata>> {
@@ -203,12 +203,16 @@ impl GitIndexImpl {
203203
}
204204

205205
/// Publish a new version for a crate
206-
async fn publish_crate_version(&self, metadata: &IndexCrateMetadata) -> Result<(), ApiError> {
206+
async fn publish_crate_version(&self, metadata: &IndexCrateMetadata, is_overwriting: bool) -> Result<(), ApiError> {
207207
let file_name = build_package_file_path(PathBuf::from(&self.config.location), &metadata.name);
208208
create_dir_all(file_name.parent().unwrap()).await?;
209209
let buffer = serde_json::to_vec(metadata)?;
210210
// write to package file
211-
{
211+
if is_overwriting {
212+
// replace the metadata
213+
Self::publish_crate_version_replace(&file_name, metadata).await?;
214+
} else {
215+
// append the metadata at the end
212216
let mut file = OpenOptions::new().create(true).append(true).open(file_name).await?;
213217
file.write_all(&buffer).await?;
214218
file.write_all(&[0x0A]).await?; // add line end
@@ -217,7 +221,12 @@ impl GitIndexImpl {
217221
}
218222
// commit and update
219223
let location = PathBuf::from(&self.config.location);
220-
let message = format!("Publish {}:{}", &metadata.name, &metadata.vers);
224+
let message = format!(
225+
"Publish{} {}:{}",
226+
if is_overwriting { " (overwriting)" } else { "" },
227+
&metadata.name,
228+
&metadata.vers
229+
);
221230
execute_git(&location, &["add", "."]).await?;
222231
execute_git(&location, &["commit", "-m", &message]).await?;
223232
execute_git(&location, &["update-server-info"]).await?;
@@ -245,4 +254,40 @@ impl GitIndexImpl {
245254
}
246255
Ok(results)
247256
}
257+
258+
/// Replaces the metadata for a version in a file
259+
async fn publish_crate_version_replace(file_name: &Path, metadata: &IndexCrateMetadata) -> Result<(), ApiError> {
260+
// get the existing versions
261+
let mut versions = {
262+
// expect the file to be present
263+
let file = OpenOptions::new().read(true).open(file_name).await?;
264+
let reader = BufReader::new(file);
265+
let mut lines = reader.lines();
266+
let mut versions = Vec::new();
267+
while let Some(line) = lines.next_line().await? {
268+
versions.push(serde_json::from_str::<IndexCrateMetadata>(&line)?);
269+
}
270+
versions
271+
};
272+
// replace old with the new version
273+
let index = versions.iter().position(|e| e.vers == metadata.vers).ok_or_else(|| {
274+
specialize(
275+
error_not_found(),
276+
format!("could not find previous version {}", metadata.vers),
277+
)
278+
})?;
279+
versions[index] = metadata.clone();
280+
// write back
281+
{
282+
let mut file = OpenOptions::new().write(true).truncate(true).open(file_name).await?;
283+
for version in versions {
284+
let buffer = serde_json::to_vec(&version)?;
285+
file.write_all(&buffer).await?;
286+
file.write_all(&[0x0A]).await?; // add line end
287+
}
288+
file.flush().await?;
289+
file.sync_all().await?;
290+
}
291+
Ok(())
292+
}
248293
}

src/services/index/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub trait Index {
2626
fn get_upload_pack_for<'a>(&'a self, input: &'a [u8]) -> FaillibleFuture<'a, Vec<u8>>;
2727

2828
/// Publish a new version for a crate
29-
fn publish_crate_version<'a>(&'a self, metadata: &'a IndexCrateMetadata) -> FaillibleFuture<'a, ()>;
29+
fn publish_crate_version<'a>(&'a self, metadata: &'a IndexCrateMetadata, is_overwriting: bool) -> FaillibleFuture<'a, ()>;
3030

3131
/// Gets the data for a crate
3232
fn get_crate_data<'a>(&'a self, package: &'a str) -> FaillibleFuture<'a, Vec<IndexCrateMetadata>>;

src/tests/mocks.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ impl Index for MockService {
9696
resolved_default()
9797
}
9898

99-
fn publish_crate_version<'a>(&'a self, _metadata: &'a IndexCrateMetadata) -> FaillibleFuture<'a, ()> {
99+
fn publish_crate_version<'a>(
100+
&'a self,
101+
_metadata: &'a IndexCrateMetadata,
102+
_is_overwriting: bool,
103+
) -> FaillibleFuture<'a, ()> {
100104
resolved_default()
101105
}
102106

0 commit comments

Comments
 (0)