RadrootsFileAccess.swift (5529B)
1 import Darwin 2 import Foundation 3 4 public enum RadrootsFileScope: Sendable, Equatable, CaseIterable, Codable { 5 case data 6 case cache 7 case temporary 8 case logs 9 } 10 11 public struct RadrootsFileReference: Sendable, Equatable, Hashable, Codable { 12 public let scope: RadrootsFileScope 13 public let relativePath: String 14 15 public init(scope: RadrootsFileScope, relativePath: String) { 16 self.scope = scope 17 self.relativePath = relativePath 18 } 19 } 20 21 public struct RadrootsFileEntry: Sendable, Equatable, Hashable { 22 public let file: RadrootsFileReference 23 public let name: String 24 public let isDirectory: Bool 25 public let sizeBytes: Int? 26 public let modifiedAt: Date? 27 28 public init( 29 file: RadrootsFileReference, 30 name: String, 31 isDirectory: Bool, 32 sizeBytes: Int?, 33 modifiedAt: Date? 34 ) { 35 self.file = file 36 self.name = name 37 self.isDirectory = isDirectory 38 self.sizeBytes = sizeBytes 39 self.modifiedAt = modifiedAt 40 } 41 } 42 43 public struct RadrootsStagedBlobReference: Sendable, Equatable, Hashable, Codable { 44 public let blobID: String 45 public let sizeBytes: Int 46 public let mediaType: String? 47 public let filenameHint: String? 48 49 public init( 50 blobID: String, 51 sizeBytes: Int, 52 mediaType: String? = nil, 53 filenameHint: String? = nil 54 ) throws { 55 let normalizedBlobID = try Self.normalizedBlobID(blobID) 56 guard sizeBytes >= 0 else { 57 throw RadrootsAppleFileError.invalidRequest 58 } 59 self.blobID = normalizedBlobID 60 self.sizeBytes = sizeBytes 61 self.mediaType = try Self.normalizedMediaType(mediaType) 62 self.filenameHint = try Self.normalizedFilenameHint(filenameHint) 63 } 64 65 public static func normalizedBlobID(_ blobID: String) throws -> String { 66 let trimmed = blobID.trimmingCharacters(in: .whitespacesAndNewlines) 67 guard !trimmed.isEmpty else { 68 throw RadrootsAppleFileError.invalidRequest 69 } 70 let allowed = CharacterSet( 71 charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" 72 ) 73 guard trimmed.rangeOfCharacter(from: allowed.inverted) == nil else { 74 throw RadrootsAppleFileError.invalidRequest 75 } 76 return trimmed 77 } 78 79 public static func normalizedMediaType(_ mediaType: String?) throws -> String? { 80 guard let mediaType else { 81 return nil 82 } 83 let trimmed = mediaType.trimmingCharacters(in: .whitespacesAndNewlines) 84 guard !trimmed.isEmpty else { 85 throw RadrootsAppleFileError.invalidRequest 86 } 87 guard trimmed.rangeOfCharacter(from: .newlines) == nil else { 88 throw RadrootsAppleFileError.invalidRequest 89 } 90 return trimmed 91 } 92 93 public static func normalizedFilenameHint(_ filenameHint: String?) throws -> String? { 94 guard let filenameHint else { 95 return nil 96 } 97 let trimmed = filenameHint.trimmingCharacters(in: .whitespacesAndNewlines) 98 guard !trimmed.isEmpty else { 99 throw RadrootsAppleFileError.invalidRequest 100 } 101 guard !trimmed.contains("/"), !trimmed.contains("\\"), !trimmed.contains("\0") else { 102 throw RadrootsAppleFileError.invalidRequest 103 } 104 return trimmed 105 } 106 } 107 108 public enum RadrootsFilePayload: Sendable, Equatable { 109 case inline(Data) 110 case stagedBlob(RadrootsStagedBlobReference) 111 } 112 113 public enum RadrootsFileReadMode: Sendable, Equatable { 114 case inline(maxBytes: Int) 115 case preferInline(maxBytes: Int) 116 case stagedBlob 117 } 118 119 public enum RadrootsFileReadResult: Sendable, Equatable { 120 case inline(Data) 121 case stagedBlob(RadrootsStagedBlobReference) 122 } 123 124 public protocol RadrootsFileAccess { 125 func write(_ payload: RadrootsFilePayload, to file: RadrootsFileReference) throws 126 func read(_ file: RadrootsFileReference, mode: RadrootsFileReadMode) throws 127 -> RadrootsFileReadResult 128 func delete(_ file: RadrootsFileReference) throws 129 func list(_ directory: RadrootsFileReference) throws -> [RadrootsFileEntry] 130 func reset(scope: RadrootsFileScope) throws 131 @discardableResult func stageBlob(_ data: Data, mediaType: String?, filenameHint: String?) throws 132 -> RadrootsStagedBlobReference 133 @discardableResult func stageFile( 134 _ file: RadrootsFileReference, mediaType: String?, filenameHint: String? 135 ) throws -> RadrootsStagedBlobReference 136 @discardableResult func stageExternalFile( 137 _ sourceURL: URL, mediaType: String?, filenameHint: String? 138 ) throws -> RadrootsStagedBlobReference 139 @discardableResult func copyExternalFile( 140 _ sourceURL: URL, 141 to file: RadrootsFileReference, 142 mediaType: String?, 143 suggestedFilename: String? 144 ) throws -> RadrootsImportedDocument 145 @discardableResult func prepareExport(_ request: RadrootsExportDocumentRequest) throws 146 -> RadrootsPreparedExportDocument 147 func preparedExportExists(_ preparedExport: RadrootsPreparedExportDocument) throws -> Bool 148 func readStagedBlob(_ blob: RadrootsStagedBlobReference) throws -> Data 149 func releaseStagedBlob(_ blob: RadrootsStagedBlobReference) throws 150 func releasePreparedExport(_ preparedExport: RadrootsPreparedExportDocument) throws 151 @discardableResult func sweepStagedBlobs(olderThan cutoff: Date) throws 152 -> [RadrootsStagedBlobReference] 153 func resetStagedBlobs() throws 154 }