mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
Compare commits
2 Commits
7cc0e825f6
...
0448137d84
| Author | SHA1 | Date | |
|---|---|---|---|
| 0448137d84 | |||
| 4074f4bee8 |
@@ -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;
|
||||
|
||||
@@ -16,6 +16,8 @@ mod share;
|
||||
mod storage;
|
||||
|
||||
pub use facade::VnidropCore;
|
||||
#[cfg(test)]
|
||||
pub(crate) use provider::{consume_request_updates, RequestStreamOutcome};
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
|
||||
@@ -10,6 +10,31 @@ use tokio::sync::mpsc;
|
||||
use super::CoreInner;
|
||||
use crate::access_policy::AccessDecision;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum RequestStreamOutcome {
|
||||
TerminalUpdateReceived,
|
||||
Aborted,
|
||||
}
|
||||
|
||||
pub(crate) async fn consume_request_updates(
|
||||
mut rx: irpc::channel::mpsc::Receiver<RequestUpdate>,
|
||||
mut handle_update: impl FnMut(RequestUpdate),
|
||||
) -> RequestStreamOutcome {
|
||||
let mut terminal_update_received = false;
|
||||
while let Ok(Some(update)) = rx.recv().await {
|
||||
terminal_update_received |= matches!(
|
||||
update,
|
||||
RequestUpdate::Completed(_) | RequestUpdate::Aborted(_)
|
||||
);
|
||||
handle_update(update);
|
||||
}
|
||||
if terminal_update_received {
|
||||
RequestStreamOutcome::TerminalUpdateReceived
|
||||
} else {
|
||||
RequestStreamOutcome::Aborted
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreInner {
|
||||
pub(super) async fn spawn_provider_event_task(
|
||||
self: &Arc<Self>,
|
||||
@@ -302,7 +327,7 @@ impl CoreInner {
|
||||
transfer_id: u64,
|
||||
connection_id: u64,
|
||||
request_id: u64,
|
||||
mut rx: irpc::channel::mpsc::Receiver<RequestUpdate>,
|
||||
rx: irpc::channel::mpsc::Receiver<RequestUpdate>,
|
||||
) {
|
||||
// Request update tasks are tied to individual provider streams. Router
|
||||
// shutdown closes those streams; only the long-lived provider receiver
|
||||
@@ -318,35 +343,35 @@ impl CoreInner {
|
||||
.cloned();
|
||||
let core = self.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(Some(update)) = rx.recv().await {
|
||||
match update {
|
||||
RequestUpdate::Started(started) => core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
"started",
|
||||
json!({
|
||||
"connection_id": connection_id,
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
"hash": started.hash.to_string(),
|
||||
"size": started.size,
|
||||
"index": started.index,
|
||||
}),
|
||||
),
|
||||
RequestUpdate::Progress(progress) => core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
"progress",
|
||||
json!({
|
||||
"connection_id": connection_id,
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
"end_offset": progress.end_offset,
|
||||
}),
|
||||
),
|
||||
RequestUpdate::Completed(_) => core.emit_transfer(
|
||||
let outcome = consume_request_updates(rx, |update| match update {
|
||||
RequestUpdate::Started(started) => core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
"started",
|
||||
json!({
|
||||
"connection_id": connection_id,
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
"hash": started.hash.to_string(),
|
||||
"size": started.size,
|
||||
"index": started.index,
|
||||
}),
|
||||
),
|
||||
RequestUpdate::Progress(progress) => core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
"progress",
|
||||
json!({
|
||||
"connection_id": connection_id,
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
"end_offset": progress.end_offset,
|
||||
}),
|
||||
),
|
||||
RequestUpdate::Completed(_) => {
|
||||
core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
@@ -356,8 +381,10 @@ impl CoreInner {
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
}),
|
||||
),
|
||||
RequestUpdate::Aborted(_) => core.emit_transfer(
|
||||
);
|
||||
}
|
||||
RequestUpdate::Aborted(_) => {
|
||||
core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
@@ -367,8 +394,22 @@ impl CoreInner {
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if outcome == RequestStreamOutcome::Aborted {
|
||||
core.emit_transfer(
|
||||
transfer_id,
|
||||
"send",
|
||||
"transfer",
|
||||
"aborted",
|
||||
json!({
|
||||
"connection_id": connection_id,
|
||||
"request_id": request_id,
|
||||
"endpoint_id": endpoint_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use iroh_blobs::Hash;
|
||||
use iroh_blobs::{
|
||||
provider::{
|
||||
events::{RequestUpdate, TransferCompleted},
|
||||
TransferStats,
|
||||
},
|
||||
Hash,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
repository::{PendingDeliveryReceiptInsert, Repository, TransferUpsert},
|
||||
runtime::{consume_request_updates, RequestStreamOutcome},
|
||||
transfer_state::{TransferDirection, TransferStatus},
|
||||
CoreEvent, CoreEventSink, VnidropCore, VnidropError,
|
||||
};
|
||||
@@ -14,6 +21,37 @@ impl CoreEventSink for TestSink {
|
||||
fn on_event(&self, _event: CoreEvent) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_request_stream_distinguishes_success_from_silent_abort() {
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
runtime.block_on(async {
|
||||
let (completed_tx, completed_rx) = irpc::channel::mpsc::channel(1);
|
||||
completed_tx
|
||||
.send(RequestUpdate::Completed(TransferCompleted {
|
||||
stats: Box::new(TransferStats {
|
||||
payload_bytes_sent: 5,
|
||||
other_bytes_sent: 0,
|
||||
other_bytes_read: 0,
|
||||
duration: Duration::ZERO,
|
||||
}),
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
drop(completed_tx);
|
||||
assert_eq!(
|
||||
consume_request_updates(completed_rx, |_| {}).await,
|
||||
RequestStreamOutcome::TerminalUpdateReceived
|
||||
);
|
||||
|
||||
let (aborted_tx, aborted_rx) = irpc::channel::mpsc::channel::<RequestUpdate>(1);
|
||||
drop(aborted_tx);
|
||||
assert_eq!(
|
||||
consume_request_updates(aborted_rx, |_| {}).await,
|
||||
RequestStreamOutcome::Aborted
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initializes_and_reports_endpoint() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -1,7 +1,28 @@
|
||||
mod support;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use support::{receive_with_response, share_path, TestNode};
|
||||
use vnidrop::VnidropError;
|
||||
use vnidrop::{CoreEvent, VnidropError};
|
||||
|
||||
fn wait_for_sender_transfer_event(sender: &TestNode, transfer_id: u64, kind: &str) -> CoreEvent {
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
if let Some(event) = sender.sink.events().into_iter().find(|event| {
|
||||
event.transfer_id == Some(transfer_id)
|
||||
&& event.direction.as_deref() == Some("send")
|
||||
&& event.phase == "transfer"
|
||||
&& event.kind == kind
|
||||
}) {
|
||||
return event;
|
||||
}
|
||||
assert!(
|
||||
started.elapsed() < Duration::from_secs(5),
|
||||
"timed out waiting for sender transfer event {kind}"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfers_file_between_two_cores() {
|
||||
@@ -50,6 +71,12 @@ fn transfers_file_between_two_cores() {
|
||||
artifacts[0].locator,
|
||||
output_dir.path().join("hello.txt").to_string_lossy()
|
||||
);
|
||||
let completed = wait_for_sender_transfer_event(&sender, share.transfer_id, "completed");
|
||||
assert!(completed.data_json.contains("\"connection_id\":"));
|
||||
assert!(completed.data_json.contains("\"request_id\":"));
|
||||
assert!(completed
|
||||
.data_json
|
||||
.contains(receiver.core.status().endpoint_id.as_str()));
|
||||
|
||||
receiver.core.delete_receive_history().unwrap();
|
||||
assert_eq!(receiver.core.list_received_artifacts().unwrap(), artifacts);
|
||||
|
||||
@@ -103,7 +103,7 @@ private class AndroidFileSystemService(
|
||||
return ReceivedStorageInspection(bytes, existing, missing, inaccessible)
|
||||
}
|
||||
|
||||
override suspend fun temporaryUsage(): ULong = directorySize(context.cacheDir)
|
||||
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = directorySize(context.cacheDir)
|
||||
|
||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? =
|
||||
when (folder.kind) {
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Wird empfangen</string>
|
||||
<string name="status_stopped">Beendet</string>
|
||||
<string name="storage_calculating">Wird berechnet…</string>
|
||||
<string name="storage_clear_transfer_cache">Übertragungscache leeren</string>
|
||||
<string name="storage_clear_transfer_cache_description">Entfernt zwischengespeicherte Übertragungsinhalte, die nicht von einem laufenden Empfang oder einer aktiven Freigabe verwendet werden. Empfangene Dateien und der Übertragungsverlauf werden nicht gelöscht.</string>
|
||||
<string name="storage_clearing_transfer_cache">Cache wird geleert…</string>
|
||||
<string name="storage_delete_transfers">Alle Übertragungen löschen</string>
|
||||
<string name="storage_delete_transfers_description">Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="storage_delete_transfers_description">Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht und nicht benötigter Übertragungscache sofort freigegeben. Laufende Übertragungen und empfangene Dateien werden nicht gelöscht. Dies kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="storage_app_data">App-Daten</string>
|
||||
<string name="storage_deleting">Wird gelöscht…</string>
|
||||
<string name="storage_footer">Übertragungsdaten umfassen Ihren Verlauf und zwischengespeicherte Inhalte aktiver Freigaben. Nicht mehr benötigter Cache wird nach dem Löschen der Datensätze automatisch bereinigt; dies kann etwas dauern. Empfangene Dateien werden für diese Übersicht erfasst, aber hier niemals gelöscht.</string>
|
||||
<string name="storage_footer">Der Übertragungscache enthält heruntergeladene oder importierte Inhalte, einschließlich Daten, die durch laufende Empfänge und aktive Freigaben geschützt sind. Beim Leeren wird nur nicht benötigter Cache entfernt. Empfangene Dateien werden hier niemals gelöscht.</string>
|
||||
<string name="storage_received_files">Empfangene Dateien</string>
|
||||
<string name="storage_temporary">Temporäre Dateien</string>
|
||||
<string name="storage_title">Speicher</string>
|
||||
<string name="storage_total">Gesamt</string>
|
||||
<string name="storage_transfer_data">Übertragungsdaten</string>
|
||||
<string name="storage_transfer_data">Übertragungscache</string>
|
||||
<string name="storage_transfer_cache_cleared">Übertragungscache geleert</string>
|
||||
<string name="storage_transfers_deleted">Alle Übertragungen gelöscht</string>
|
||||
<string name="transfer_activity_description">Wichtige Aktualisierungen zu dieser Übertragung ansehen</string>
|
||||
<string name="transfer_activity_title">Aktivität</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Recibiendo</string>
|
||||
<string name="status_stopped">Detenido</string>
|
||||
<string name="storage_calculating">Calculando…</string>
|
||||
<string name="storage_clear_transfer_cache">Borrar caché de transferencias</string>
|
||||
<string name="storage_clear_transfer_cache_description">Elimina el contenido de transferencia en caché que no esté siendo utilizado por una recepción en curso o un recurso compartido activo. Los archivos recibidos y el historial no se eliminan.</string>
|
||||
<string name="storage_clearing_transfer_cache">Borrando caché…</string>
|
||||
<string name="storage_delete_transfers">Eliminar todas las transferencias</string>
|
||||
<string name="storage_delete_transfers_description">Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer.</string>
|
||||
<string name="storage_delete_transfers_description">Esto borra del historial todos los registros de transferencias enviadas y recibidas y libera inmediatamente la caché de transferencia no utilizada. Las transferencias en curso y los archivos recibidos no se eliminan. Esto no se puede deshacer.</string>
|
||||
<string name="storage_app_data">Datos de la aplicación</string>
|
||||
<string name="storage_deleting">Eliminando…</string>
|
||||
<string name="storage_footer">Los datos de transferencia incluyen su historial y el contenido en caché de los recursos compartidos activos. La caché innecesaria se recupera automáticamente tras eliminar los registros, lo que puede tardar un poco. Los archivos recibidos se registran para este resumen, pero nunca se eliminan aquí.</string>
|
||||
<string name="storage_footer">La caché de transferencias incluye contenido descargado o importado, incluidos los datos protegidos por recepciones en curso y recursos compartidos activos. Al borrarla solo se elimina la caché no utilizada. Los archivos recibidos nunca se eliminan aquí.</string>
|
||||
<string name="storage_received_files">Archivos recibidos</string>
|
||||
<string name="storage_temporary">Archivos temporales</string>
|
||||
<string name="storage_title">Almacenamiento</string>
|
||||
<string name="storage_total">Total</string>
|
||||
<string name="storage_transfer_data">Datos de transferencia</string>
|
||||
<string name="storage_transfer_data">Caché de transferencias</string>
|
||||
<string name="storage_transfer_cache_cleared">Caché de transferencias borrada</string>
|
||||
<string name="storage_transfers_deleted">Todas las transferencias eliminadas</string>
|
||||
<string name="transfer_activity_description">Vea las actualizaciones importantes de esta transferencia</string>
|
||||
<string name="transfer_activity_title">Actividad</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Réception</string>
|
||||
<string name="status_stopped">Arrêté</string>
|
||||
<string name="storage_calculating">Calcul…</string>
|
||||
<string name="storage_clear_transfer_cache">Vider le cache des transferts</string>
|
||||
<string name="storage_clear_transfer_cache_description">Supprime le contenu de transfert en cache qui n’est pas utilisé par une réception en cours ou un partage actif. Les fichiers reçus et l’historique ne sont pas supprimés.</string>
|
||||
<string name="storage_clearing_transfer_cache">Vidage du cache…</string>
|
||||
<string name="storage_delete_transfers">Supprimer tous les transferts</string>
|
||||
<string name="storage_delete_transfers_description">Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible.</string>
|
||||
<string name="storage_delete_transfers_description">Cela efface tous les transferts envoyés et reçus de l’historique et libère immédiatement le cache inutilisé. Les transferts en cours et les fichiers reçus ne sont pas supprimés. Cette action est irréversible.</string>
|
||||
<string name="storage_app_data">Données de l’app</string>
|
||||
<string name="storage_deleting">Suppression…</string>
|
||||
<string name="storage_footer">Les données de transfert comprennent votre historique et le contenu mis en cache pour les partages actifs. Le cache inutile est récupéré automatiquement après la suppression des enregistrements, ce qui peut prendre un peu de temps. Les fichiers reçus sont suivis pour ce récapitulatif, mais ne sont jamais supprimés ici.</string>
|
||||
<string name="storage_footer">Le cache des transferts comprend le contenu téléchargé ou importé, y compris les données protégées par les réceptions en cours et les partages actifs. Le vider ne supprime que le cache inutilisé. Les fichiers reçus ne sont jamais supprimés ici.</string>
|
||||
<string name="storage_received_files">Fichiers reçus</string>
|
||||
<string name="storage_temporary">Fichiers temporaires</string>
|
||||
<string name="storage_title">Stockage</string>
|
||||
<string name="storage_total">Total</string>
|
||||
<string name="storage_transfer_data">Données de transfert</string>
|
||||
<string name="storage_transfer_data">Cache des transferts</string>
|
||||
<string name="storage_transfer_cache_cleared">Cache des transferts vidé</string>
|
||||
<string name="storage_transfers_deleted">Tous les transferts supprimés</string>
|
||||
<string name="transfer_activity_description">Consultez les mises à jour importantes de ce transfert</string>
|
||||
<string name="transfer_activity_title">Activité</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Ricezione</string>
|
||||
<string name="status_stopped">Interrotto</string>
|
||||
<string name="storage_calculating">Calcolo…</string>
|
||||
<string name="storage_clear_transfer_cache">Svuota cache trasferimenti</string>
|
||||
<string name="storage_clear_transfer_cache_description">Rimuove il contenuto dei trasferimenti memorizzato nella cache che non è usato da una ricezione in corso o da una condivisione attiva. I file ricevuti e la cronologia non vengono eliminati.</string>
|
||||
<string name="storage_clearing_transfer_cache">Svuotamento cache…</string>
|
||||
<string name="storage_delete_transfers">Elimina tutti i trasferimenti</string>
|
||||
<string name="storage_delete_transfers_description">Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata.</string>
|
||||
<string name="storage_delete_transfers_description">Elimina dalla cronologia tutti i trasferimenti inviati e ricevuti e libera immediatamente la cache inutilizzata. I trasferimenti in corso e i file ricevuti non vengono eliminati. Questa azione non può essere annullata.</string>
|
||||
<string name="storage_app_data">Dati dell’app</string>
|
||||
<string name="storage_deleting">Eliminazione…</string>
|
||||
<string name="storage_footer">I dati di trasferimento includono la cronologia e il contenuto nella cache per le condivisioni attive. La cache non necessaria viene recuperata automaticamente dopo la rimozione dei record, operazione che può richiedere un po’ di tempo. I file ricevuti vengono monitorati per questo riepilogo, ma non sono mai eliminati qui.</string>
|
||||
<string name="storage_footer">La cache dei trasferimenti include contenuti scaricati o importati, compresi i dati protetti da ricezioni in corso e condivisioni attive. Lo svuotamento rimuove solo la cache inutilizzata. I file ricevuti non vengono mai eliminati qui.</string>
|
||||
<string name="storage_received_files">File ricevuti</string>
|
||||
<string name="storage_temporary">File temporanei</string>
|
||||
<string name="storage_title">Archiviazione</string>
|
||||
<string name="storage_total">Totale</string>
|
||||
<string name="storage_transfer_data">Dati di trasferimento</string>
|
||||
<string name="storage_transfer_data">Cache trasferimenti</string>
|
||||
<string name="storage_transfer_cache_cleared">Cache trasferimenti svuotata</string>
|
||||
<string name="storage_transfers_deleted">Tutti i trasferimenti eliminati</string>
|
||||
<string name="transfer_activity_description">Veda gli aggiornamenti importanti di questo trasferimento</string>
|
||||
<string name="transfer_activity_title">Attività</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Ontvangen</string>
|
||||
<string name="status_stopped">Gestopt</string>
|
||||
<string name="storage_calculating">Berekenen…</string>
|
||||
<string name="storage_clear_transfer_cache">Overdrachtscache wissen</string>
|
||||
<string name="storage_clear_transfer_cache_description">Verwijdert overdrachtsinhoud uit de cache die niet wordt gebruikt door een lopende ontvangst of actieve share. Ontvangen bestanden en de overdrachtsgeschiedenis worden niet verwijderd.</string>
|
||||
<string name="storage_clearing_transfer_cache">Cache wissen…</string>
|
||||
<string name="storage_delete_transfers">Alle overdrachten verwijderen</string>
|
||||
<string name="storage_delete_transfers_description">Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt.</string>
|
||||
<string name="storage_delete_transfers_description">Hiermee worden alle verzonden en ontvangen overdrachten uit de geschiedenis gewist en wordt ongebruikte overdrachtscache direct vrijgemaakt. Lopende overdrachten en ontvangen bestanden worden niet verwijderd. Dit kan niet ongedaan worden gemaakt.</string>
|
||||
<string name="storage_app_data">Appgegevens</string>
|
||||
<string name="storage_deleting">Verwijderen…</string>
|
||||
<string name="storage_footer">Overdrachtsgegevens omvatten uw geschiedenis en inhoud in de cache voor actieve shares. Onnodige cache wordt automatisch opgeruimd nadat overdrachtsrecords zijn verwijderd; dit kan enige tijd duren. Ontvangen bestanden worden voor dit overzicht bijgehouden, maar hier nooit verwijderd.</string>
|
||||
<string name="storage_footer">De overdrachtscache bevat gedownloade of geïmporteerde inhoud, inclusief gegevens die door lopende ontvangsten en actieve shares worden beschermd. Wissen verwijdert alleen ongebruikte cache. Ontvangen bestanden worden hier nooit verwijderd.</string>
|
||||
<string name="storage_received_files">Ontvangen bestanden</string>
|
||||
<string name="storage_temporary">Tijdelijke bestanden</string>
|
||||
<string name="storage_title">Opslag</string>
|
||||
<string name="storage_total">Totaal</string>
|
||||
<string name="storage_transfer_data">Overdrachtsgegevens</string>
|
||||
<string name="storage_transfer_data">Overdrachtscache</string>
|
||||
<string name="storage_transfer_cache_cleared">Overdrachtscache gewist</string>
|
||||
<string name="storage_transfers_deleted">Alle overdrachten verwijderd</string>
|
||||
<string name="transfer_activity_description">Bekijk belangrijke updates voor deze overdracht</string>
|
||||
<string name="transfer_activity_title">Activiteit</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Odbieranie</string>
|
||||
<string name="status_stopped">Zatrzymany</string>
|
||||
<string name="storage_calculating">Obliczanie…</string>
|
||||
<string name="storage_clear_transfer_cache">Wyczyść pamięć podręczną transferów</string>
|
||||
<string name="storage_clear_transfer_cache_description">Usuwa zawartość transferów z pamięci podręcznej, która nie jest używana przez trwające odbieranie ani aktywne udostępnianie. Odebrane pliki i historia nie są usuwane.</string>
|
||||
<string name="storage_clearing_transfer_cache">Czyszczenie pamięci podręcznej…</string>
|
||||
<string name="storage_delete_transfers">Usuń wszystkie transfery</string>
|
||||
<string name="storage_delete_transfers_description">Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć.</string>
|
||||
<string name="storage_delete_transfers_description">Usuwa z historii wszystkie wysłane i odebrane transfery oraz natychmiast zwalnia nieużywaną pamięć podręczną. Trwające transfery i odebrane pliki nie są usuwane. Tej operacji nie można cofnąć.</string>
|
||||
<string name="storage_app_data">Dane aplikacji</string>
|
||||
<string name="storage_deleting">Usuwanie…</string>
|
||||
<string name="storage_footer">Dane transferu obejmują historię oraz zawartość w pamięci podręcznej dla aktywnych udostępnień. Niepotrzebna pamięć podręczna jest odzyskiwana automatycznie po usunięciu rekordów, co może chwilę potrwać. Odebrane pliki są śledzone na potrzeby tego podsumowania, ale nigdy nie są tu usuwane.</string>
|
||||
<string name="storage_footer">Pamięć podręczna transferów zawiera pobraną lub zaimportowaną zawartość, w tym dane chronione przez trwające odbieranie i aktywne udostępnianie. Czyszczenie usuwa tylko nieużywaną pamięć podręczną. Odebrane pliki nigdy nie są tutaj usuwane.</string>
|
||||
<string name="storage_received_files">Odebrane pliki</string>
|
||||
<string name="storage_temporary">Pliki tymczasowe</string>
|
||||
<string name="storage_title">Pamięć</string>
|
||||
<string name="storage_total">Łącznie</string>
|
||||
<string name="storage_transfer_data">Dane transferu</string>
|
||||
<string name="storage_transfer_data">Pamięć podręczna transferów</string>
|
||||
<string name="storage_transfer_cache_cleared">Wyczyszczono pamięć podręczną transferów</string>
|
||||
<string name="storage_transfers_deleted">Usunięto wszystkie transfery</string>
|
||||
<string name="transfer_activity_description">Zobacz ważne aktualizacje tego transferu</string>
|
||||
<string name="transfer_activity_title">Aktywność</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">A receber</string>
|
||||
<string name="status_stopped">Parada</string>
|
||||
<string name="storage_calculating">A calcular…</string>
|
||||
<string name="storage_clear_transfer_cache">Limpar cache de transferências</string>
|
||||
<string name="storage_clear_transfer_cache_description">Remove conteúdo de transferência em cache que não esteja a ser utilizado por uma receção em curso ou partilha ativa. Os ficheiros recebidos e o histórico não são eliminados.</string>
|
||||
<string name="storage_clearing_transfer_cache">A limpar cache…</string>
|
||||
<string name="storage_delete_transfers">Eliminar todas as transferências</string>
|
||||
<string name="storage_delete_transfers_description">Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada.</string>
|
||||
<string name="storage_delete_transfers_description">Isto elimina do histórico todas as transferências enviadas e recebidas e liberta imediatamente a cache não utilizada. As transferências em curso e os ficheiros recebidos não são eliminados. Esta ação não pode ser anulada.</string>
|
||||
<string name="storage_app_data">Dados da aplicação</string>
|
||||
<string name="storage_deleting">A eliminar…</string>
|
||||
<string name="storage_footer">Os dados de transferência incluem o histórico e o conteúdo em cache das partilhas ativas. A cache desnecessária é recuperada automaticamente após a remoção dos registos, o que pode demorar algum tempo. Os ficheiros recebidos são acompanhados para este resumo, mas nunca são eliminados aqui.</string>
|
||||
<string name="storage_footer">A cache de transferências inclui conteúdo descarregado ou importado, incluindo dados protegidos por receções em curso e partilhas ativas. A limpeza remove apenas a cache não utilizada. Os ficheiros recebidos nunca são eliminados aqui.</string>
|
||||
<string name="storage_received_files">Ficheiros recebidos</string>
|
||||
<string name="storage_temporary">Ficheiros temporários</string>
|
||||
<string name="storage_title">Armazenamento</string>
|
||||
<string name="storage_total">Total</string>
|
||||
<string name="storage_transfer_data">Dados de transferência</string>
|
||||
<string name="storage_transfer_data">Cache de transferências</string>
|
||||
<string name="storage_transfer_cache_cleared">Cache de transferências limpa</string>
|
||||
<string name="storage_transfers_deleted">Todas as transferências eliminadas</string>
|
||||
<string name="transfer_activity_description">Ver as atualizações importantes desta transferência</string>
|
||||
<string name="transfer_activity_title">Atividade</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Получение</string>
|
||||
<string name="status_stopped">Остановлено</string>
|
||||
<string name="storage_calculating">Вычисление…</string>
|
||||
<string name="storage_clear_transfer_cache">Очистить кэш передач</string>
|
||||
<string name="storage_clear_transfer_cache_description">Удаляет кэшированное содержимое передач, которое не используется текущим приёмом или активной раздачей. Полученные файлы и история передач не удаляются.</string>
|
||||
<string name="storage_clearing_transfer_cache">Очистка кэша…</string>
|
||||
<string name="storage_delete_transfers">Удалить все передачи</string>
|
||||
<string name="storage_delete_transfers_description">Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить.</string>
|
||||
<string name="storage_delete_transfers_description">Это удалит из истории все отправленные и полученные передачи и немедленно освободит неиспользуемый кэш. Текущие передачи и полученные файлы не удаляются. Это действие нельзя отменить.</string>
|
||||
<string name="storage_app_data">Данные приложения</string>
|
||||
<string name="storage_deleting">Удаление…</string>
|
||||
<string name="storage_footer">Данные передачи включают историю и кэшированное содержимое активных раздач. Ненужный кэш освобождается автоматически после удаления записей; это может занять некоторое время. Полученные файлы учитываются в этой сводке, но никогда не удаляются здесь.</string>
|
||||
<string name="storage_footer">Кэш передач содержит загруженные или импортированные данные, включая содержимое, защищённое текущими приёмами и активными раздачами. Очистка удаляет только неиспользуемый кэш. Полученные файлы здесь никогда не удаляются.</string>
|
||||
<string name="storage_received_files">Полученные файлы</string>
|
||||
<string name="storage_temporary">Временные файлы</string>
|
||||
<string name="storage_title">Хранилище</string>
|
||||
<string name="storage_total">Всего</string>
|
||||
<string name="storage_transfer_data">Данные передачи</string>
|
||||
<string name="storage_transfer_data">Кэш передач</string>
|
||||
<string name="storage_transfer_cache_cleared">Кэш передач очищен</string>
|
||||
<string name="storage_transfers_deleted">Все передачи удалены</string>
|
||||
<string name="transfer_activity_description">Просматривайте важные обновления этой передачи</string>
|
||||
<string name="transfer_activity_title">Активность</string>
|
||||
|
||||
@@ -231,16 +231,20 @@
|
||||
<string name="status_receiving">Receiving</string>
|
||||
<string name="status_stopped">Stopped</string>
|
||||
<string name="storage_calculating">Calculating…</string>
|
||||
<string name="storage_clear_transfer_cache">Clear transfer cache</string>
|
||||
<string name="storage_clear_transfer_cache_description">Removes cached transfer content after briefly restarting VniDrop. Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted.</string>
|
||||
<string name="storage_clearing_transfer_cache">Clearing cache…</string>
|
||||
<string name="storage_delete_transfers">Delete all transfers</string>
|
||||
<string name="storage_delete_transfers_description">This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone.</string>
|
||||
<string name="storage_delete_transfers_description">This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. Ongoing transfers and received files are not deleted. This can’t be undone.</string>
|
||||
<string name="storage_app_data">App data</string>
|
||||
<string name="storage_deleting">Deleting…</string>
|
||||
<string name="storage_footer">Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here.</string>
|
||||
<string name="storage_footer">Transfer cache includes downloaded or imported content used by transfers and shares. It can be cleared when no transfer or share is active. Received files are tracked for this summary but are never deleted here.</string>
|
||||
<string name="storage_received_files">Received files</string>
|
||||
<string name="storage_temporary">Temporary files</string>
|
||||
<string name="storage_title">Storage</string>
|
||||
<string name="storage_total">Total</string>
|
||||
<string name="storage_transfer_data">Transfer data</string>
|
||||
<string name="storage_transfer_data">Transfer cache</string>
|
||||
<string name="storage_transfer_cache_cleared">Transfer cache cleared</string>
|
||||
<string name="storage_transfers_deleted">All transfers deleted</string>
|
||||
<string name="transfer_activity_description">See important updates for this transfer</string>
|
||||
<string name="transfer_activity_title">Activity</string>
|
||||
|
||||
@@ -182,6 +182,7 @@ interface CoreGateway {
|
||||
suspend fun receiveWithOutputSink(ticket: String, outputSink: ReceiveOutputSink, receiverName: String): Result<Unit>
|
||||
suspend fun receiveWithOutputSinkV2(ticket: String, outputSink: ReceiveOutputSinkV2, receiverName: String): Result<Unit>
|
||||
suspend fun storageUsage(): Result<CoreStorageUsageModel>
|
||||
suspend fun clearTransferCache(): Result<ULong>
|
||||
suspend fun receivedArtifacts(): Result<List<ReceivedArtifactModel>>
|
||||
suspend fun cancel(transferId: ULong): Result<Unit>
|
||||
suspend fun delete(transferId: ULong): Result<Unit>
|
||||
|
||||
@@ -30,6 +30,7 @@ import uniffi.vnidrop.TicketInspection
|
||||
import uniffi.vnidrop.TransferMetadata
|
||||
import uniffi.vnidrop.TransferAccessMode
|
||||
import uniffi.vnidrop.VnidropCore
|
||||
import uniffi.vnidrop.clearInactiveTransferCache
|
||||
import uniffi.vnidrop.defaultCoreNetworkConfig
|
||||
|
||||
class CoreRepository(
|
||||
@@ -45,6 +46,8 @@ class CoreRepository(
|
||||
override val signals: SharedFlow<CoreSignal> = _signals.asSharedFlow()
|
||||
|
||||
private var core: VnidropCore? = null
|
||||
private var currentAppDataDir: String? = null
|
||||
private var currentRelaySettings = RelaySettings()
|
||||
private val lifecycleGate = CoreLifecycleGate()
|
||||
|
||||
private val sink = object : CoreEventSink {
|
||||
@@ -77,6 +80,8 @@ class CoreRepository(
|
||||
previousCore?.shutdown()
|
||||
core = null
|
||||
core = VnidropCore.initializeWithNetworkConfig(appDataDir, sink, relaySettings.toNative())
|
||||
currentAppDataDir = appDataDir
|
||||
currentRelaySettings = relaySettings
|
||||
refreshSnapshot(requireCore())
|
||||
_state.update { it.copy(isInitialized = true) }
|
||||
}
|
||||
@@ -84,6 +89,7 @@ class CoreRepository(
|
||||
override fun shutdown() {
|
||||
core?.shutdown()
|
||||
core = null
|
||||
currentAppDataDir = null
|
||||
_state.value = CoreState()
|
||||
}
|
||||
|
||||
@@ -168,6 +174,28 @@ class CoreRepository(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearTransferCache(): Result<ULong> = runReconfiguration {
|
||||
val activeCore = requireCore()
|
||||
val status = activeCore.status()
|
||||
require(status.activeTransfers == 0UL && status.activeShares == 0UL) {
|
||||
"Wait for active transfers and shares to finish before clearing the transfer cache"
|
||||
}
|
||||
val appDataDir = requireNotNull(currentAppDataDir) { "Core data directory is unavailable" }
|
||||
val relaySettings = currentRelaySettings
|
||||
_state.update { it.copy(isInitialized = false, status = null) }
|
||||
activeCore.shutdown()
|
||||
core = null
|
||||
var reclaimed = 0UL
|
||||
try {
|
||||
reclaimed = clearInactiveTransferCache(appDataDir)
|
||||
} finally {
|
||||
core = VnidropCore.initializeWithNetworkConfig(appDataDir, sink, relaySettings.toNative())
|
||||
refreshSnapshot(requireCore())
|
||||
_state.update { it.copy(isInitialized = true) }
|
||||
}
|
||||
reclaimed
|
||||
}
|
||||
|
||||
override suspend fun receivedArtifacts(): Result<List<ReceivedArtifactModel>> = runCore { activeCore ->
|
||||
activeCore.listReceivedArtifacts().map { artifact ->
|
||||
ReceivedArtifactModel(
|
||||
|
||||
@@ -40,7 +40,7 @@ interface FileSystemService {
|
||||
if (supportsCustomReceiveFolders) configuredFolder else defaultReceiveFolder()
|
||||
suspend fun validateReceiveFolder(folder: ReceiveFolder): FolderAccessStatus
|
||||
suspend fun inspectReceivedArtifacts(artifacts: List<ReceivedArtifactModel>): ReceivedStorageInspection
|
||||
suspend fun temporaryUsage(): ULong
|
||||
suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong
|
||||
fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2?
|
||||
fun canRevealReceiveFolder(folder: ReceiveFolder): Boolean = false
|
||||
suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> =
|
||||
|
||||
@@ -41,5 +41,6 @@ fun SettingsRoute(viewModel: SettingsViewModel, windowClass: WindowClass) {
|
||||
onBugIncludeLogsChanged = viewModel::setBugIncludeLogs,
|
||||
onSubmitBugReport = viewModel::submitBugReport,
|
||||
onDeleteAllTransfers = viewModel::deleteAllTransfers,
|
||||
onClearTransferCache = viewModel::clearTransferCache,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ fun SettingsScreen(
|
||||
onBugIncludeLogsChanged: (Boolean) -> Unit,
|
||||
onSubmitBugReport: () -> Unit,
|
||||
onDeleteAllTransfers: () -> Unit = {},
|
||||
onClearTransferCache: () -> Unit = {},
|
||||
onRelayModeChanged: (RelayMode) -> Unit = {},
|
||||
onRelayUrlChanged: (Int, String) -> Unit = { _, _ -> },
|
||||
onAddRelayUrl: () -> Unit = {},
|
||||
@@ -67,6 +68,7 @@ fun SettingsScreen(
|
||||
onBugIncludeLogsChanged = onBugIncludeLogsChanged,
|
||||
onSubmitBugReport = onSubmitBugReport,
|
||||
onDeleteAllTransfers = onDeleteAllTransfers,
|
||||
onClearTransferCache = onClearTransferCache,
|
||||
onRelayModeChanged = onRelayModeChanged,
|
||||
onRelayUrlChanged = onRelayUrlChanged,
|
||||
onAddRelayUrl = onAddRelayUrl,
|
||||
@@ -107,6 +109,7 @@ fun SettingsScreen(
|
||||
onBugIncludeLogsChanged = onBugIncludeLogsChanged,
|
||||
onSubmitBugReport = onSubmitBugReport,
|
||||
onDeleteAllTransfers = onDeleteAllTransfers,
|
||||
onClearTransferCache = onClearTransferCache,
|
||||
onRelayModeChanged = onRelayModeChanged,
|
||||
onRelayUrlChanged = onRelayUrlChanged,
|
||||
onAddRelayUrl = onAddRelayUrl,
|
||||
@@ -139,6 +142,7 @@ private fun SettingsSectionContent(
|
||||
onBugIncludeLogsChanged: (Boolean) -> Unit,
|
||||
onSubmitBugReport: () -> Unit,
|
||||
onDeleteAllTransfers: () -> Unit,
|
||||
onClearTransferCache: () -> Unit,
|
||||
onRelayModeChanged: (RelayMode) -> Unit,
|
||||
onRelayUrlChanged: (Int, String) -> Unit,
|
||||
onAddRelayUrl: () -> Unit,
|
||||
@@ -160,7 +164,14 @@ private fun SettingsSectionContent(
|
||||
showBack = showBack,
|
||||
)
|
||||
SettingsSection.Notifications -> NotificationSettings(state, onNotificationsChanged, onOpenNotificationSettings, onBack, showBack)
|
||||
SettingsSection.Storage -> StorageSettings(state, windowClass, onDeleteAllTransfers, onBack, showBack)
|
||||
SettingsSection.Storage -> StorageSettings(
|
||||
state,
|
||||
windowClass,
|
||||
onDeleteAllTransfers,
|
||||
onClearTransferCache,
|
||||
onBack,
|
||||
showBack,
|
||||
)
|
||||
SettingsSection.About -> AboutSettings(
|
||||
state = state,
|
||||
onDiagnosticsChanged = onDiagnosticsChanged,
|
||||
|
||||
@@ -49,6 +49,8 @@ import vnidrop.shared.generated.resources.notifications_permission_denied
|
||||
import vnidrop.shared.generated.resources.notifications_settings_open_failed
|
||||
import vnidrop.shared.generated.resources.notifications_unsupported
|
||||
import vnidrop.shared.generated.resources.relay_settings_applied
|
||||
import vnidrop.shared.generated.resources.storage_transfer_cache_cleared
|
||||
import vnidrop.shared.generated.resources.storage_transfers_deleted
|
||||
|
||||
enum class SettingsSection {
|
||||
Overview,
|
||||
@@ -111,6 +113,7 @@ data class SettingsState(
|
||||
val storage: StorageBreakdown? = null,
|
||||
val isCalculatingStorage: Boolean = false,
|
||||
val isDeletingTransfers: Boolean = false,
|
||||
val isClearingTransferCache: Boolean = false,
|
||||
) {
|
||||
val hasRelaySettingsChanges: Boolean
|
||||
get() = relayMode != savedRelaySettings.mode ||
|
||||
@@ -208,6 +211,7 @@ class SettingsViewModel(
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isCalculatingStorage = true) }
|
||||
try {
|
||||
val receiveFolder = _state.value.receiveFolder ?: fileSystemService.defaultReceiveFolder()
|
||||
val coreUsage = repository.storageUsage().getOrThrow()
|
||||
val artifacts = repository.receivedArtifacts().getOrThrow()
|
||||
val received = fileSystemService.inspectReceivedArtifacts(artifacts)
|
||||
@@ -216,7 +220,7 @@ class SettingsViewModel(
|
||||
storage = StorageBreakdown(
|
||||
transferCacheBytes = coreUsage.blobStoreBytes,
|
||||
appDataBytes = coreUsage.appDataBytes,
|
||||
temporaryBytes = fileSystemService.temporaryUsage(),
|
||||
temporaryBytes = fileSystemService.temporaryUsage(receiveFolder),
|
||||
receivedBytes = received.existingBytes,
|
||||
receivedFileCount = received.existingCount,
|
||||
missingReceivedFileCount = received.missingCount,
|
||||
@@ -235,18 +239,45 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
fun deleteAllTransfers() {
|
||||
if (_state.value.isDeletingTransfers) return
|
||||
if (_state.value.isDeletingTransfers || _state.value.isClearingTransferCache) return
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isDeletingTransfers = true) }
|
||||
val failures = repository.state.value.transfers
|
||||
.map { repository.delete(it.transferId) }
|
||||
.count { it.isFailure }
|
||||
val cleanup = repository.clearTransferCache()
|
||||
_state.update { it.copy(isDeletingTransfers = false) }
|
||||
if (failures == 0) {
|
||||
loadStorageUsage()
|
||||
} else {
|
||||
loadStorageUsage()
|
||||
if (failures > 0) {
|
||||
messages.error(IllegalStateException("Could not delete $failures transfer records"))
|
||||
} else if (cleanup.isSuccess) {
|
||||
messages.tryShow(UiMessage(UiText.Resource(Res.string.storage_transfers_deleted), UiMessageTone.Success))
|
||||
}
|
||||
cleanup.onFailure(messages::error)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTransferCache() {
|
||||
if (
|
||||
_state.value.isDeletingTransfers ||
|
||||
_state.value.isClearingTransferCache ||
|
||||
_state.value.hasActiveNetworkWork
|
||||
) return
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isClearingTransferCache = true) }
|
||||
repository.clearTransferCache().fold(
|
||||
onSuccess = {
|
||||
_state.update { it.copy(isClearingTransferCache = false) }
|
||||
loadStorageUsage()
|
||||
messages.tryShow(
|
||||
UiMessage(UiText.Resource(Res.string.storage_transfer_cache_cleared), UiMessageTone.Success),
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
_state.update { it.copy(isClearingTransferCache = false) }
|
||||
messages.error(error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ import vnidrop.shared.generated.resources.Res
|
||||
import vnidrop.shared.generated.resources.button_cancel
|
||||
import vnidrop.shared.generated.resources.storage_app_data
|
||||
import vnidrop.shared.generated.resources.storage_calculating
|
||||
import vnidrop.shared.generated.resources.storage_clear_transfer_cache
|
||||
import vnidrop.shared.generated.resources.storage_clear_transfer_cache_description
|
||||
import vnidrop.shared.generated.resources.storage_clearing_transfer_cache
|
||||
import vnidrop.shared.generated.resources.storage_delete_transfers
|
||||
import vnidrop.shared.generated.resources.storage_delete_transfers_description
|
||||
import vnidrop.shared.generated.resources.storage_deleting
|
||||
@@ -48,10 +51,12 @@ internal fun StorageSettings(
|
||||
state: SettingsState,
|
||||
windowClass: WindowClass,
|
||||
onDeleteAllTransfers: () -> Unit,
|
||||
onClearTransferCache: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
showBack: Boolean,
|
||||
) {
|
||||
var showDeleteConfirmation by rememberSaveable { mutableStateOf(false) }
|
||||
var showClearCacheConfirmation by rememberSaveable { mutableStateOf(false) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
SettingsTopBar(stringResource(Res.string.storage_title), onBack, showBack)
|
||||
val storage = state.storage
|
||||
@@ -79,9 +84,25 @@ internal fun StorageSettings(
|
||||
)
|
||||
}
|
||||
}
|
||||
SecondaryButton(
|
||||
text = stringResource(
|
||||
if (state.isClearingTransferCache) {
|
||||
Res.string.storage_clearing_transfer_cache
|
||||
} else {
|
||||
Res.string.storage_clear_transfer_cache
|
||||
},
|
||||
),
|
||||
onClick = { showClearCacheConfirmation = true },
|
||||
enabled = !state.isDeletingTransfers &&
|
||||
!state.isClearingTransferCache &&
|
||||
!state.isCalculatingStorage &&
|
||||
!state.hasActiveNetworkWork,
|
||||
)
|
||||
Button(
|
||||
onClick = { showDeleteConfirmation = true },
|
||||
enabled = !state.isDeletingTransfers,
|
||||
enabled = !state.isDeletingTransfers &&
|
||||
!state.isClearingTransferCache &&
|
||||
!state.isCalculatingStorage,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
) {
|
||||
Text(stringResource(if (state.isDeletingTransfers) Res.string.storage_deleting else Res.string.storage_delete_transfers))
|
||||
@@ -92,6 +113,20 @@ internal fun StorageSettings(
|
||||
color = LocalVniDropColors.current.foregroundLighter,
|
||||
)
|
||||
}
|
||||
if (showClearCacheConfirmation) {
|
||||
AdaptiveDrawer(
|
||||
windowClass = windowClass,
|
||||
onDismissRequest = { showClearCacheConfirmation = false },
|
||||
) {
|
||||
ClearTransferCachePanel(
|
||||
onCancel = { showClearCacheConfirmation = false },
|
||||
onConfirm = {
|
||||
showClearCacheConfirmation = false
|
||||
onClearTransferCache()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDeleteConfirmation) {
|
||||
AdaptiveDrawer(
|
||||
windowClass = windowClass,
|
||||
@@ -108,6 +143,36 @@ internal fun StorageSettings(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClearTransferCachePanel(
|
||||
onCancel: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.storage_clear_transfer_cache),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.storage_clear_transfer_cache_description),
|
||||
color = LocalVniDropColors.current.foregroundLighter,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)) {
|
||||
SecondaryButton(stringResource(Res.string.button_cancel), onClick = onCancel)
|
||||
DestructiveButton(
|
||||
stringResource(Res.string.storage_clear_transfer_cache),
|
||||
onClick = onConfirm,
|
||||
modifier = Modifier.testTag("confirm-clear-transfer-cache"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteAllTransfersPanel(
|
||||
onCancel: () -> Unit,
|
||||
|
||||
@@ -137,6 +137,57 @@ class ViewModelsTest {
|
||||
assertEquals("Ada ", viewModel.state.value.username)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsClearsTransferCacheExplicitly() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
clearTransferCacheResult = Result.success(3_700_000_000UL)
|
||||
}
|
||||
val viewModel = settingsViewModel(repository = core)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.clearTransferCache()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, core.clearTransferCacheCount)
|
||||
assertFalse(viewModel.state.value.isClearingTransferCache)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsDoesNotClearTransferCacheDuringActiveNetworkWork() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
mutableState.value = CoreState(status = CoreStatus("endpoint", 1UL, 0UL))
|
||||
}
|
||||
val viewModel = settingsViewModel(repository = core)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.clearTransferCache()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(0, core.clearTransferCacheCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsDeleteAllTransfersImmediatelyClearsUnusedCache() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
mutableState.value = CoreState(
|
||||
isInitialized = true,
|
||||
transfers = listOf(receivedTransfer(41UL, TransferStatus.Done)),
|
||||
)
|
||||
}
|
||||
val viewModel = settingsViewModel(repository = core)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.deleteAllTransfers()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(listOf(41UL), core.deletedTransfers)
|
||||
assertEquals(1, core.clearTransferCacheCount)
|
||||
assertFalse(viewModel.state.value.isDeletingTransfers)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsAppliesNormalizedCustomRelaysAndPersistsThem() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
|
||||
@@ -47,10 +47,12 @@ class FakeCoreGateway : CoreGateway {
|
||||
var receiveSuspend: Boolean = false
|
||||
private var receiveGate: CompletableDeferred<Unit>? = null
|
||||
var deleteResult: Result<Unit> = Result.success(Unit)
|
||||
var clearTransferCacheResult: Result<ULong> = Result.success(0UL)
|
||||
var clearReceiveHistoryResult: Result<ULong> = Result.success(0UL)
|
||||
val deletedTransfers = mutableListOf<ULong>()
|
||||
val cancelledTransfers = mutableListOf<ULong>()
|
||||
var clearReceiveHistoryCount = 0
|
||||
var clearTransferCacheCount = 0
|
||||
var receiveCount = 0
|
||||
var lastReceiveTicket: String? = null
|
||||
var lastReceiveReceiverName: String? = null
|
||||
@@ -154,6 +156,10 @@ class FakeCoreGateway : CoreGateway {
|
||||
override suspend fun storageUsage(): Result<CoreStorageUsageModel> = Result.success(
|
||||
CoreStorageUsageModel(0UL, 0UL, 0UL, 0UL, 0UL),
|
||||
)
|
||||
override suspend fun clearTransferCache(): Result<ULong> {
|
||||
clearTransferCacheCount += 1
|
||||
return clearTransferCacheResult
|
||||
}
|
||||
override suspend fun receivedArtifacts(): Result<List<ReceivedArtifactModel>> = Result.success(emptyList())
|
||||
override suspend fun cancel(transferId: ULong): Result<Unit> {
|
||||
cancelledTransfers += transferId
|
||||
@@ -252,7 +258,7 @@ class FakeFileSystemService(
|
||||
override suspend fun validateReceiveFolder(folder: ReceiveFolder) = FolderAccessStatus.Writable
|
||||
override suspend fun inspectReceivedArtifacts(artifacts: List<ReceivedArtifactModel>) =
|
||||
ReceivedStorageInspection(artifacts.fold(0UL) { total, item -> total + item.logicalSize }, artifacts.size, 0, 0)
|
||||
override suspend fun temporaryUsage(): ULong = 0UL
|
||||
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = 0UL
|
||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null
|
||||
override fun canRevealReceiveFolder(folder: ReceiveFolder) = canRevealFolder
|
||||
override suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> {
|
||||
|
||||
@@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import uniffi.vnidrop.ReceiveOutputSinkV2
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
|
||||
@Composable
|
||||
actual fun rememberFileSystemService(): FileSystemService =
|
||||
@@ -42,7 +44,9 @@ private class JvmFileSystemService : FileSystemService {
|
||||
return ReceivedStorageInspection(bytes, existing, missing, 0)
|
||||
}
|
||||
|
||||
override suspend fun temporaryUsage(): ULong = 0UL
|
||||
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong {
|
||||
return desktopTemporaryUsage(receiveFolder)
|
||||
}
|
||||
|
||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null
|
||||
|
||||
@@ -65,3 +69,22 @@ private class JvmFileSystemService : FileSystemService {
|
||||
return repository.shareSources(sources, transferName, senderName, accessPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun desktopTemporaryUsage(receiveFolder: ReceiveFolder): ULong {
|
||||
if (receiveFolder.kind != ReceiveFolderKind.FileSystemPath) return 0UL
|
||||
val root = File(receiveFolder.value).toPath()
|
||||
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) return 0UL
|
||||
return runCatching {
|
||||
Files.walk(root).use { paths ->
|
||||
paths.iterator().asSequence()
|
||||
.filter { path ->
|
||||
val name = path.fileName?.toString().orEmpty()
|
||||
Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) &&
|
||||
name.startsWith(".") &&
|
||||
name.contains(".vnidrop-") &&
|
||||
name.endsWith(".part")
|
||||
}
|
||||
.fold(0UL) { total, path -> total + Files.size(path).toULong() }
|
||||
}
|
||||
}.getOrDefault(0UL)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CoreRepositoryStorageTest {
|
||||
@Test
|
||||
fun cacheClearWaitsForActiveSharesThenRestartsWithTheSameIdentity() = runTest {
|
||||
val appData = createTempDirectory("vnidrop-cache-clear")
|
||||
val source = Files.write(appData.resolve("source.bin"), ByteArray(64 * 1024) { 5 })
|
||||
val repository = CoreRepository()
|
||||
try {
|
||||
assertTrue(
|
||||
repository.initialize(
|
||||
appData.toString(),
|
||||
RelaySettings(mode = RelayMode.LocalOnly),
|
||||
).isSuccess,
|
||||
)
|
||||
val endpointId = repository.state.value.status?.endpointId
|
||||
val share = repository.sharePath(
|
||||
path = source.toString(),
|
||||
transferName = "source.bin",
|
||||
senderName = "Sender",
|
||||
accessPolicy = ShareAccessPolicy.RequireApproval,
|
||||
).getOrThrow()
|
||||
|
||||
assertTrue(repository.clearTransferCache().isFailure)
|
||||
assertTrue(repository.state.value.isInitialized)
|
||||
|
||||
repository.delete(share.transferId).getOrThrow()
|
||||
assertTrue(repository.clearTransferCache().getOrThrow() >= 64UL * 1024UL)
|
||||
assertTrue(repository.state.value.isInitialized)
|
||||
assertEquals(endpointId, repository.state.value.status?.endpointId)
|
||||
} finally {
|
||||
repository.shutdown()
|
||||
appData.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FileSystemServiceTest {
|
||||
@Test
|
||||
fun desktopTemporaryUsageCountsOnlyVnidropPartFiles() {
|
||||
val root = createTempDirectory("vnidrop-temporary-usage")
|
||||
try {
|
||||
val nested = root.resolve("nested").createDirectories()
|
||||
Files.write(nested.resolve(".photo.jpg.vnidrop-test.part"), ByteArray(7))
|
||||
Files.write(nested.resolve("photo.jpg"), ByteArray(13))
|
||||
Files.write(nested.resolve(".unrelated.part"), ByteArray(17))
|
||||
|
||||
assertEquals(
|
||||
7UL,
|
||||
desktopTemporaryUsage(
|
||||
ReceiveFolder(ReceiveFolderKind.FileSystemPath, root.toString(), "Test"),
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
root.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,7 @@ class FoundationComposeTest {
|
||||
@Test
|
||||
fun storageDeleteAllTransfersRequiresConfirmation() = runComposeUiTest {
|
||||
var deleteRequested = false
|
||||
var cacheClearRequested = false
|
||||
setContent {
|
||||
VniDropTheme(isDarkTheme = false) {
|
||||
SettingsScreen(
|
||||
@@ -196,15 +197,24 @@ class FoundationComposeTest {
|
||||
onBugIncludeLogsChanged = {},
|
||||
onSubmitBugReport = {},
|
||||
onDeleteAllTransfers = { deleteRequested = true },
|
||||
onClearTransferCache = { cacheClearRequested = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithText("Clear transfer cache").performClick()
|
||||
onNodeWithText(
|
||||
"Removes cached transfer content after briefly restarting VniDrop. " +
|
||||
"Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted.",
|
||||
).assertIsDisplayed()
|
||||
runOnIdle { assertFalse(cacheClearRequested) }
|
||||
onNodeWithTag("confirm-clear-transfer-cache").performClick()
|
||||
runOnIdle { assertTrue(cacheClearRequested) }
|
||||
|
||||
onNodeWithText("Delete all transfers").performClick()
|
||||
onNodeWithText(
|
||||
"This clears all sent and received transfer records from your history. Your received files are not deleted. " +
|
||||
"Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. " +
|
||||
"This can’t be undone.",
|
||||
"This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. " +
|
||||
"Ongoing transfers and received files are not deleted. This can’t be undone.",
|
||||
).assertIsDisplayed()
|
||||
runOnIdle { assertFalse(deleteRequested) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user