PromtGarageMac / Sources /PromptGarageCore /Logging /ActivityLogger.swift
smlflg's picture
Initial public upload from Projekte/PromtGarageMac
4977db2 verified
Raw History Blame Contribute Delete
3.41 kB
import Foundation
/// Append-only audit log for user-visible mutations. Entries intentionally do
/// not contain prompt titles, contents, tags, complete source identifiers, or
/// selected file paths.
public final class ActivityLogger: @unchecked Sendable {
public let logURL: URL
private let lock = NSLock()
public init(logURL: URL) throws {
self.logURL = logURL
let fileManager = FileManager.default
try fileManager.createDirectory(
at: logURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
if !fileManager.fileExists(atPath: logURL.path) {
guard fileManager.createFile(atPath: logURL.path, contents: nil) else {
throw ActivityLoggerError.couldNotCreateLog(logURL)
}
}
// The log is local operational metadata and should not be readable by
// other accounts on the Mac.
try fileManager.setAttributes(
[.posixPermissions: 0o600],
ofItemAtPath: logURL.path
)
}
public func logImport(
statistics: ImportStatistics,
sourceFamilies: [String]
) throws {
try append(ActivityLogEntry(
event: .importCompleted,
promptID: nil,
sourceFamily: nil,
sourceFamilies: Array(Set(sourceFamilies)).sorted(),
statistics: statistics
))
}
public func logImport(_ report: ImportReport) throws {
// `sourceName` is deliberately omitted: even a last path component can
// disclose personal project or export names.
try append(ActivityLogEntry(
event: .importCompleted,
promptID: nil,
sourceFamily: report.provider,
sourceFamilies: [],
statistics: report.statistics
))
}
public func logDeletion(promptID: Int64, sourceFamily: String?) throws {
try append(ActivityLogEntry(
event: .promptDeleted,
promptID: promptID,
sourceFamily: sourceFamily,
sourceFamilies: [],
statistics: nil
))
}
private func append(_ entry: ActivityLogEntry) throws {
lock.lock()
defer { lock.unlock() }
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
var line = try encoder.encode(entry)
line.append(0x0A)
let handle = try FileHandle(forWritingTo: logURL)
defer { try? handle.close() }
try handle.seekToEnd()
try handle.write(contentsOf: line)
try handle.synchronize()
}
}
public enum ActivityLoggerError: LocalizedError, Sendable {
case couldNotCreateLog(URL)
public var errorDescription: String? {
switch self {
case let .couldNotCreateLog(url):
"Das lokale Aktivitätsprotokoll konnte nicht angelegt werden: \(url.path)"
}
}
}
private struct ActivityLogEntry: Encodable {
enum Event: String, Encodable {
case importCompleted = "import_completed"
case promptDeleted = "prompt_deleted"
}
let schemaVersion = 1
let timestamp = Date()
let event: Event
let promptID: Int64?
let sourceFamily: String?
let sourceFamilies: [String]
let statistics: ImportStatistics?
}