File size: 3,410 Bytes
4977db2 | 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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 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?
}
|