diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 79322c4e28..d91e6dcc93 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -188,8 +188,8 @@ public final class MediaBox { let _ = self.ensureDirectoryCreated } - public func setMaxStoreTimes(general: Int32, shortLived: Int32) { - self.timeBasedCleanup.setMaxStoreTimes(general: general, shortLived: shortLived) + public func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { + self.timeBasedCleanup.setMaxStoreTimes(general: general, shortLived: shortLived, gigabytesLimit: gigabytesLimit) } private func fileNameForId(_ id: MediaResourceId) -> String { diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index c2946c2769..83a79df798 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -3,15 +3,15 @@ import SwiftSignalKit private typealias SignalKitTimer = SwiftSignalKit.Timer -private func scanFiles(at path: String, olderThan minTimestamp: Int32, _ f: (String) -> Void) { - guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey, .isDirectoryKey], options: [.skipsSubdirectoryDescendants], errorHandler: nil) else { +private func scanFiles(at path: String, olderThan minTimestamp: Int32, anyway: ((String, Int, Int32)) -> Void, unlink f: (String) -> Void) { + guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey, .isDirectoryKey, .fileSizeKey], options: [.skipsSubdirectoryDescendants], errorHandler: nil) else { return } while let item = enumerator.nextObject() { guard let url = item as? NSURL else { continue } - guard let resourceValues = try? url.resourceValues(forKeys: [.contentModificationDateKey, .isDirectoryKey]) else { + guard let resourceValues = try? url.resourceValues(forKeys: [.contentModificationDateKey, .isDirectoryKey, .fileSizeKey]) else { continue } if let value = resourceValues[.isDirectoryKey] as? Bool, value { @@ -23,6 +23,11 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, _ f: (Str f(file) } } + if let file = url.path { + if let size = (resourceValues[.fileSizeKey] as? NSNumber)?.intValue { + anyway((file, size, Int32(value.timeIntervalSince1970))) + } + } } } } @@ -37,8 +42,22 @@ private final class TimeBasedCleanupImpl { private var generalMaxStoreTime: Int32? private var shortLivedMaxStoreTime: Int32? + private var gigabytesLimit: Int32? private let scheduledScanDisposable = MetaDisposable() + + private struct GeneralFile : Comparable, Equatable { + let file: String + let size: Int + let timestamp:Int32 + static func == (lhs: GeneralFile, rhs: GeneralFile) -> Bool { + return lhs.timestamp == rhs.timestamp && lhs.size == rhs.size && lhs.file == rhs.file + } + static func < (lhs: GeneralFile, rhs: GeneralFile) -> Bool { + return lhs.timestamp < rhs.timestamp + } + } + init(queue: Queue, generalPaths: [String], shortLivedPaths: [String]) { self.queue = queue self.generalPaths = generalPaths @@ -51,38 +70,60 @@ private final class TimeBasedCleanupImpl { self.scheduledScanDisposable.dispose() } - func setMaxStoreTimes(general: Int32, shortLived: Int32) { - if self.generalMaxStoreTime != general || self.shortLivedMaxStoreTime != shortLived { + func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { + if self.generalMaxStoreTime != general || self.shortLivedMaxStoreTime != shortLived || self.gigabytesLimit != gigabytesLimit { self.generalMaxStoreTime = general + self.gigabytesLimit = gigabytesLimit self.shortLivedMaxStoreTime = shortLived - self.resetScan(general: general, shortLived: shortLived) + self.resetScan(general: general, shortLived: shortLived, gigabytesLimit: gigabytesLimit) } } - private func resetScan(general: Int32, shortLived: Int32) { + private func resetScan(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { let generalPaths = self.generalPaths let shortLivedPaths = self.shortLivedPaths let scanOnce = Signal { subscriber in DispatchQueue.global(qos: .utility).async { var removedShortLivedCount: Int = 0 var removedGeneralCount: Int = 0 + var removedGeneralLimitCount: Int = 0 let timestamp = Int32(Date().timeIntervalSince1970) + let bytesLimit = UInt64(gigabytesLimit) * 1024 * 1024 * 1024 let oldestShortLivedTimestamp = timestamp - shortLived let oldestGeneralTimestamp = timestamp - general for path in shortLivedPaths { - scanFiles(at: path, olderThan: oldestShortLivedTimestamp, { file in + scanFiles(at: path, olderThan: oldestShortLivedTimestamp, anyway: { _, _, _ in + + }, unlink: { file in removedShortLivedCount += 1 unlink(file) }) } + + var checkFiles:[GeneralFile] = [] + + var totalLimitSize: Int = 0 + for path in generalPaths { - scanFiles(at: path, olderThan: oldestGeneralTimestamp, { file in + scanFiles(at: path, olderThan: oldestGeneralTimestamp, anyway: { file, size, timestamp in + checkFiles.append(GeneralFile(file: file, size: size, timestamp: timestamp)) + totalLimitSize += size + }, unlink: { file in removedGeneralCount += 1 unlink(file) }) } - if removedShortLivedCount != 0 || removedGeneralCount != 0 { - print("[TimeBasedCleanup] removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files") + + for item in checkFiles.sorted(by: <) { + if totalLimitSize > bytesLimit { + unlink(item.file) + removedGeneralLimitCount += 1 + totalLimitSize -= item.size + } + } + + if removedShortLivedCount != 0 || removedGeneralCount != 0 || removedGeneralLimitCount != 0 { + print("[TimeBasedCleanup] removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files, \(removedGeneralLimitCount) limit files") } subscriber.putCompletion() } @@ -148,9 +189,9 @@ final class TimeBasedCleanup { } } - func setMaxStoreTimes(general: Int32, shortLived: Int32) { + func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { self.impl.with { impl in - impl.setMaxStoreTimes(general: general, shortLived: shortLived) + impl.setMaxStoreTimes(general: general, shortLived: shortLived, gigabytesLimit: gigabytesLimit) } } } diff --git a/submodules/SyncCore/Sources/CacheStorageSettings.swift b/submodules/SyncCore/Sources/CacheStorageSettings.swift index 3e3f859825..7cb77ded9e 100644 --- a/submodules/SyncCore/Sources/CacheStorageSettings.swift +++ b/submodules/SyncCore/Sources/CacheStorageSettings.swift @@ -2,21 +2,25 @@ import Postbox public struct CacheStorageSettings: PreferencesEntry, Equatable { public let defaultCacheStorageTimeout: Int32 - + public let defaultCacheStorageLimitGigabytes: Int32 + public static var defaultSettings: CacheStorageSettings { - return CacheStorageSettings(defaultCacheStorageTimeout: Int32.max) + return CacheStorageSettings(defaultCacheStorageTimeout: Int32.max, defaultCacheStorageLimitGigabytes: Int32.max) } - public init(defaultCacheStorageTimeout: Int32) { + public init(defaultCacheStorageTimeout: Int32, defaultCacheStorageLimitGigabytes: Int32) { self.defaultCacheStorageTimeout = defaultCacheStorageTimeout + self.defaultCacheStorageLimitGigabytes = defaultCacheStorageLimitGigabytes } public init(decoder: PostboxDecoder) { self.defaultCacheStorageTimeout = decoder.decodeInt32ForKey("dt", orElse: Int32.max) + self.defaultCacheStorageLimitGigabytes = decoder.decodeInt32ForKey("dl", orElse: Int32.max) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.defaultCacheStorageTimeout, forKey: "dt") + encoder.encodeInt32(self.defaultCacheStorageLimitGigabytes, forKey: "dl") } public func isEqual(to: PreferencesEntry) -> Bool { @@ -28,10 +32,13 @@ public struct CacheStorageSettings: PreferencesEntry, Equatable { } public static func ==(lhs: CacheStorageSettings, rhs: CacheStorageSettings) -> Bool { - return lhs.defaultCacheStorageTimeout == rhs.defaultCacheStorageTimeout + return lhs.defaultCacheStorageTimeout == rhs.defaultCacheStorageTimeout && lhs.defaultCacheStorageLimitGigabytes == rhs.defaultCacheStorageLimitGigabytes } public func withUpdatedDefaultCacheStorageTimeout(_ defaultCacheStorageTimeout: Int32) -> CacheStorageSettings { - return CacheStorageSettings(defaultCacheStorageTimeout: defaultCacheStorageTimeout) + return CacheStorageSettings(defaultCacheStorageTimeout: defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: self.defaultCacheStorageLimitGigabytes) + } + public func withUpdatedDefaultCacheStorageLimitGigabytes(_ defaultCacheStorageLimitGigabytes: Int32) -> CacheStorageSettings { + return CacheStorageSettings(defaultCacheStorageTimeout: self.defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: defaultCacheStorageLimitGigabytes) } } diff --git a/submodules/TelegramCore/Sources/Account.swift b/submodules/TelegramCore/Sources/Account.swift index b6acafea9e..24194959ca 100644 --- a/submodules/TelegramCore/Sources/Account.swift +++ b/submodules/TelegramCore/Sources/Account.swift @@ -1121,7 +1121,7 @@ public class Account { return } let settings: CacheStorageSettings = sharedData.entries[SharedDataKeys.cacheStorageSettings] as? CacheStorageSettings ?? CacheStorageSettings.defaultSettings - mediaBox.setMaxStoreTimes(general: settings.defaultCacheStorageTimeout, shortLived: 60 * 60) + mediaBox.setMaxStoreTimes(general: settings.defaultCacheStorageTimeout, shortLived: 60 * 60, gigabytesLimit: settings.defaultCacheStorageLimitGigabytes) }) let _ = masterNotificationsKey(masterNotificationKeyValue: self.masterNotificationKey, postbox: self.postbox, ignoreDisabled: false).start(next: { key in