Integrate git archival into deletion flow

- Archive git repositories before moving events to holding database
- Extract npub and identifier from repository addresses (kind 30617)
- Create tar.gz archives in .archive/<npub>/<identifier>-<timestamp>.tar.gz
- Store archive metadata with deletion info and expiry timestamp
- Pass archive path to holding database for cleanup coordination
- Continue deletion even if archival fails (logged as error)
- Export archive_repositories_for_addresses from policy module
- Fix metadata file path in cleanup test (tar.gz.json not json)
This commit is contained in:
DanConwayDev
2026-01-14 12:13:52 +00:00
parent 04e783b27a
commit 0b805089c0
4 changed files with 162 additions and 4 deletions
+1 -1
View File
@@ -326,7 +326,7 @@ mod tests {
// Create test archive files
let archive_path = archive_dir.join("test-repo-123456.tar.gz");
let metadata_path = archive_dir.join("test-repo-123456.json");
let metadata_path = archive_dir.join("test-repo-123456.tar.gz.json");
tokio::fs::write(&archive_path, b"fake archive data")
.await
+39
View File
@@ -513,6 +513,44 @@ impl Nip34WritePolicy {
return WritePolicyResult::Accept;
};
// Archive git repositories before deletion (if any addresses are repositories)
let archive_path = if !addresses.is_empty() {
let git_data_path = std::path::Path::new(&self.ctx.config.git_data_path);
let archive_base_path = git_data_path
.parent()
.unwrap_or(git_data_path)
.join(".archive");
match crate::nostr::policy::archive_repositories_for_addresses(
&addresses,
git_data_path,
&archive_base_path,
&event.id,
self.ctx.config.archive_retention_secs,
) {
Ok(path) => {
if let Some(ref p) = path {
tracing::info!(
event_id = %event_id_str,
archive_path = %p,
"Successfully archived git repository"
);
}
path
}
Err(e) => {
tracing::error!(
event_id = %event_id_str,
error = %e,
"Failed to archive git repository, continuing with deletion"
);
None // Continue with deletion even if archival fails
}
}
} else {
None
};
// Query for all dependent events
match crate::nostr::policy::query_dependent_events(
&self.ctx.database,
@@ -539,6 +577,7 @@ impl Nip34WritePolicy {
&all_events,
&event.id,
self.ctx.config.archive_retention_secs,
archive_path,
)
.await
{
+119 -2
View File
@@ -3,11 +3,13 @@
/// Handles querying for dependent events and moving events between databases
/// for NIP-09 deletion request processing.
use std::collections::HashSet;
use std::path::Path;
use nostr_relay_builder::prelude::{Event, EventId, Filter, Kind};
use nostr_relay_builder::prelude::{Event, EventId, Filter, Kind, PublicKey, ToBech32};
use super::SharedDatabase;
use crate::database::{DeletionMetadata, HoldingDatabase};
use crate::git::archive::{archive_repository, create_archive_metadata};
/// Query all events that depend on the given event IDs
///
@@ -197,6 +199,7 @@ fn has_event_tag(event: &Event, event_id: &EventId) -> bool {
/// * `event_ids` - Event IDs to move
/// * `deletion_event_id` - ID of the deletion request event
/// * `retention_secs` - Retention period in seconds before permanent deletion
/// * `archive_path` - Optional path to git archive (if repository was archived)
///
/// # Returns
/// Number of events successfully moved
@@ -206,6 +209,7 @@ pub async fn move_to_holding_database(
event_ids: &HashSet<EventId>,
deletion_event_id: &EventId,
retention_secs: u64,
archive_path: Option<String>,
) -> Result<usize, String> {
let mut moved_count = 0;
let deletion_ts = std::time::SystemTime::now()
@@ -238,7 +242,7 @@ pub async fn move_to_holding_database(
deletion_timestamp: deletion_ts,
deletion_event_id: *deletion_event_id,
expiry_timestamp: deletion_ts + retention_secs,
archive_path: None, // TODO: Add archive path when git archival is implemented
archive_path: archive_path.clone(),
};
// Store in holding database with deletion metadata
@@ -262,6 +266,119 @@ pub async fn move_to_holding_database(
Ok(moved_count)
}
/// Archive git repositories for address deletions
///
/// When deleting repository announcements (kind 30617), this function archives
/// the git repository data before deletion.
///
/// # Arguments
/// * `addresses` - Repository addresses to archive (format: `30617:<pubkey>:<identifier>`)
/// * `git_data_path` - Base path for git repositories
/// * `archive_base_path` - Base path for archives (typically `.archive`)
/// * `deletion_event_id` - ID of the deletion request event
/// * `retention_secs` - Retention period in seconds
///
/// # Returns
/// Optional path to the archive file (if any repositories were archived)
pub fn archive_repositories_for_addresses(
addresses: &[String],
git_data_path: &Path,
archive_base_path: &Path,
deletion_event_id: &EventId,
retention_secs: u64,
) -> Result<Option<String>, String> {
let deletion_ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// Only archive kind 30617 repository announcements
for address in addresses {
let parts: Vec<&str> = address.split(':').collect();
if parts.len() != 3 {
continue; // Skip invalid addresses
}
let kind = parts[0];
if kind != "30617" {
continue; // Only archive repository announcements
}
let pubkey_hex = parts[1];
let identifier = parts[2];
// Parse pubkey to get npub
let pubkey = PublicKey::from_hex(pubkey_hex)
.map_err(|e| format!("Invalid pubkey in address: {}", e))?;
let npub = pubkey
.to_bech32()
.map_err(|e| format!("Failed to convert pubkey to npub: {}", e))?;
// Build repository path: <git_data_path>/<npub>/<identifier>.git
let repo_path = git_data_path
.join(&npub)
.join(format!("{}.git", identifier));
if !repo_path.exists() {
tracing::warn!(
npub = %npub,
identifier = %identifier,
path = %repo_path.display(),
"Repository directory not found, skipping archive"
);
continue;
}
// Archive the repository
match archive_repository(
&repo_path,
archive_base_path,
&npub,
identifier,
deletion_ts,
) {
Ok(archive_path) => {
tracing::info!(
npub = %npub,
identifier = %identifier,
archive_path = %archive_path.display(),
"Successfully archived git repository"
);
// Create archive metadata
if let Err(e) = create_archive_metadata(
&archive_path,
deletion_ts,
deletion_event_id.to_hex(),
npub.clone(),
identifier.to_string(),
retention_secs,
) {
tracing::error!(
error = %e,
archive_path = %archive_path.display(),
"Failed to create archive metadata"
);
}
// Return the archive path (relative to working directory)
return Ok(Some(archive_path.display().to_string()));
}
Err(e) => {
tracing::error!(
error = %e,
npub = %npub,
identifier = %identifier,
"Failed to archive git repository"
);
return Err(format!("Failed to archive repository: {}", e));
}
}
}
Ok(None) // No repositories archived
}
#[cfg(test)]
mod tests {
use super::*;
+3 -1
View File
@@ -16,7 +16,9 @@ mod state;
pub use announcement::{AnnouncementPolicy, AnnouncementResult};
pub use deletion::{DeletionPolicy, DeletionResult};
pub use deletion_ops::{move_to_holding_database, query_dependent_events};
pub use deletion_ops::{
archive_repositories_for_addresses, move_to_holding_database, query_dependent_events,
};
pub use pr_event::PrEventPolicy;
pub use related::{ReferenceResult, RelatedEventPolicy};
pub use state::{StatePolicy, StateResult};