diff --git a/Package.resolved b/Package.resolved index 2275ef86..cdcc1dd8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire", "state" : { - "revision" : "bda9ed57d72988a3a2ada33d824583541f86eac6", - "version" : "5.12.2" + "revision" : "7595cbcf59809f9977c5f6378500de2ad73b7ddb", + "version" : "5.12.0" } }, { diff --git a/Sources/NextcloudKit/Log/NKLogFileManager.swift b/Sources/NextcloudKit/Log/NKLogFileManager.swift index b1d1bafa..0bb3a9dd 100644 --- a/Sources/NextcloudKit/Log/NKLogFileManager.swift +++ b/Sources/NextcloudKit/Log/NKLogFileManager.swift @@ -64,9 +64,10 @@ public final class NKLogFileManager: @unchecked Sendable { /// Configures the shared logger instance. /// - Parameters: - /// - minLevel: The minimum log level to be recorded. - public static func configure(logLevel: NKLogLevel = .normal) { - shared.setConfiguration(logLevel: logLevel) + /// - logLevel: The minimum log level to be recorded. + /// - logDirectory: The directory that contains the log files. Defaults to `Documents/Logs`. + public static func configure(logLevel: NKLogLevel = .normal, logDirectory: URL? = nil) { + shared.setConfiguration(logLevel: logLevel, logDirectory: logDirectory) } /// Configures filter. @@ -83,7 +84,7 @@ public final class NKLogFileManager: @unchecked Sendable { shared.setWhitelist(whitelist: whitelist) } - /// Creates the "Logs" folder inside the user's Documents directory if it does not already exist. + /// Creates the configured log directory if it does not already exist. /// /// This static method delegates to the singleton instance (`shared`) and ensures /// that the log folder structure is created or re-created when needed. @@ -92,8 +93,7 @@ public final class NKLogFileManager: @unchecked Sendable { /// (e.g., by iTunes File Sharing, iCloud Drive sync conflicts, or cleanup tools), /// and must be re-initialized manually. /// - /// The folder path is: - /// `~/Documents/Logs` + /// Unless a custom directory was configured, the folder path is `~/Documents/Logs`. /// /// If the folder already exists, the method does nothing. If creation fails, the error is silently ignored. /// @@ -102,6 +102,14 @@ public final class NKLogFileManager: @unchecked Sendable { shared.createLogsFolder() } + /// Waits until all pending file writes have completed. + /// + /// Extensions can call this before returning control to the system to prevent their final + /// messages from being lost if the extension process terminates immediately afterward. + public static func flush() { + shared.flush() + } + /// Returns the file URL of the currently active log file. public func currentLogFileURL() -> URL { return logDirectory.appendingPathComponent(logFileName) @@ -110,7 +118,7 @@ public final class NKLogFileManager: @unchecked Sendable { // MARK: - Configuration private let logFileName = "log.txt" - private let logDirectory: URL + private var logDirectory: URL public var logLevel: NKLogLevel private var blacklist: [String] = [] private var whitelist: [String] = [] @@ -125,7 +133,7 @@ public final class NKLogFileManager: @unchecked Sendable { // MARK: - Initialization - private init(logLevel: NKLogLevel = .normal, blacklist: [String]? = nil, whitelist: [String]? = nil) { + private init(logLevel: NKLogLevel = .normal, logDirectory: URL? = nil, blacklist: [String]? = nil, whitelist: [String]? = nil) { self.logLevel = logLevel if let blacklist { self.blacklist = blacklist @@ -134,8 +142,7 @@ public final class NKLogFileManager: @unchecked Sendable { self.whitelist = whitelist } - let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let logsFolder = documents.appendingPathComponent("Logs", isDirectory: true) + let logsFolder = logDirectory ?? Self.defaultLogDirectory() if !FileManager.default.fileExists(atPath: logsFolder.path) { try? FileManager.default.createDirectory(at: logsFolder, withIntermediateDirectories: true) } @@ -143,22 +150,21 @@ public final class NKLogFileManager: @unchecked Sendable { self.currentLogDate = Self.currentDateString() } - /// Creates the "Logs" folder inside the user's Documents directory if it does not already exist. + /// Creates the configured log directory if it does not already exist. /// /// This method performs the following steps: - /// - Retrieves the path to the `.documentDirectory` using `FileManager`. - /// - Appends a "Logs" subdirectory path. + /// - Uses the custom directory supplied during configuration, when available. + /// - Otherwise uses the "Logs" subdirectory of `.documentDirectory`. /// - Checks if the folder already exists. /// - If not, it creates the folder, including any intermediate directories. - /// - Finally, it sets the `logDirectory` and initializes the current log date. + /// - Initializes the current log date after ensuring the directory exists. /// /// If folder creation fails, the method silently ignores the error. /// /// - Note: The `logDirectory` property will point to the created `Logs` folder. /// private func createLogsFolder() { - let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let logsFolder = documents.appendingPathComponent("Logs", isDirectory: true) + let logsFolder = logDirectory if !FileManager.default.fileExists(atPath: logsFolder.path) { try? FileManager.default.createDirectory(at: logsFolder, withIntermediateDirectories: true) } @@ -168,8 +174,16 @@ public final class NKLogFileManager: @unchecked Sendable { /// Sets configuration parameters for the logger. /// - Parameters: /// - logLevel: The NKLogLevel { disabled .. verbose } - private func setConfiguration(logLevel: NKLogLevel) { + private func setConfiguration(logLevel: NKLogLevel, logDirectory: URL?) { + flush() self.logLevel = logLevel + self.logDirectory = logDirectory ?? Self.defaultLogDirectory() + createLogsFolder() + } + + private static func defaultLogDirectory() -> URL { + let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + return documents.appendingPathComponent("Logs", isDirectory: true) } /// Sets blacklist for the logger. @@ -337,17 +351,27 @@ public final class NKLogFileManager: @unchecked Sendable { private func rotateLog(for date: String) { let currentPath = logDirectory.appendingPathComponent(logFileName) let rotatedPath = logDirectory.appendingPathComponent("log-\(date).txt") - - do { - if fileManager.fileExists(atPath: currentPath.path) { - try fileManager.moveItem(at: currentPath, to: rotatedPath) + var coordinationError: NSError? + let coordinator = NSFileCoordinator(filePresenter: nil) + coordinator.coordinate( + writingItemAt: currentPath, + options: .forMoving, + writingItemAt: rotatedPath, + options: .forReplacing, + error: &coordinationError + ) { coordinatedCurrentPath, coordinatedRotatedPath in + do { + if fileManager.fileExists(atPath: coordinatedCurrentPath.path), + !fileManager.fileExists(atPath: coordinatedRotatedPath.path) { + try fileManager.moveItem(at: coordinatedCurrentPath, to: coordinatedRotatedPath) + } + + if !fileManager.fileExists(atPath: coordinatedCurrentPath.path) { + try Data().write(to: coordinatedCurrentPath) + } + } catch { + print("Log rotation failed: \(error)") } - - // Create a new empty log file for today - try Data().write(to: currentPath) - - } catch { - print("Log rotation failed: \(error)") } } @@ -363,22 +387,30 @@ public final class NKLogFileManager: @unchecked Sendable { guard let data = message.data(using: .utf8) else { return } - do { - if !fileManager.fileExists(atPath: logPath.path) { - fileManager.createFile(atPath: logPath.path, contents: nil) - } + var coordinationError: NSError? + let coordinator = NSFileCoordinator(filePresenter: nil) + coordinator.coordinate(writingItemAt: logPath, options: .forMerging, error: &coordinationError) { coordinatedURL in + do { + if !fileManager.fileExists(atPath: coordinatedURL.path) { + fileManager.createFile(atPath: coordinatedURL.path, contents: nil) + } - let handle = try FileHandle(forWritingTo: logPath) + let handle = try FileHandle(forWritingTo: coordinatedURL) - defer { try? handle.close() } + defer { try? handle.close() } - try handle.seekToEnd() - try handle.write(contentsOf: data) - } catch { - // Ignore log write failures to avoid crashes when the device has no free space. + try handle.seekToEnd() + try handle.write(contentsOf: data) + } catch { + // Ignore log write failures to avoid crashes when the device has no free space. + } } } + private func flush() { + logQueue.sync {} + } + // MARK: - Cached DateFormatters /// Cached formatter for "yyyy-MM-dd". Uses current calendar, locale, and time zone. diff --git a/Sources/NextcloudKit/NextcloudKit+Logging.swift b/Sources/NextcloudKit/NextcloudKit+Logging.swift index b4693703..8281ad38 100644 --- a/Sources/NextcloudKit/NextcloudKit+Logging.swift +++ b/Sources/NextcloudKit/NextcloudKit+Logging.swift @@ -2,15 +2,17 @@ // SPDX-FileCopyrightText: 2025 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later +import Foundation + public extension NextcloudKit { /// Shared logger accessible via NextcloudKit.logger static var logger: NKLogFileManager { return NKLogFileManager.shared } - /// Configure the shared logger from NextcloudKit - static func configureLogger(logLevel: NKLogLevel = .normal) { - NKLogFileManager.configure(logLevel: logLevel) + /// Configures the shared logger and optionally stores logs in a custom directory. + static func configureLogger(logLevel: NKLogLevel = .normal, logDirectory: URL? = nil) { + NKLogFileManager.configure(logLevel: logLevel, logDirectory: logDirectory) } /// Configure the shared logger blacklist from NextcloudKit @@ -22,4 +24,9 @@ public extension NextcloudKit { static func configureLoggerWhitelist(whitelist: [String]) { NKLogFileManager.setCandidate(whitelist: whitelist) } + + /// Waits until all pending log file writes have completed. + static func flushLogger() { + NKLogFileManager.flush() + } }