summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadLogging/LogRotation.swift
blob: ffe8c11997415141dd16cfced8b7b14d4152f2bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//
//  LogRotation.swift
//  MullvadVPN
//
//  Created by pronebird on 02/08/2020.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadTypes

public enum LogRotation {
    private struct LogData {
        var path: URL
        var size: UInt64
        var creationDate: Date
    }

    public struct Options {
        let storageSizeLimit: Int
        let oldestAllowedDate: Date

        /// Options for log rotation, defining how logs should be retained.
        ///
        /// - Parameter storageSizeLimit: Storage size limit in bytes.
        /// - Parameter oldestAllowedDate: Oldest allowed date.
        public init(storageSizeLimit: Int, oldestAllowedDate: Date) {
            self.storageSizeLimit = storageSizeLimit
            self.oldestAllowedDate = oldestAllowedDate
        }
    }

    public enum Error: LocalizedError, WrappingError {
        case rotateLogFiles(Swift.Error)

        public var errorDescription: String? {
            switch self {
            case .rotateLogFiles:
                return "Failure to rotate logs"
            }
        }

        public var underlyingError: Swift.Error? {
            switch self {
            case let .rotateLogFiles(error):
                return error
            }
        }
    }

    public static func rotateLogs(logDirectory: URL, options: Options) throws {
        let fileManager = FileManager.default

        do {
            // Filter out all log files in directory.
            let logPaths: [URL] =
                (try fileManager.contentsOfDirectory(
                    atPath: logDirectory.relativePath
                )).compactMap { file in
                    if file.split(separator: ".").last == "log" {
                        logDirectory.appendingPathComponent(file)
                    } else {
                        nil
                    }
                }

            // Convert logs into objects with necessary meta data.
            let logs = try logPaths.map { logPath in
                let attributes = try fileManager.attributesOfItem(atPath: logPath.relativePath)
                let size = (attributes[.size] as? UInt64) ?? 0
                let creationDate = (attributes[.creationDate] as? Date) ?? Date.distantPast

                return LogData(path: logPath, size: size, creationDate: creationDate)
            }.sorted { log1, log2 in
                log1.creationDate > log2.creationDate
            }

            try deleteLogs(dateThreshold: options.oldestAllowedDate, sizeThreshold: options.storageSizeLimit, in: logs)
        } catch {
            throw Error.rotateLogFiles(error)
        }
    }

    private static func deleteLogs(dateThreshold: Date, sizeThreshold: Int, in logs: [LogData]) throws {
        let fileManager = FileManager.default

        var fileSizes = UInt64.zero
        for log in logs {
            fileSizes += log.size

            let logIsTooOld = log.creationDate < dateThreshold
            let logCapacityIsExceeded = fileSizes > sizeThreshold

            if logIsTooOld || logCapacityIsExceeded {
                try fileManager.removeItem(at: log.path)
            }
        }
    }
}