|
9 | 9 | import Foundation
|
10 | 10 | import MullvadTypes
|
11 | 11 |
|
| 12 | + |
12 | 13 | public enum LogRotation {
|
| 14 | + private struct LogData { |
| 15 | + var path: URL |
| 16 | + var size: UInt64 |
| 17 | + var creationDate: Date |
| 18 | + } |
| 19 | + |
| 20 | + public struct Options { |
| 21 | + let storageSizeLimit: Int? |
| 22 | + let oldestAllowedDate: Date? |
| 23 | + |
| 24 | + /// Options for log rotation, defining how logs should be retained. |
| 25 | + /// |
| 26 | + /// - Parameter storageSizeLimit: Storage size limit in bytes. |
| 27 | + /// - Parameter oldestAllowedDate: Oldest allowed date. |
| 28 | + public init(storageSizeLimit: Int? = nil, oldestAllowedDate: Date? = nil) { |
| 29 | + self.storageSizeLimit = storageSizeLimit |
| 30 | + self.oldestAllowedDate = oldestAllowedDate |
| 31 | + } |
| 32 | + } |
| 33 | + |
13 | 34 | public enum Error: LocalizedError, WrappingError {
|
14 |
| - case noSourceLogFile |
15 |
| - case moveSourceLogFile(Swift.Error) |
| 35 | + case rotateLogFiles(Swift.Error) |
16 | 36 |
|
17 | 37 | public var errorDescription: String? {
|
18 | 38 | switch self {
|
19 |
| - case .noSourceLogFile: |
20 |
| - return "Source log file does not exist." |
21 |
| - case .moveSourceLogFile: |
22 |
| - return "Failure to move the source log file to backup." |
| 39 | + case .rotateLogFiles: |
| 40 | + return "Failure to rotate the source log file to backup." |
23 | 41 | }
|
24 | 42 | }
|
25 | 43 |
|
26 | 44 | public var underlyingError: Swift.Error? {
|
27 | 45 | switch self {
|
28 |
| - case .noSourceLogFile: |
29 |
| - return nil |
30 |
| - case let .moveSourceLogFile(error): |
| 46 | + case let .rotateLogFiles(error): |
31 | 47 | return error
|
32 | 48 | }
|
33 | 49 | }
|
34 | 50 | }
|
35 | 51 |
|
36 |
| - public static func rotateLog(logsDirectory: URL, logFileName: String) throws { |
37 |
| - let source = logsDirectory.appendingPathComponent(logFileName) |
38 |
| - let backup = source.deletingPathExtension().appendingPathExtension("old.log") |
| 52 | + public static func rotateLogs(logDirectory: URL, options: Options) throws { |
| 53 | + let fileManager = FileManager.default |
39 | 54 |
|
40 | 55 | do {
|
41 |
| - _ = try FileManager.default.replaceItemAt(backup, withItemAt: source) |
42 |
| - } catch { |
43 |
| - // FileManager returns a very obscure error chain so we need to traverse it to find |
44 |
| - // the root cause of the error. |
45 |
| - for case let fileError as CocoaError in error.underlyingErrorChain { |
46 |
| - // .fileNoSuchFile is returned when both backup and source log files do not exist |
47 |
| - // .fileReadNoSuchFile is returned when backup exists but source log file does not |
48 |
| - if fileError.code == .fileNoSuchFile || fileError.code == .fileReadNoSuchFile, |
49 |
| - fileError.url == source { |
50 |
| - throw Error.noSourceLogFile |
| 56 | + // Filter out all log files in directory. |
| 57 | + let logPaths: [URL] = (try fileManager.contentsOfDirectory( |
| 58 | + atPath: logDirectory.relativePath |
| 59 | + )).compactMap { file in |
| 60 | + if file.split(separator: ".").last == "log" { |
| 61 | + logDirectory.appendingPathComponent(file) |
| 62 | + } else { |
| 63 | + nil |
51 | 64 | }
|
52 | 65 | }
|
53 | 66 |
|
54 |
| - throw Error.moveSourceLogFile(error) |
| 67 | + // Convert logs into objects with necessary meta data. |
| 68 | + let logs = try logPaths.map { logPath in |
| 69 | + let attributes = try fileManager.attributesOfItem(atPath: logPath.relativePath) |
| 70 | + let size = (attributes[.size] as? UInt64) ?? 0 |
| 71 | + let creationDate = (attributes[.creationDate] as? Date) ?? Date.distantPast |
| 72 | + |
| 73 | + return LogData(path: logPath, size: size, creationDate: creationDate) |
| 74 | + }.sorted { log1, log2 in |
| 75 | + log1.creationDate > log2.creationDate |
| 76 | + } |
| 77 | + |
| 78 | + if let oldestAllowedDate = options.oldestAllowedDate { |
| 79 | + try rotateLogsByDate(logs: logs, oldestAllowedDate: oldestAllowedDate) |
| 80 | + } |
| 81 | + |
| 82 | + if let storageSizeLimit = options.storageSizeLimit { |
| 83 | + try rotateLogsByStorageSizeLimit(logs: logs, storageSizeLimit: storageSizeLimit) |
| 84 | + } |
| 85 | + } catch { |
| 86 | + throw Error.rotateLogFiles(error) |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + private static func rotateLogsByDate(logs: [LogData], oldestAllowedDate: Date) throws { |
| 91 | + let fileManager = FileManager.default |
| 92 | + |
| 93 | + for log in logs where log.creationDate < oldestAllowedDate { |
| 94 | + try fileManager.removeItem(at: log.path) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private static func rotateLogsByStorageSizeLimit(logs: [LogData], storageSizeLimit: Int) throws { |
| 99 | + let fileManager = FileManager.default |
| 100 | + |
| 101 | + // From newest to oldest, delete all logs outside maximum capacity. |
| 102 | + var fileSizes = UInt64.zero |
| 103 | + for log in logs { |
| 104 | + fileSizes += log.size |
| 105 | + |
| 106 | + if fileSizes > storageSizeLimit { |
| 107 | + try fileManager.removeItem(at: log.path) |
| 108 | + } |
55 | 109 | }
|
56 | 110 | }
|
57 | 111 | }
|
0 commit comments