diff --git a/Sources/Puppy/FileLogger.swift b/Sources/Puppy/FileLogger.swift index 5cc02bd..d9c39ba 100644 --- a/Sources/Puppy/FileLogger.swift +++ b/Sources/Puppy/FileLogger.swift @@ -9,23 +9,59 @@ public struct FileLogger: FileLoggerable { public let fileURL: URL public let filePermission: String + + #if os(iOS) || os(macOS) + public let fileProtectionType: FileProtectionType? + public let isExcludedFromBackup: Bool + #endif public let flushMode: FlushMode public let writeMode: FileWritingErrorHandlingMode - public init(_ label: String, logLevel: LogLevel = .trace, logFormat: LogFormattable? = nil, fileURL: URL, filePermission: String = "640", flushMode: FlushMode = .always, writeMode: FileWritingErrorHandlingMode = .force) throws { + #if os(iOS) || os(macOS) + public init(_ label: String, + logLevel: LogLevel = .trace, + logFormat: LogFormattable? = nil, + fileURL: URL, + filePermission: String = "640", + fileProtectionType: FileProtectionType? = nil, + isExcludedFromBackup: Bool = false, + flushMode: FlushMode = .always, + writeMode: FileWritingErrorHandlingMode = .force) throws { + self.label = label + self.queue = DispatchQueue(label: label) + self.logLevel = logLevel + self.logFormat = logFormat + self.fileURL = fileURL + self.filePermission = filePermission + self.fileProtectionType = fileProtectionType + self.isExcludedFromBackup = isExcludedFromBackup + self.flushMode = flushMode + self.writeMode = writeMode + try commonInit() + } + #else + public init(_ label: String, + logLevel: LogLevel = .trace, + logFormat: LogFormattable? = nil, + fileURL: URL, + filePermission: String = "640", + flushMode: FlushMode = .always, + writeMode: FileWritingErrorHandlingMode = .force) throws { self.label = label self.queue = DispatchQueue(label: label) self.logLevel = logLevel self.logFormat = logFormat - self.fileURL = fileURL - puppyDebug("initialized, fileURL: \(fileURL)") self.filePermission = filePermission - self.flushMode = flushMode self.writeMode = writeMode + try commonInit() + } + #endif + private func commonInit() throws { + puppyDebug("initialized, fileURL: \(fileURL)") try validateFileURL(fileURL) try validateFilePermission(fileURL, filePermission: filePermission) try openFile() diff --git a/Sources/Puppy/FileLoggerable.swift b/Sources/Puppy/FileLoggerable.swift index 2089c2e..1a41fd5 100644 --- a/Sources/Puppy/FileLoggerable.swift +++ b/Sources/Puppy/FileLoggerable.swift @@ -19,6 +19,8 @@ public enum FileWritingErrorHandlingMode: Sendable { public protocol FileLoggerable: Loggerable, Sendable { var fileURL: URL { get } var filePermission: String { get } + var fileProtectionType: FileProtectionType? { get } + var isExcludedFromBackup: Bool { get } } extension FileLoggerable { @@ -107,6 +109,18 @@ extension FileLoggerable { if !FileManager.default.fileExists(atPath: fileURL.path) { let successful = FileManager.default.createFile(atPath: fileURL.path, contents: nil, attributes: [FileAttributeKey.posixPermissions: uintPermission]) + + if let fileProtectionType = fileProtectionType { + try FileManager.default.setAttributes([.protectionKey: fileProtectionType], ofItemAtPath: fileURL.path) + } + + if isExcludedFromBackup { + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + var fileURL = fileURL + try fileURL.setResourceValues(resourceValues) + } + if successful { puppyDebug("succeeded in creating filePath") } else { diff --git a/Sources/Puppy/FileRotationLogger.swift b/Sources/Puppy/FileRotationLogger.swift index 78fe376..0b6c421 100644 --- a/Sources/Puppy/FileRotationLogger.swift +++ b/Sources/Puppy/FileRotationLogger.swift @@ -9,30 +9,78 @@ public struct FileRotationLogger: FileLoggerable { public let fileURL: URL public let filePermission: String + + #if os(iOS) || os(macOS) + public let fileProtectionType: FileProtectionType? + public let isExcludedFromBackup: Bool + #endif + + public let flushMode: FlushMode + public let writeMode: FileWritingErrorHandlingMode let rotationConfig: RotationConfig private weak var delegate: FileRotationLoggerDelegate? - private var dateFormat: DateFormatter - - public init(_ label: String, logLevel: LogLevel = .trace, logFormat: LogFormattable? = nil, fileURL: URL, filePermission: String = "640", rotationConfig: RotationConfig, delegate: FileRotationLoggerDelegate? = nil) throws { + private let dateFormat: DateFormatter = { + let dateFormat = DateFormatter() + dateFormat.dateFormat = "yyyyMMdd'T'HHmmssZZZZZ" + dateFormat.timeZone = TimeZone(identifier: "UTC") + dateFormat.locale = Locale(identifier: "en_US_POSIX") + return dateFormat + }() + + #if os(iOS) || os(macOS) + public init(_ label: String, + logLevel: LogLevel = .trace, + logFormat: LogFormattable? = nil, + fileURL: URL, + filePermission: String = "640", + fileProtectionType: FileProtectionType? = nil, + isExcludedFromBackup: Bool = false, + rotationConfig: RotationConfig, + flushMode: FlushMode = .always, + writeMode: FileWritingErrorHandlingMode = .force, + delegate: FileRotationLoggerDelegate? = nil) throws { self.label = label self.queue = DispatchQueue(label: label) self.logLevel = logLevel self.logFormat = logFormat - - self.dateFormat = DateFormatter() - self.dateFormat.dateFormat = "yyyyMMdd'T'HHmmssZZZZZ" - self.dateFormat.timeZone = TimeZone(identifier: "UTC") - self.dateFormat.locale = Locale(identifier: "en_US_POSIX") - self.fileURL = fileURL - puppyDebug("initialized, fileURL: \(fileURL)") self.filePermission = filePermission - + self.fileProtectionType = fileProtectionType + self.isExcludedFromBackup = isExcludedFromBackup + self.flushMode = flushMode + self.writeMode = writeMode self.rotationConfig = rotationConfig self.delegate = delegate + try commonInit() + } + #else + public init(_ label: String, + logLevel: LogLevel = .trace, + logFormat: LogFormattable? = nil, + fileURL: URL, + filePermission: String = "640", + rotationConfig: RotationConfig, + flushMode: FlushMode = .always, + writeMode: FileWritingErrorHandlingMode = .force, + delegate: FileRotationLoggerDelegate? = nil) throws { + self.label = label + self.queue = DispatchQueue(label: label) + self.logLevel = logLevel + self.logFormat = logFormat + self.fileURL = fileURL + self.filePermission = filePermission + self.flushMode = flushMode + self.writeMode = writeMode + self.rotationConfig = rotationConfig + self.delegate = delegate + try commonInit() + } + #endif + private func commonInit() throws { + puppyDebug("initialized, fileURL: \(fileURL)") try validateFileURL(fileURL) try validateFilePermission(fileURL, filePermission: filePermission) try openFile() @@ -40,7 +88,7 @@ public struct FileRotationLogger: FileLoggerable { public func log(_ level: LogLevel, string: String) { rotateFiles() - append(level, string: string) + append(level, string: string, flushMode: flushMode, writeMode: writeMode) rotateFiles() } diff --git a/Tests/PuppyTests/FileLoggerTests.swift b/Tests/PuppyTests/FileLoggerTests.swift index 0f5defa..2191424 100644 --- a/Tests/PuppyTests/FileLoggerTests.swift +++ b/Tests/PuppyTests/FileLoggerTests.swift @@ -88,6 +88,46 @@ final class FileLoggerTests: XCTestCase { log.remove(fileLogger) } + func testFileProtection() throws { + #if os(iOS) || os(macOS) + let fileURL = URL(fileURLWithPath: "./protected.log").absoluteURL + let fileLogger: FileLogger = try .init("com.example.yourapp.filelogger.protected", fileURL: fileURL, fileProtectionType: .completeUntilFirstUserAuthentication) + var log = Puppy() + log.add(fileLogger) + log.trace("protection, TRACE message using FileLogger") + log.verbose("protection, VERBOSE message using FileLogger") + fileLogger.flush(fileURL) + + let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + // swiftlint:disable force_cast + let protection = attributes[FileAttributeKey.protectionKey] as! FileProtectionType + // swiftlint:enable force_cast + + XCTAssertEqual(protection, FileProtectionType.completeUntilFirstUserAuthentication) + + _ = fileLogger.delete(fileURL) + log.remove(fileLogger) + #endif + } + + func testExcludeFromBackup() throws { + #if os(iOS) || os(macOS) + let fileURL = URL(fileURLWithPath: "./exclude-from-backup.log").absoluteURL + let fileLogger: FileLogger = try .init("com.example.yourapp.filelogger.exclude-from-backup", fileURL: fileURL, isExcludedFromBackup: true) + var log = Puppy() + log.add(fileLogger) + log.trace("exclude from backup, TRACE message using FileLogger") + log.verbose("exclude from backup, VERBOSE message using FileLogger") + fileLogger.flush(fileURL) + + let resourceValues = try fileURL.resourceValues(forKeys: [.isExcludedFromBackupKey]) + XCTAssertTrue(resourceValues.isExcludedFromBackup == true) + + _ = fileLogger.delete(fileURL) + log.remove(fileLogger) + #endif + } + func testFilePermissionError() throws { let permission800FileURL = URL(fileURLWithPath: "./permission800.log").absoluteURL let filePermission800 = "800"