From 0b805089c0f6be655eaf3529053e99e35f9febda Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 14 Jan 2026 12:13:52 +0000 Subject: [PATCH] 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//-.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) --- src/database/cleanup.rs | 2 +- src/nostr/builder.rs | 39 ++++++++++ src/nostr/policy/deletion_ops.rs | 121 ++++++++++++++++++++++++++++++- src/nostr/policy/mod.rs | 4 +- 4 files changed, 162 insertions(+), 4 deletions(-) diff --git a/src/database/cleanup.rs b/src/database/cleanup.rs index d828d4b..5ede5e6 100644 --- a/src/database/cleanup.rs +++ b/src/database/cleanup.rs @@ -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 diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index b9af38e..4fdf5a5 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs @@ -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 { diff --git a/src/nostr/policy/deletion_ops.rs b/src/nostr/policy/deletion_ops.rs index 3b864d3..2e7a2a7 100644 --- a/src/nostr/policy/deletion_ops.rs +++ b/src/nostr/policy/deletion_ops.rs @@ -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, deletion_event_id: &EventId, retention_secs: u64, + archive_path: Option, ) -> Result { 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::`) +/// * `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, 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 + 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::*; diff --git a/src/nostr/policy/mod.rs b/src/nostr/policy/mod.rs index f16f52e..18f7980 100644 --- a/src/nostr/policy/mod.rs +++ b/src/nostr/policy/mod.rs @@ -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};