apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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 }