Files

109 lines
4.5 KiB
Swift

import AppKit
public struct Note: Identifiable {
public let id: UUID
public let contents: NSAttributedString
public let title: String
init(id: UUID, contents: NSAttributedString) {
self.id = id
self.contents = NSAttributedString(attributedString: contents)
var title = "Untitled Note"
contents.string.enumerateLines { line, stop in
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
title = String(trimmed.prefix(60))
stop = true
}
}
self.title = title
}
}
@MainActor
public final class NotesStore {
public private(set) var notes: [Note] = []
private let directory: URL
private var legacyIDs: Set<UUID> = []
public init(directory: URL = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask
)[0].appendingPathComponent("Conjure/Notes", isDirectory: true)) throws {
self.directory = directory
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let files = try FileManager.default.contentsOfDirectory(
at: directory, includingPropertiesForKeys: nil
).filter { $0.pathExtension.lowercased() == "rtfd" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
var loaded: [Note] = []
for url in files {
guard let id = UUID(uuidString: url.deletingPathExtension().lastPathComponent) else {
throw CocoaError(.fileReadCorruptFile)
}
let wrapper = try FileWrapper(url: url, options: .immediate)
guard wrapper.isDirectory else { throw CocoaError(.fileReadCorruptFile) }
let contents: NSAttributedString
if wrapper.fileWrappers?[NoteDocument.metadataFilename] != nil {
contents = try NoteDocument.contents(from: wrapper)
} else {
guard let legacy = NSAttributedString(rtfdFileWrapper: wrapper, documentAttributes: nil) else {
throw CocoaError(.fileReadCorruptFile)
}
contents = NoteDocument.importMarkdown(legacy)
legacyIDs.insert(id)
}
loaded.append(Note(id: id, contents: contents))
}
notes = loaded
}
public func create() throws -> UUID {
let note = Note(id: UUID(), contents: NSAttributedString(string: ""))
try save(note)
notes.append(note)
notes.sort { $0.id.uuidString < $1.id.uuidString }
return note.id
}
public func update(id: UUID, contents: NSAttributedString) throws {
guard let index = notes.firstIndex(where: { $0.id == id }) else {
throw CocoaError(.fileNoSuchFile)
}
let note = Note(id: id, contents: contents)
try save(note)
notes[index] = note
}
public func delete(id: UUID) throws {
guard let index = notes.firstIndex(where: { $0.id == id }) else {
throw CocoaError(.fileNoSuchFile)
}
try FileManager.default.removeItem(at: directory.appendingPathComponent(id.uuidString + ".rtfd"))
notes.remove(at: index)
legacyIDs.remove(id)
}
private func save(_ note: Note) throws {
let wrapper = try NoteDocument.fileWrapper(note.contents)
let url = directory.appendingPathComponent(note.id.uuidString + ".rtfd", isDirectory: true)
if legacyIDs.contains(note.id) {
let backups = directory.appendingPathComponent("LegacyBackups", isDirectory: true)
try FileManager.default.createDirectory(at: backups, withIntermediateDirectories: true)
let backup = backups.appendingPathComponent(note.id.uuidString + ".rtfd", isDirectory: true)
if !FileManager.default.fileExists(atPath: backup.path) {
// copyItem leaves the live legacy package untouched if backup creation fails.
try FileManager.default.copyItem(at: url, to: backup)
} else {
// A previous failed save may already have preserved the original package.
let original = try FileWrapper(url: backup, options: .immediate)
guard original.isDirectory,
NSAttributedString(rtfdFileWrapper: original, documentAttributes: nil) != nil else {
throw CocoaError(.fileWriteFileExists)
}
}
}
try wrapper.write(to: url, options: .atomic, originalContentsURL: url)
legacyIDs.remove(note.id)
}
}