mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(apple): add a Free up space action to reclaim leaked storage
Delete all transfers only clears core records, and the blob-store cache is reclaimed by the core's own timer. Neither touches the app's temporary directory (leftover picker/staging copies — hundreds of MB on macOS) or the stray .Trash folders that accumulate in app-owned directories and can't be removed via Files/Finder. Add a non-destructive Free up space button that empties the temp directory and removes .Trash folders under the core data dir (and, on iOS, the fixed Documents receive folder), reporting the bytes reclaimed. Guarded against running while a transfer is in flight; never touches received files, the core database, or user-chosen macOS receive folders.
This commit is contained in:
@@ -56,6 +56,7 @@ struct SettingsState: Equatable {
|
||||
var storage: StorageBreakdown?
|
||||
var isCalculatingStorage = false
|
||||
var isDeletingTransfers = false
|
||||
var isCleaningStorage = false
|
||||
|
||||
static func == (lhs: SettingsState, rhs: SettingsState) -> Bool {
|
||||
lhs.selectedSection == rhs.selectedSection && lhs.username == rhs.username
|
||||
@@ -72,6 +73,7 @@ struct SettingsState: Equatable {
|
||||
&& lhs.bugLogPreviewBytes == rhs.bugLogPreviewBytes
|
||||
&& lhs.storage == rhs.storage && lhs.isCalculatingStorage == rhs.isCalculatingStorage
|
||||
&& lhs.isDeletingTransfers == rhs.isDeletingTransfers
|
||||
&& lhs.isCleaningStorage == rhs.isCleaningStorage
|
||||
&& lhs.deviceInfo?.operatingSystem == rhs.deviceInfo?.operatingSystem
|
||||
}
|
||||
}
|
||||
@@ -315,6 +317,83 @@ final class SettingsModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reclaims disk space the core's transfer deletion doesn't touch: the app's
|
||||
/// temporary directory (leftover picker/staging copies) and any stray `.Trash`
|
||||
/// folders that accumulate inside app-owned directories. Never touches received
|
||||
/// files, the core database, or user-chosen receive folders.
|
||||
func freeUpSpace() {
|
||||
if state.isCleaningStorage { return }
|
||||
// Purging staging while a transfer is mid-flight could break it.
|
||||
let hasActive = repository.state.transfers.contains {
|
||||
$0.status == .sharing || $0.status == .importing || $0.status == .receiving
|
||||
}
|
||||
if hasActive {
|
||||
messages.tryShow(UiMessage(text: .resource(L10n.Storage.cleanupBusy), tone: .warning))
|
||||
return
|
||||
}
|
||||
state.isCleaningStorage = true
|
||||
let tempDir = NSTemporaryDirectory()
|
||||
let dataDir = environment.defaultCoreDataDir
|
||||
// Only clean the receive folder's trash when it is app-owned (iOS fixed
|
||||
// Documents), never a user-chosen macOS folder like ~/Downloads.
|
||||
let receiveTrashRoot = fileSystemService.supportsCustomReceiveFolders ? nil : state.receiveFolder?.value
|
||||
Task {
|
||||
let freed = await Task.detached {
|
||||
SettingsModel.reclaimJunk(tempDir: tempDir, dataDir: dataDir, receiveTrashRoot: receiveTrashRoot)
|
||||
}.value
|
||||
state.isCleaningStorage = false
|
||||
loadStorageUsage()
|
||||
messages.show(UiMessage(
|
||||
text: .dynamic(L10n.Storage.cleanupFreed(size: formatBytes(freed))),
|
||||
tone: .success
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes temp-directory contents and `.Trash` folders under the given roots,
|
||||
/// returning the number of bytes reclaimed. Runs off the main actor.
|
||||
nonisolated static func reclaimJunk(tempDir: String, dataDir: String, receiveTrashRoot: String?) -> UInt64 {
|
||||
let fm = FileManager.default
|
||||
var freed: UInt64 = 0
|
||||
// Empty the temporary directory.
|
||||
if let entries = try? fm.contentsOfDirectory(atPath: tempDir) {
|
||||
for name in entries {
|
||||
let path = (tempDir as NSString).appendingPathComponent(name)
|
||||
freed += itemSize(path)
|
||||
try? fm.removeItem(atPath: path)
|
||||
}
|
||||
}
|
||||
// Remove stray `.Trash` folders inside app-owned directories.
|
||||
for root in [dataDir, receiveTrashRoot].compactMap({ $0 }) {
|
||||
for trash in trashDirectories(under: root) {
|
||||
freed += directorySize(trash)
|
||||
try? fm.removeItem(atPath: trash)
|
||||
}
|
||||
}
|
||||
return freed
|
||||
}
|
||||
|
||||
/// Paths of every directory named `.Trash` under `root` (not descending into them).
|
||||
private nonisolated static func trashDirectories(under root: String) -> [String] {
|
||||
let url = URL(fileURLWithPath: root, isDirectory: true)
|
||||
guard let enumerator = FileManager.default.enumerator(
|
||||
at: url, includingPropertiesForKeys: [.isDirectoryKey]
|
||||
) else { return [] }
|
||||
var result: [String] = []
|
||||
for case let fileURL as URL in enumerator where fileURL.lastPathComponent == ".Trash" {
|
||||
result.append(fileURL.path)
|
||||
enumerator.skipDescendants()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Allocated size of a file or directory (0 if missing).
|
||||
private nonisolated static func itemSize(_ path: String) -> UInt64 {
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { return 0 }
|
||||
return isDirectory.boolValue ? directorySize(path) : fileSize(path)
|
||||
}
|
||||
|
||||
nonisolated static func fileSize(_ path: String) -> UInt64 {
|
||||
let values = try? URL(fileURLWithPath: path).resourceValues(
|
||||
forKeys: [.isRegularFileKey, .totalFileAllocatedSizeKey, .fileSizeKey]
|
||||
|
||||
@@ -99,6 +99,23 @@ struct StorageSettings: View {
|
||||
Text(String(localized: L10n.Storage.footer))
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
model.freeUpSpace()
|
||||
} label: {
|
||||
HStack {
|
||||
Text(model.state.isCleaningStorage
|
||||
? String(localized: L10n.Storage.cleaning)
|
||||
: String(localized: L10n.Storage.freeUpSpace))
|
||||
if model.state.isCleaningStorage {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(model.state.isCleaningStorage)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showDeleteConfirmation = true
|
||||
|
||||
@@ -3140,6 +3140,68 @@
|
||||
"ru": "Вычисление…"
|
||||
}
|
||||
},
|
||||
"storage_cleaning": {
|
||||
"context": "Settings > Storage: free-up-space button while cleanup runs.",
|
||||
"translations": {
|
||||
"en": "Cleaning up…",
|
||||
"fr": "Nettoyage…",
|
||||
"es": "Limpiando…",
|
||||
"it": "Pulizia…",
|
||||
"de": "Wird bereinigt…",
|
||||
"pt": "A limpar…",
|
||||
"pl": "Czyszczenie…",
|
||||
"nl": "Opschonen…",
|
||||
"ru": "Очистка…"
|
||||
}
|
||||
},
|
||||
"storage_cleanup_busy": {
|
||||
"context": "Settings > Storage: shown when cleanup is blocked by in-flight transfers.",
|
||||
"translations": {
|
||||
"en": "Finish active transfers before freeing up space",
|
||||
"fr": "Terminez les transferts en cours avant de libérer de l'espace",
|
||||
"es": "Finaliza las transferencias activas antes de liberar espacio",
|
||||
"it": "Completa i trasferimenti attivi prima di liberare spazio",
|
||||
"de": "Beende aktive Übertragungen, bevor du Speicher freigibst",
|
||||
"pt": "Conclui as transferências ativas antes de libertar espaço",
|
||||
"pl": "Zakończ aktywne transfery przed zwolnieniem miejsca",
|
||||
"nl": "Voltooi actieve overdrachten voordat je ruimte vrijmaakt",
|
||||
"ru": "Завершите активные передачи перед освобождением места"
|
||||
}
|
||||
},
|
||||
"storage_cleanup_freed": {
|
||||
"context": "Settings > Storage: cleanup success. {size} = amount freed.",
|
||||
"args": [
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"en": "Freed {size}",
|
||||
"fr": "{size} libéré",
|
||||
"es": "Se liberó {size}",
|
||||
"it": "Liberati {size}",
|
||||
"de": "{size} freigegeben",
|
||||
"pt": "Libertado {size}",
|
||||
"pl": "Zwolniono {size}",
|
||||
"nl": "{size} vrijgemaakt",
|
||||
"ru": "Освобождено {size}"
|
||||
}
|
||||
},
|
||||
"storage_free_up_space": {
|
||||
"context": "Settings > Storage: button that clears temporary files and stray trash.",
|
||||
"translations": {
|
||||
"en": "Free up space",
|
||||
"fr": "Libérer de l'espace",
|
||||
"es": "Liberar espacio",
|
||||
"it": "Libera spazio",
|
||||
"de": "Speicher freigeben",
|
||||
"pt": "Libertar espaço",
|
||||
"pl": "Zwolnij miejsce",
|
||||
"nl": "Ruimte vrijmaken",
|
||||
"ru": "Освободить место"
|
||||
}
|
||||
},
|
||||
"storage_delete_transfers": {
|
||||
"context": "Settings > Storage: button to delete all transfer records.",
|
||||
"translations": {
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Wird empfangen</string>
|
||||
<string name="status_stopped">Beendet</string>
|
||||
<string name="storage_calculating">Wird berechnet…</string>
|
||||
<string name="storage_cleaning">Wird bereinigt…</string>
|
||||
<string name="storage_cleanup_busy">Beende aktive Übertragungen, bevor du Speicher freigibst</string>
|
||||
<string name="storage_cleanup_freed">%1$s freigegeben</string>
|
||||
<string name="storage_free_up_space">Speicher freigeben</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_app_data">App-Daten</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Recibiendo</string>
|
||||
<string name="status_stopped">Detenido</string>
|
||||
<string name="storage_calculating">Calculando…</string>
|
||||
<string name="storage_cleaning">Limpiando…</string>
|
||||
<string name="storage_cleanup_busy">Finaliza las transferencias activas antes de liberar espacio</string>
|
||||
<string name="storage_cleanup_freed">Se liberó %1$s</string>
|
||||
<string name="storage_free_up_space">Liberar espacio</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_app_data">Datos de la aplicación</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Réception</string>
|
||||
<string name="status_stopped">Arrêté</string>
|
||||
<string name="storage_calculating">Calcul…</string>
|
||||
<string name="storage_cleaning">Nettoyage…</string>
|
||||
<string name="storage_cleanup_busy">Terminez les transferts en cours avant de libérer de l\'espace</string>
|
||||
<string name="storage_cleanup_freed">%1$s libéré</string>
|
||||
<string name="storage_free_up_space">Libérer de l\'espace</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_app_data">Données de l’app</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Ricezione</string>
|
||||
<string name="status_stopped">Interrotto</string>
|
||||
<string name="storage_calculating">Calcolo…</string>
|
||||
<string name="storage_cleaning">Pulizia…</string>
|
||||
<string name="storage_cleanup_busy">Completa i trasferimenti attivi prima di liberare spazio</string>
|
||||
<string name="storage_cleanup_freed">Liberati %1$s</string>
|
||||
<string name="storage_free_up_space">Libera spazio</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_app_data">Dati dell’app</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Ontvangen</string>
|
||||
<string name="status_stopped">Gestopt</string>
|
||||
<string name="storage_calculating">Berekenen…</string>
|
||||
<string name="storage_cleaning">Opschonen…</string>
|
||||
<string name="storage_cleanup_busy">Voltooi actieve overdrachten voordat je ruimte vrijmaakt</string>
|
||||
<string name="storage_cleanup_freed">%1$s vrijgemaakt</string>
|
||||
<string name="storage_free_up_space">Ruimte vrijmaken</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_app_data">Appgegevens</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Odbieranie</string>
|
||||
<string name="status_stopped">Zatrzymany</string>
|
||||
<string name="storage_calculating">Obliczanie…</string>
|
||||
<string name="storage_cleaning">Czyszczenie…</string>
|
||||
<string name="storage_cleanup_busy">Zakończ aktywne transfery przed zwolnieniem miejsca</string>
|
||||
<string name="storage_cleanup_freed">Zwolniono %1$s</string>
|
||||
<string name="storage_free_up_space">Zwolnij miejsce</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_app_data">Dane aplikacji</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">A receber</string>
|
||||
<string name="status_stopped">Parada</string>
|
||||
<string name="storage_calculating">A calcular…</string>
|
||||
<string name="storage_cleaning">A limpar…</string>
|
||||
<string name="storage_cleanup_busy">Conclui as transferências ativas antes de libertar espaço</string>
|
||||
<string name="storage_cleanup_freed">Libertado %1$s</string>
|
||||
<string name="storage_free_up_space">Libertar espaço</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_app_data">Dados da aplicação</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Получение</string>
|
||||
<string name="status_stopped">Остановлено</string>
|
||||
<string name="storage_calculating">Вычисление…</string>
|
||||
<string name="storage_cleaning">Очистка…</string>
|
||||
<string name="storage_cleanup_busy">Завершите активные передачи перед освобождением места</string>
|
||||
<string name="storage_cleanup_freed">Освобождено %1$s</string>
|
||||
<string name="storage_free_up_space">Освободить место</string>
|
||||
<string name="storage_delete_transfers">Удалить все передачи</string>
|
||||
<string name="storage_delete_transfers_description">Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить.</string>
|
||||
<string name="storage_app_data">Данные приложения</string>
|
||||
|
||||
@@ -203,6 +203,10 @@
|
||||
<string name="status_receiving">Receiving</string>
|
||||
<string name="status_stopped">Stopped</string>
|
||||
<string name="storage_calculating">Calculating…</string>
|
||||
<string name="storage_cleaning">Cleaning up…</string>
|
||||
<string name="storage_cleanup_busy">Finish active transfers before freeing up space</string>
|
||||
<string name="storage_cleanup_freed">Freed %1$s</string>
|
||||
<string name="storage_free_up_space">Free up space</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_app_data">App data</string>
|
||||
|
||||
Reference in New Issue
Block a user