Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

View File

@@ -0,0 +1,338 @@
//
// StorageDiagnostics.swift
// Yattee
//
// Storage diagnostics and utilities for downloads.
//
import Foundation
// MARK: - Storage Diagnostics
/// Represents storage usage for a specific directory or category.
struct StorageUsageItem: Identifiable {
let id = UUID()
let name: String
let path: String
let size: Int64
let fileCount: Int
}
/// Comprehensive storage diagnostics for debugging app storage usage.
struct StorageDiagnostics {
let items: [StorageUsageItem]
let totalSize: Int64
let documentsSize: Int64
let cachesSize: Int64
let appSupportSize: Int64
let tempSize: Int64
let otherSize: Int64
var formattedTotal: String { formatBytes(totalSize) }
var formattedDocuments: String { formatBytes(documentsSize) }
var formattedCaches: String { formatBytes(cachesSize) }
var formattedAppSupport: String { formatBytes(appSupportSize) }
var formattedTemp: String { formatBytes(tempSize) }
private func formatBytes(_ bytes: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
return formatter.string(fromByteCount: bytes)
}
@MainActor
func logDiagnostics() {
LoggingService.shared.logDownload(
"=== COMPREHENSIVE STORAGE DIAGNOSTICS ===",
details: """
Total app storage: \(formattedTotal)
Documents: \(formattedDocuments)
Caches: \(formattedCaches)
Application Support: \(formattedAppSupport)
Temp: \(formattedTemp)
"""
)
LoggingService.shared.logDownload("=== STORAGE BREAKDOWN ===")
for item in items.sorted(by: { $0.size > $1.size }) {
let formatted = formatBytes(item.size)
LoggingService.shared.logDownload(
"\(item.name): \(formatted)",
details: "Files: \(item.fileCount), Path: \(item.path)"
)
}
}
}
/// Helper to format bytes (standalone function for use in scanAppStorage)
private func formatBytesStatic(_ bytes: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
return formatter.string(fromByteCount: bytes)
}
/// Scans all app directories and returns comprehensive storage diagnostics.
@MainActor
func scanAppStorage() -> StorageDiagnostics {
let fileManager = FileManager.default
var items: [StorageUsageItem] = []
// Helper to calculate directory size (including hidden files)
func directorySize(at url: URL, includeHidden: Bool = false) -> (size: Int64, count: Int) {
var totalSize: Int64 = 0
var fileCount = 0
let options: FileManager.DirectoryEnumerationOptions = includeHidden ? [] : [.skipsHiddenFiles]
guard let enumerator = fileManager.enumerator(
at: url,
includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey, .totalFileAllocatedSizeKey],
options: options
) else {
return (0, 0)
}
for case let fileURL as URL in enumerator {
guard let resourceValues = try? fileURL.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey, .totalFileAllocatedSizeKey]),
resourceValues.isDirectory != true else {
continue
}
// Use allocated size if available (accounts for sparse files and actual disk usage)
let size = Int64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileSize ?? 0)
totalSize += size
fileCount += 1
}
return (totalSize, fileCount)
}
// Documents directory
var documentsTotal: Int64 = 0
if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
// Downloads subfolder
let downloadsURL = documentsURL.appendingPathComponent("Downloads")
if fileManager.fileExists(atPath: downloadsURL.path) {
let (size, count) = directorySize(at: downloadsURL)
items.append(StorageUsageItem(name: "Downloads", path: downloadsURL.path, size: size, fileCount: count))
documentsTotal += size
}
// Check for other items in Documents
if let contents = try? fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) {
for item in contents where item.lastPathComponent != "Downloads" {
let (size, count) = directorySize(at: item)
if size > 0 {
items.append(StorageUsageItem(name: "Documents/\(item.lastPathComponent)", path: item.path, size: size, fileCount: count))
documentsTotal += size
}
}
}
}
// Caches directory
var cachesTotal: Int64 = 0
if let cachesURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
// Image cache
let imageCacheURL = cachesURL.appendingPathComponent("ImageCache")
if fileManager.fileExists(atPath: imageCacheURL.path) {
let (size, count) = directorySize(at: imageCacheURL)
items.append(StorageUsageItem(name: "Image Cache", path: imageCacheURL.path, size: size, fileCount: count))
cachesTotal += size
}
// Feed cache
let feedCacheURL = cachesURL.appendingPathComponent("FeedCache")
if fileManager.fileExists(atPath: feedCacheURL.path) {
let (size, count) = directorySize(at: feedCacheURL)
items.append(StorageUsageItem(name: "Feed Cache", path: feedCacheURL.path, size: size, fileCount: count))
cachesTotal += size
}
// URLSession cache (com.apple.nsurlsessiond)
if let contents = try? fileManager.contentsOfDirectory(at: cachesURL, includingPropertiesForKeys: nil) {
for item in contents {
let name = item.lastPathComponent
if name == "ImageCache" || name == "FeedCache" { continue }
let (size, count) = directorySize(at: item)
if size > 1024 { // Only show if > 1KB
items.append(StorageUsageItem(name: "Caches/\(name)", path: item.path, size: size, fileCount: count))
cachesTotal += size
}
}
}
}
// Application Support directory
var appSupportTotal: Int64 = 0
if let appSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
if let contents = try? fileManager.contentsOfDirectory(at: appSupportURL, includingPropertiesForKeys: nil) {
for item in contents {
let (size, count) = directorySize(at: item)
if size > 1024 { // Only show if > 1KB
items.append(StorageUsageItem(name: "AppSupport/\(item.lastPathComponent)", path: item.path, size: size, fileCount: count))
appSupportTotal += size
}
}
}
}
// Temp directory
var tempTotal: Int64 = 0
let tempURL = fileManager.temporaryDirectory
if let contents = try? fileManager.contentsOfDirectory(at: tempURL, includingPropertiesForKeys: nil) {
for item in contents {
let (size, count) = directorySize(at: item)
if size > 1024 { // Only show if > 1KB
items.append(StorageUsageItem(name: "Temp/\(item.lastPathComponent)", path: item.path, size: size, fileCount: count))
tempTotal += size
}
}
}
// Library directory - scan ALL subdirectories to find hidden storage
var otherTotal: Int64 = 0
if let libraryURL = fileManager.urls(for: .libraryDirectory, in: .userDomainMask).first {
// Get all items in Library (including hidden)
if let contents = try? fileManager.contentsOfDirectory(at: libraryURL, includingPropertiesForKeys: nil, options: []) {
for item in contents {
let name = item.lastPathComponent
// Skip Caches (already scanned) and Application Support (already scanned)
if name == "Caches" || name == "Application Support" { continue }
let (size, count) = directorySize(at: item, includeHidden: true)
if size > 1024 { // Only show if > 1KB
items.append(StorageUsageItem(name: "Library/\(name)", path: item.path, size: size, fileCount: count))
otherTotal += size
}
}
}
}
// Check the app container root for anything we might have missed
// Go up from Documents to the container root
if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
let containerURL = documentsURL.deletingLastPathComponent()
// Scan for any top-level directories we haven't covered
if let contents = try? fileManager.contentsOfDirectory(at: containerURL, includingPropertiesForKeys: nil, options: []) {
for item in contents {
let name = item.lastPathComponent
// Skip directories we've already scanned
if name == "Documents" || name == "Library" || name == "tmp" || name == "SystemData" { continue }
let (size, count) = directorySize(at: item, includeHidden: true)
if size > 1024 {
items.append(StorageUsageItem(name: "Container/\(name)", path: item.path, size: size, fileCount: count))
otherTotal += size
}
}
}
// Log the container path for reference
LoggingService.shared.logDownload("App Container", details: containerURL.path)
// Scan ENTIRE container recursively to find all storage
let (totalContainerSize, totalContainerFiles) = directorySize(at: containerURL, includeHidden: true)
LoggingService.shared.logDownload(
"TOTAL Data Container: \(formatBytesStatic(totalContainerSize))",
details: "\(totalContainerFiles) files at \(containerURL.path)"
)
// Scan ALL top-level directories in container to find where storage is hiding
LoggingService.shared.logDownload("=== CONTAINER BREAKDOWN ===")
if let allContents = try? fileManager.contentsOfDirectory(at: containerURL, includingPropertiesForKeys: nil, options: []) {
for item in allContents {
let (itemSize, itemCount) = directorySize(at: item, includeHidden: true)
LoggingService.shared.logDownload(
" \(item.lastPathComponent): \(formatBytesStatic(itemSize))",
details: "\(itemCount) files"
)
// If this is a large directory, scan its subdirectories too
if itemSize > 100 * 1024 * 1024 { // > 100 MB
if let subContents = try? fileManager.contentsOfDirectory(at: item, includingPropertiesForKeys: nil, options: []) {
for subItem in subContents {
let (subSize, subCount) = directorySize(at: subItem, includeHidden: true)
if subSize > 10 * 1024 * 1024 { // > 10 MB
LoggingService.shared.logDownload(
" \(subItem.lastPathComponent): \(formatBytesStatic(subSize))",
details: "\(subCount) files"
)
}
}
}
}
}
}
}
// Check the app bundle size (this is read-only, can't be cleared)
var bundleSize: Int64 = 0
if let bundleURL = Bundle.main.bundleURL as URL? {
let (size, count) = directorySize(at: bundleURL, includeHidden: true)
bundleSize = size
items.append(StorageUsageItem(name: "App Bundle (read-only)", path: bundleURL.path, size: size, fileCount: count))
LoggingService.shared.logDownload("App Bundle", details: bundleURL.path)
// Log contents of bundle for debugging
if let contents = try? fileManager.contentsOfDirectory(at: bundleURL, includingPropertiesForKeys: nil, options: []) {
for item in contents {
let (itemSize, itemCount) = directorySize(at: item, includeHidden: true)
if itemSize > 1024 * 1024 { // Only log items > 1MB
LoggingService.shared.logDownload(" Bundle/\(item.lastPathComponent): \(formatBytesStatic(itemSize))", details: "\(itemCount) files")
}
}
}
// Check parent of bundle for other app-related directories
let bundleParent = bundleURL.deletingLastPathComponent()
if let parentContents = try? fileManager.contentsOfDirectory(at: bundleParent, includingPropertiesForKeys: nil, options: []) {
for item in parentContents where item.lastPathComponent != bundleURL.lastPathComponent {
let (itemSize, itemCount) = directorySize(at: item, includeHidden: true)
if itemSize > 1024 * 1024 { // Only log items > 1MB
items.append(StorageUsageItem(name: "BundleContainer/\(item.lastPathComponent)", path: item.path, size: itemSize, fileCount: itemCount))
bundleSize += itemSize
LoggingService.shared.logDownload("BundleContainer/\(item.lastPathComponent): \(formatBytesStatic(itemSize))", details: "\(itemCount) files")
}
}
}
LoggingService.shared.logDownload("Bundle container", details: bundleParent.path)
}
let totalSize = documentsTotal + cachesTotal + appSupportTotal + tempTotal + otherTotal + bundleSize
return StorageDiagnostics(
items: items,
totalSize: totalSize,
documentsSize: documentsTotal,
cachesSize: cachesTotal,
appSupportSize: appSupportTotal,
tempSize: tempTotal,
otherSize: otherTotal
)
}
// MARK: - Thread-Safe Storage
/// A thread-safe wrapper for mutable values using NSLock.
/// Conforms to Sendable since all access is synchronized.
final class LockedStorage<Value>: @unchecked Sendable {
private var value: Value
private let lock = NSLock()
init(_ value: Value) {
self.value = value
}
func read<T>(_ block: (Value) -> T) -> T {
lock.lock()
defer { lock.unlock() }
return block(value)
}
func write(_ block: (inout Value) -> Void) {
lock.lock()
defer { lock.unlock() }
block(&value)
}
}