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
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user