Menu
Public repository / b9bf8c65e7cff19e76ae78f73b05d66f6a1b4729
AkurAI-Build / src/repo_delete.rs
Latest change 40a6aea9365ad1de0b4db056536c1fde2ec8cb9f - Repository deletion: DELETE /api/repos/{repository}?confirm={repository} by Ólafur Búi Ólafsson
//! Deleting a repository from the forge: its database rows first (atomic,
//! refused while a run is queued or running), then the files those rows
//! owned under the data root — the hosted bare repository, the cached browse
//! mirror, and each run's artifact store and workspace.
use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use serde::Serialize;
use crate::{
db::{self, Database, RepositoryDeletion},
hosted_git, tree,
};
#[derive(Debug, Serialize)]
pub struct RepositoryRemoval {
#[serde(flatten)]
pub deleted: RepositoryDeletion,
/// Directories removed from disk, absolute.
pub removed_paths: Vec<PathBuf>,
}
pub fn delete(database: &Database, data_root: &Path, name: &str) -> Result<RepositoryRemoval> {
// Validates the name before anything is deleted.
let hosted = hosted_git::repository_path(data_root, name)?;
let deleted = database.delete_repository(name)?;
let (_, workspaces, artifacts) = db::default_paths(data_root);
let run_dirs = deleted.run_ids.iter().flat_map(|id| {
[
artifacts.join(id.to_string()),
workspaces.join(id.to_string()),
]
});
let mut removed_paths = Vec::new();
for path in [hosted, tree::mirror_dir(data_root, name)]
.into_iter()
.chain(run_dirs)
{
if path.is_dir() {
fs::remove_dir_all(&path).with_context(|| {
format!(
"repository {name} rows are deleted but {} could not be removed",
path.display()
)
})?;
removed_paths.push(path);
}
}
Ok(RepositoryRemoval {
deleted,
removed_paths,
})
}
#[cfg(test)]
mod tests {
use super::*;
const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
#[test]
fn delete_removes_rows_and_owned_directories_only() -> Result<()> {
let root = tempfile::tempdir()?;
let database = Database::memory(KEY)?;
let gone = database.add_repository("gone", "https://example.com/gone.git", "main")?;
let kept = database.add_repository("kept", "https://example.com/kept.git", "main")?;
let run = database.create_run(gone.id, "main", None, "manual")?;
database.finish_run(run, "succeeded", None)?;
let kept_run = database.create_run(kept.id, "main", None, "manual")?;
let owned = [
root.path().join("hosted/gone.git"),
root.path().join("mirrors/gone.git"),
root.path().join(format!("artifacts/{run}/1")),
root.path().join(format!("workspaces/{run}")),
];
let untouched = [
root.path().join("hosted/kept.git"),
root.path().join(format!("artifacts/{kept_run}")),
];
for path in owned.iter().chain(&untouched) {
fs::create_dir_all(path)?;
}
let removal = delete(&database, root.path(), "gone")?;
assert_eq!(removal.deleted.run_ids, vec![run]);
assert_eq!(removal.removed_paths.len(), 4);
assert!(
owned.iter().all(|path| !path.exists()),
"owned dirs removed"
);
assert!(untouched.iter().all(|path| path.is_dir()), "others kept");
assert!(database.repository("gone").is_err());
assert!(database.repository("kept").is_ok());
Ok(())
}
#[test]
fn delete_rejects_invalid_names_before_touching_anything() -> Result<()> {
let root = tempfile::tempdir()?;
let database = Database::memory(KEY)?;
assert!(delete(&database, root.path(), "../escape").is_err());
Ok(())
}
}