Files
cairnquire/apps/macos-sync/Sources/CairnquireSync/Upload/UploadManager.swift
2026-06-01 10:53:28 -04:00

142 lines
4.7 KiB
Swift

import Foundation
import AppKit
public actor UploadManager {
private let client: APIClient
private let config: SyncConfig
public init(client: APIClient, config: SyncConfig) {
self.client = client
self.config = config
}
public func uploadFile(
_ url: URL,
duplicateAction: UploadDuplicateAction? = nil,
onProgress: UploadProgressHandler? = nil
) async throws -> UploadResult {
let response = try await client.uploadFile(
url: url,
folder: config.defaultUploadFolder,
duplicateAction: duplicateAction,
onProgress: onProgress
)
let fullURL = resolvedURL(response.url ?? response.attachmentUrl)
// Copy URL to clipboard
await MainActor.run {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(fullURL.absoluteString, forType: .string)
}
return UploadResult(
filename: url.lastPathComponent,
hash: response.hash,
url: fullURL,
copiedToClipboard: true
)
}
public func uploadData(
_ data: Data,
filename: String,
duplicateAction: UploadDuplicateAction? = nil,
onProgress: UploadProgressHandler? = nil
) async throws -> UploadResult {
let response = try await client.uploadData(
data,
filename: filename,
folder: config.defaultUploadFolder,
duplicateAction: duplicateAction,
onProgress: onProgress
)
let fullURL = resolvedURL(response.url ?? response.attachmentUrl)
// Copy URL to clipboard
await MainActor.run {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(fullURL.absoluteString, forType: .string)
}
return UploadResult(
filename: filename,
hash: response.hash,
url: fullURL,
copiedToClipboard: true
)
}
public func createNote(from text: String, title: String? = nil) async throws -> DocumentSaveResponse {
let noteTitle = title ?? generateTitle(from: text)
let folder = config.defaultUploadFolder ?? ""
let path = folder.isEmpty ? "\(sanitizeFilename(noteTitle)).md" : "\(folder)/\(sanitizeFilename(noteTitle)).md"
let content = "# \(noteTitle)\n\n\(text)"
return try await client.saveDocument(path: path, content: content)
}
private func generateTitle(from text: String) -> String {
let lines = text.split(separator: "\n", omittingEmptySubsequences: true)
if let firstLine = lines.first {
let title = String(firstLine).trimmingCharacters(in: .whitespaces)
if title.count > 60 {
return String(title.prefix(60)) + "..."
}
return title
}
return "Note \(ISO8601DateFormatter().string(from: Date()))"
}
private func sanitizeFilename(_ name: String) -> String {
let invalidChars = CharacterSet(charactersIn: ":/\\?%*|\"<>").union(.controlCharacters)
return name.components(separatedBy: invalidChars).joined(separator: "-")
}
private func resolvedURL(_ path: String) -> URL {
URL(string: path, relativeTo: client.baseURL)?.absoluteURL ?? client.baseURL.appendingPathComponent(path)
}
}
public struct UploadResult {
public let filename: String
public let hash: String
public let url: URL
public let copiedToClipboard: Bool
}
public struct UploadCancelledError: LocalizedError {
public init() {}
public var errorDescription: String? { "Upload cancelled" }
}
@MainActor
public func promptForUploadDuplicate(_ error: APIError, filename: String) -> UploadDuplicateAction? {
let alert = NSAlert()
alert.alertStyle = .warning
if error.conflictType == "document" {
alert.messageText = "A document named \(filename) already exists"
alert.informativeText = "Choose whether to replace the inbox document or keep both files."
alert.addButton(withTitle: "Keep Both")
alert.addButton(withTitle: "Replace")
alert.addButton(withTitle: "Cancel")
switch alert.runModal() {
case .alertFirstButtonReturn:
return .rename
case .alertSecondButtonReturn:
return .overwrite
default:
return nil
}
}
alert.messageText = "\(filename) was already uploaded"
alert.informativeText = "The server already has the same file content. Use its existing URL?"
alert.addButton(withTitle: "Copy Existing URL")
alert.addButton(withTitle: "Cancel")
return alert.runModal() == .alertFirstButtonReturn ? .reuse : nil
}