2025-08-30 21:52:30 -06:00
|
|
|
//
|
|
|
|
|
// Attachments.swift
|
|
|
|
|
// kordophone2
|
|
|
|
|
//
|
|
|
|
|
// Created by Assistant on 8/31/25.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import UniformTypeIdentifiers
|
|
|
|
|
|
|
|
|
|
enum AttachmentState
|
|
|
|
|
{
|
|
|
|
|
case staged
|
|
|
|
|
case uploading(progress: Double)
|
2025-09-03 22:38:26 -07:00
|
|
|
case uploaded(String) // fileTransferGUID
|
2025-08-30 21:52:30 -06:00
|
|
|
case failed
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 22:38:26 -07:00
|
|
|
@Observable
|
|
|
|
|
class OutgoingAttachment: Identifiable, Hashable
|
2025-08-30 21:52:30 -06:00
|
|
|
{
|
|
|
|
|
let id: String
|
|
|
|
|
let originalURL: URL
|
|
|
|
|
let stagedURL: URL
|
|
|
|
|
let fileName: String
|
|
|
|
|
let contentType: UTType?
|
|
|
|
|
var state: AttachmentState
|
|
|
|
|
|
|
|
|
|
init(id: String = UUID().uuidString,
|
|
|
|
|
originalURL: URL,
|
|
|
|
|
stagedURL: URL,
|
|
|
|
|
fileName: String? = nil,
|
|
|
|
|
contentType: UTType? = nil,
|
|
|
|
|
state: AttachmentState = .staged)
|
|
|
|
|
{
|
|
|
|
|
self.id = id
|
|
|
|
|
self.originalURL = originalURL
|
|
|
|
|
self.stagedURL = stagedURL
|
|
|
|
|
self.fileName = fileName ?? originalURL.lastPathComponent
|
|
|
|
|
self.contentType = contentType
|
|
|
|
|
self.state = state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func ==(lhs: OutgoingAttachment, rhs: OutgoingAttachment) -> Bool {
|
|
|
|
|
return lhs.id == rhs.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
|
|
|
hasher.combine(id)
|
|
|
|
|
hasher.combine(originalURL)
|
|
|
|
|
hasher.combine(stagedURL)
|
|
|
|
|
hasher.combine(fileName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class AttachmentManager
|
|
|
|
|
{
|
|
|
|
|
static let shared = AttachmentManager()
|
|
|
|
|
|
|
|
|
|
private let fileManager = FileManager.default
|
|
|
|
|
private let stagingRoot: URL
|
|
|
|
|
|
|
|
|
|
private init() {
|
|
|
|
|
let base = fileManager.temporaryDirectory.appendingPathComponent("kordophone-staging", isDirectory: true)
|
|
|
|
|
try? fileManager.createDirectory(at: base, withIntermediateDirectories: true)
|
|
|
|
|
self.stagingRoot = base
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stageIfImage(url: URL) -> OutgoingAttachment? {
|
|
|
|
|
guard isImage(url: url) else { return nil }
|
|
|
|
|
let staged = stage(url: url)
|
|
|
|
|
let type = (try? url.resourceValues(forKeys: [.contentTypeKey]))?.contentType
|
|
|
|
|
?? UTType(filenameExtension: url.pathExtension)
|
|
|
|
|
return OutgoingAttachment(originalURL: url, stagedURL: staged, fileName: url.lastPathComponent, contentType: type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Helpers
|
|
|
|
|
|
|
|
|
|
private func isImage(url: URL) -> Bool {
|
|
|
|
|
if let type = (try? url.resourceValues(forKeys: [.contentTypeKey]))?.contentType {
|
|
|
|
|
return type.conforms(to: .image)
|
|
|
|
|
}
|
|
|
|
|
return UTType(filenameExtension: url.pathExtension)?.conforms(to: .image) ?? false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func stage(url: URL) -> URL {
|
|
|
|
|
let ext = url.pathExtension
|
|
|
|
|
let dest = stagingRoot.appendingPathComponent(UUID().uuidString).appendingPathExtension(ext)
|
|
|
|
|
do {
|
|
|
|
|
if fileManager.fileExists(atPath: dest.path) { try? fileManager.removeItem(at: dest) }
|
|
|
|
|
try fileManager.copyItem(at: url, to: dest)
|
|
|
|
|
return dest
|
|
|
|
|
} catch {
|
|
|
|
|
do {
|
|
|
|
|
try fileManager.moveItem(at: url, to: dest)
|
|
|
|
|
return dest
|
|
|
|
|
} catch {
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|