From 0a801b01cb66ebf18a3cc2cb0d17ce7e98d18119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 13 Mar 2026 15:06:27 -0700 Subject: [PATCH] Add full container file breakdown to storage settings export Users reported total app storage significantly higher than what the storage settings view shows. To aid debugging, the export now includes a "Full Container File Breakdown" section that enumerates every regular file in the app sandbox and shared app group container, sorted by size descending so orphaned/unexpectedly large files are easy to spot. Changes: - StorageStatsManager: add ContainerFileEntry and containerFileBreakdown() - StorageStatsViewHelper: add formatContainerFileBreakdown() helper and include it at the end of formatStorageStatsAsText output Closes: https://github.com/damus-io/damus/issues/3677 Changelog-Added: Full container file breakdown in storage settings export --- damus/Core/Storage/StorageStatsManager.swift | 68 +++++++++++++++++++ .../Core/Storage/StorageStatsViewHelper.swift | 35 ++++++++++ 2 files changed, 103 insertions(+) diff --git a/damus/Core/Storage/StorageStatsManager.swift b/damus/Core/Storage/StorageStatsManager.swift index 846f06df..193e6706 100644 --- a/damus/Core/Storage/StorageStatsManager.swift +++ b/damus/Core/Storage/StorageStatsManager.swift @@ -151,4 +151,72 @@ struct StorageStatsManager { formatter.isAdaptive = true return formatter.string(fromByteCount: Int64(bytes)) } + + /// A single file entry produced by container enumeration + struct ContainerFileEntry { + /// Human-readable label for the container root (e.g. "Documents") + let containerLabel: String + /// File path relative to the container root URL + let relativePath: String + /// File size in bytes + let size: UInt64 + } + + /// Enumerate every file in the app sandbox and the shared app group container. + /// + /// Results are sorted by size descending so the largest files appear first, + /// making it easy to spot unexpectedly large or orphaned items. + /// + /// - Returns: Array of `ContainerFileEntry` values, one per regular file found. + func containerFileBreakdown() -> [ContainerFileEntry] { + let fm = FileManager.default + + // Collect (label, root URL) pairs to walk + var roots: [(label: String, url: URL)] = [] + + // Primary sandbox container + if let home = fm.urls(for: .documentDirectory, in: .userDomainMask).first?.deletingLastPathComponent() { + roots.append((label: "Sandbox", url: home)) + } + + // Shared app group container (legacy nostrdb + snapshot) + if let groupURL = fm.containerURL(forSecurityApplicationGroupIdentifier: "group.com.damus") { + roots.append((label: "AppGroup", url: groupURL)) + } + + var entries: [ContainerFileEntry] = [] + + for root in roots { + guard let enumerator = fm.enumerator( + at: root.url, + includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey], + options: [.skipsHiddenFiles] + ) else { continue } + + for case let fileURL as URL in enumerator { + guard let resourceValues = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]), + resourceValues.isRegularFile == true, + let size = resourceValues.fileSize else { continue } + + // Build a path relative to the container root + let relativePath: String + if fileURL.path.hasPrefix(root.url.path) { + relativePath = String(fileURL.path.dropFirst(root.url.path.count)) + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + } else { + relativePath = fileURL.path + } + + entries.append(ContainerFileEntry( + containerLabel: root.label, + relativePath: relativePath, + size: UInt64(max(0, size)) + )) + } + } + + // Largest files first + entries.sort { $0.size > $1.size } + return entries + } } diff --git a/damus/Core/Storage/StorageStatsViewHelper.swift b/damus/Core/Storage/StorageStatsViewHelper.swift index f5dc5680..9b6e1af8 100644 --- a/damus/Core/Storage/StorageStatsViewHelper.swift +++ b/damus/Core/Storage/StorageStatsViewHelper.swift @@ -110,6 +110,9 @@ enum StorageStatsViewHelper { text += await formatNostrDBDetails(details: details) } + // Full container file listing for debugging orphaned files + text += await formatContainerFileBreakdown() + return text } @@ -166,4 +169,36 @@ enum StorageStatsViewHelper { return text } + + /// Enumerate all files in the app sandbox and shared app group container and format them as text. + /// + /// Files are sorted by size descending so the largest contributors appear first, + /// making orphaned or unexpectedly large files easy to identify. + /// + /// - Returns: Formatted text listing every file with its size and relative path. + @concurrent + private static func formatContainerFileBreakdown() async -> String { + let entries = StorageStatsManager.shared.containerFileBreakdown() + + var text = String(repeating: "=", count: 50) + "\n\n" + text += "Full Container File Breakdown:\n" + text += String(repeating: "-", count: 50) + "\n" + + if entries.isEmpty { + text += "(no files found)\n" + } else { + var totalSize: UInt64 = 0 + for entry in entries { + let sizePadded = StorageStatsManager.formatBytes(entry.size).padding(toLength: 12, withPad: " ", startingAt: 0) + text += "[\(entry.containerLabel)] \(sizePadded) \(entry.relativePath)\n" + totalSize += entry.size + } + text += String(repeating: "-", count: 50) + "\n" + let totalTitlePadded = "Total (all files)".padding(toLength: 25, withPad: " ", startingAt: 0) + let totalSizePadded = StorageStatsManager.formatBytes(totalSize).padding(toLength: 12, withPad: " ", startingAt: 0) + text += "\(totalTitlePadded) \(totalSizePadded)\n" + } + + return text + } }