mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
feat(storage): clear inactive transfer cache
This commit is contained in:
@@ -91,6 +91,11 @@ or temporary tag. Receive downloads keep a temporary tag through export and beco
|
||||
reclaimable after publication. Restart reconciliation repairs active-share tags,
|
||||
removes orphan share tags, and never restores a stopped share.
|
||||
|
||||
The explicit transfer-cache action is available only when no transfer or share is
|
||||
active. It shuts the core down cleanly, removes the app-owned blob store, and then
|
||||
restarts the core with the same identity and network configuration. Deleting all
|
||||
transfer records invokes the same cleanup after live shares have been stopped.
|
||||
|
||||
## Resource Limits
|
||||
|
||||
`CoreLimits` controls source count, collection files and bytes, path and ticket
|
||||
|
||||
@@ -2,8 +2,9 @@ use anyhow::Context;
|
||||
use iroh::RelayUrl;
|
||||
use iroh_blobs::Hash;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeSet, net::IpAddr, str::FromStr};
|
||||
use std::{collections::BTreeSet, net::IpAddr, path::PathBuf, str::FromStr};
|
||||
|
||||
use crate::error::VnidropError;
|
||||
use crate::util::{non_empty, now_ms};
|
||||
|
||||
pub(crate) const MAX_CUSTOM_RELAYS: usize = 8;
|
||||
@@ -126,6 +127,49 @@ pub fn default_core_network_config() -> CoreNetworkConfig {
|
||||
CoreNetworkConfig::default()
|
||||
}
|
||||
|
||||
/// Removes the blob store after its owning core has shut down.
|
||||
///
|
||||
/// Callers must verify that no transfer or share is active before shutdown.
|
||||
#[uniffi::export]
|
||||
pub fn clear_inactive_transfer_cache(app_data_dir: String) -> Result<u64, VnidropError> {
|
||||
let result = (|| -> anyhow::Result<u64> {
|
||||
let app_data_dir = PathBuf::from(app_data_dir);
|
||||
anyhow::ensure!(
|
||||
app_data_dir.is_absolute(),
|
||||
"app data directory must be absolute"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
app_data_dir.file_name().is_some(),
|
||||
"app data directory must not be a filesystem root"
|
||||
);
|
||||
let blobs = app_data_dir.join("blobs");
|
||||
let metadata = match std::fs::symlink_metadata(&blobs) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
anyhow::ensure!(
|
||||
metadata.is_dir() && !metadata.file_type().is_symlink(),
|
||||
"blob store path is not a directory"
|
||||
);
|
||||
let bytes = walkdir::WalkDir::new(&blobs)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.try_fold(0u64, |total, entry| {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
Ok::<_, walkdir::Error>(if metadata.is_file() {
|
||||
total.saturating_add(metadata.len())
|
||||
} else {
|
||||
total
|
||||
})
|
||||
})?;
|
||||
std::fs::remove_dir_all(blobs)?;
|
||||
Ok(bytes)
|
||||
})();
|
||||
result.map_err(VnidropError::filesystem)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct CoreLimits {
|
||||
pub max_sources: u64,
|
||||
|
||||
@@ -14,11 +14,11 @@ mod transfer_state;
|
||||
mod util;
|
||||
|
||||
pub use api::{
|
||||
default_core_limits, default_core_network_config, CoreEvent, CoreEventSink, CoreLimits,
|
||||
CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, PublishedOutput, ReceiveOutputSink,
|
||||
ReceiveOutputSinkV2, ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus,
|
||||
ShareMetadataInput, ShareResult, ShareSource, SourceKind, StoredTransfer, TicketInspection,
|
||||
TransferAccessMode, TransferMetadata,
|
||||
clear_inactive_transfer_cache, default_core_limits, default_core_network_config, CoreEvent,
|
||||
CoreEventSink, CoreLimits, CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, PublishedOutput,
|
||||
ReceiveOutputSink, ReceiveOutputSinkV2, ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest,
|
||||
RuntimeStatus, ShareMetadataInput, ShareResult, ShareSource, SourceKind, StoredTransfer,
|
||||
TicketInspection, TransferAccessMode, TransferMetadata,
|
||||
};
|
||||
pub use error::VnidropError;
|
||||
pub use runtime::VnidropCore;
|
||||
|
||||
26
crates/vnidrop/tests/storage.rs
Normal file
26
crates/vnidrop/tests/storage.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use vnidrop::clear_inactive_transfer_cache;
|
||||
|
||||
#[test]
|
||||
fn inactive_transfer_cache_is_removed_and_reports_reclaimed_bytes() {
|
||||
let app_data = tempfile::tempdir().unwrap();
|
||||
let blobs = app_data.path().join("blobs");
|
||||
std::fs::create_dir_all(blobs.join("data")).unwrap();
|
||||
std::fs::write(blobs.join("data").join("payload"), vec![9u8; 4096]).unwrap();
|
||||
std::fs::write(blobs.join("blobs.db"), vec![3u8; 512]).unwrap();
|
||||
|
||||
let reclaimed =
|
||||
clear_inactive_transfer_cache(app_data.path().to_string_lossy().into_owned()).unwrap();
|
||||
|
||||
assert_eq!(reclaimed, 4608);
|
||||
assert!(!blobs.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_transfer_cache_rejects_relative_app_data_paths() {
|
||||
assert!(clear_inactive_transfer_cache("relative/path".to_string()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_transfer_cache_rejects_filesystem_roots() {
|
||||
assert!(clear_inactive_transfer_cache(std::path::MAIN_SEPARATOR_STR.to_string()).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user