feat(torrents): harden lifecycle and web-seed management

- Enforce generation-safe seed admission and budget tracking.
- Make web-seed RPC, persistence, rollback, and startup attachment lifecycle-safe.
- Keep Torrent progress, DHT, seed-capacity, and web-seed validation covered.
- Ignore local TORRENT_FEATURES.md roadmap notes.
This commit is contained in:
NimBold
2026-08-03 16:56:01 +03:30
parent 79c0e48c43
commit c4d3a2be51
31 changed files with 3398 additions and 258 deletions
+113 -1
View File
@@ -5,6 +5,9 @@ pub const PORTABLE_MARKER: &str = "portable.flag";
const PORTABLE_DATA_DIR: &str = "data";
const PORTABLE_LOG_DIR: &str = "logs";
const PORTABLE_WEBVIEW_DIR: &str = "webview";
const ARIA2_DATA_DIR: &str = "aria2";
const ARIA2_DHT_FILE: &str = "dht.dat";
const ARIA2_DHT6_FILE: &str = "dht6.dat";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StorageMode {
@@ -104,6 +107,59 @@ impl StorageLayout {
pub fn webview_dir(&self) -> &Path {
&self.webview_dir
}
pub fn aria2_dht_paths(&self) -> (PathBuf, PathBuf) {
let directory = self.data_dir.join(ARIA2_DATA_DIR);
(
directory.join(ARIA2_DHT_FILE),
directory.join(ARIA2_DHT6_FILE),
)
}
/// Create and validate only Firelink's Aria2 state directory. Aria2 owns
/// the table contents; Firelink owns this exact location and must never
/// fall back to a user-global default when it cannot establish it.
pub fn prepare_aria2_dht_paths(&self) -> Result<(PathBuf, PathBuf), String> {
let directory = self.data_dir.join(ARIA2_DATA_DIR);
if crate::path_has_symlink_component(&directory) {
return Err(format!(
"Aria2 state directory contains a symlink: '{}'",
directory.display()
));
}
match std::fs::symlink_metadata(&directory) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(format!(
"Aria2 state directory is a symlink: '{}'",
directory.display()
));
}
Ok(metadata) if !metadata.is_dir() => {
return Err(format!(
"Aria2 state path is not a directory: '{}'",
directory.display()
));
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
std::fs::create_dir(&directory).map_err(|error| {
format!(
"failed to create Aria2 state directory '{}': {error}",
directory.display()
)
})?;
}
Err(error) => {
return Err(format!(
"failed to inspect Aria2 state directory '{}': {error}",
directory.display()
));
}
}
Ok(self.aria2_dht_paths())
}
}
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
@@ -154,7 +210,7 @@ fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
#[cfg(test)]
mod tests {
use super::{canonicalize_storage_path, StorageMode, PORTABLE_MARKER};
use super::{canonicalize_storage_path, StorageLayout, StorageMode, PORTABLE_MARKER};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
@@ -182,6 +238,62 @@ mod tests {
);
}
fn test_layout(data_dir: &Path) -> StorageLayout {
let data_dir = fs::canonicalize(data_dir).unwrap();
StorageLayout {
mode: StorageMode::Standard,
data_dir: data_dir.clone(),
log_dir: data_dir.join("logs"),
webview_dir: data_dir.join("webview"),
}
}
#[test]
fn aria2_dht_paths_are_owned_by_the_selected_data_directory() {
let root = TempDir::new().unwrap();
let layout = test_layout(root.path());
let root_path = fs::canonicalize(root.path()).unwrap();
assert_eq!(
layout.aria2_dht_paths(),
(
root_path.join("aria2/dht.dat"),
root_path.join("aria2/dht6.dat")
)
);
let prepared = layout.prepare_aria2_dht_paths().unwrap();
assert_eq!(prepared, layout.aria2_dht_paths());
assert!(root_path.join("aria2").is_dir());
}
#[test]
fn aria2_dht_preparation_rejects_a_file_at_the_directory_boundary() {
let root = TempDir::new().unwrap();
let root_path = fs::canonicalize(root.path()).unwrap();
fs::write(root_path.join("aria2"), b"not a directory").unwrap();
let error = test_layout(root.path())
.prepare_aria2_dht_paths()
.unwrap_err();
assert!(error.contains("not a directory"));
}
#[cfg(unix)]
#[test]
fn aria2_dht_preparation_rejects_a_symlinked_directory() {
use std::os::unix::fs::symlink;
let root = TempDir::new().unwrap();
let target = TempDir::new().unwrap();
let root_path = fs::canonicalize(root.path()).unwrap();
symlink(target.path(), root_path.join("aria2")).unwrap();
let error = test_layout(root.path())
.prepare_aria2_dht_paths()
.unwrap_err();
assert!(error.contains("symlink"));
}
#[cfg(unix)]
#[test]
fn rejects_symlinked_storage_directories() {