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