Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
202ed6063b
commit
ab95353119
@ -30,6 +30,7 @@ actor FileStorageHelper {
|
||||
let baseURL = try appGroupContainerURL(identifier: appGroupIdentifier)
|
||||
let directoryURL = try resolveDirectoryURL(baseURL: baseURL, directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
Logger.debug("Writing to App Group file: \(url.path)")
|
||||
try ensureDirectoryExists(at: url.deletingLastPathComponent())
|
||||
try write(data, to: url, useCompleteFileProtection: useCompleteFileProtection)
|
||||
}
|
||||
@ -47,22 +48,15 @@ actor FileStorageHelper {
|
||||
fileName: String,
|
||||
useCompleteFileProtection: Bool = false
|
||||
) throws {
|
||||
let url = directory.url().appendingPathComponent(fileName)
|
||||
let directoryURL = try resolveDirectoryURL(directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
Logger.debug("Writing file: \(url.path)")
|
||||
|
||||
// Ensure directory exists
|
||||
try ensureDirectoryExists(at: url.deletingLastPathComponent())
|
||||
|
||||
// Write with appropriate options
|
||||
var options: Data.WritingOptions = [.atomic]
|
||||
if useCompleteFileProtection {
|
||||
options.insert(.completeFileProtection)
|
||||
}
|
||||
|
||||
do {
|
||||
try data.write(to: url, options: options)
|
||||
} catch {
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
try write(data, to: url, useCompleteFileProtection: useCompleteFileProtection)
|
||||
}
|
||||
|
||||
/// Reads data from a file.
|
||||
@ -75,17 +69,10 @@ actor FileStorageHelper {
|
||||
from directory: FileDirectory,
|
||||
fileName: String
|
||||
) throws -> Data? {
|
||||
let url = directory.url().appendingPathComponent(fileName)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
let directoryURL = try resolveDirectoryURL(directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
Logger.debug("Reading file: \(url.path)")
|
||||
return try read(from: url)
|
||||
}
|
||||
|
||||
/// Reads data from an App Group container.
|
||||
@ -97,6 +84,7 @@ actor FileStorageHelper {
|
||||
let baseURL = try appGroupContainerURL(identifier: appGroupIdentifier)
|
||||
let directoryURL = try resolveDirectoryURL(baseURL: baseURL, directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
Logger.debug("Reading App Group file: \(url.path)")
|
||||
return try read(from: url)
|
||||
}
|
||||
|
||||
@ -109,17 +97,9 @@ actor FileStorageHelper {
|
||||
from directory: FileDirectory,
|
||||
fileName: String
|
||||
) throws {
|
||||
let url = directory.url().appendingPathComponent(fileName)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
return // File doesn't exist, nothing to delete
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
let directoryURL = try resolveDirectoryURL(directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
try delete(file: url)
|
||||
}
|
||||
|
||||
/// Deletes a file from an App Group container.
|
||||
@ -143,8 +123,13 @@ actor FileStorageHelper {
|
||||
in directory: FileDirectory,
|
||||
fileName: String
|
||||
) -> Bool {
|
||||
let url = directory.url().appendingPathComponent(fileName)
|
||||
return FileManager.default.fileExists(atPath: url.path)
|
||||
do {
|
||||
let directoryURL = try resolveDirectoryURL(directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
return FileManager.default.fileExists(atPath: url.path)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a file exists in an App Group container.
|
||||
@ -168,17 +153,8 @@ actor FileStorageHelper {
|
||||
/// - Returns: An array of file names.
|
||||
/// - Throws: `StorageError.fileError` if listing fails.
|
||||
public func list(in directory: FileDirectory) throws -> [String] {
|
||||
let url = directory.url()
|
||||
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
return try FileManager.default.contentsOfDirectory(atPath: url.path)
|
||||
} catch {
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
let directoryURL = try resolveDirectoryURL(directory: directory)
|
||||
return try list(in: directoryURL)
|
||||
}
|
||||
|
||||
/// Lists all files in an App Group container directory.
|
||||
@ -198,18 +174,9 @@ actor FileStorageHelper {
|
||||
of directory: FileDirectory,
|
||||
fileName: String
|
||||
) throws -> Int64? {
|
||||
let url = directory.url().appendingPathComponent(fileName)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
||||
return attributes[.size] as? Int64
|
||||
} catch {
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
let directoryURL = try resolveDirectoryURL(directory: directory)
|
||||
let url = directoryURL.appendingPathComponent(fileName)
|
||||
return try size(of: url)
|
||||
}
|
||||
|
||||
/// Gets the size of a file in an App Group container.
|
||||
@ -232,12 +199,14 @@ actor FileStorageHelper {
|
||||
}
|
||||
|
||||
do {
|
||||
Logger.debug("Ensuring directory exists: \(url.path)")
|
||||
try FileManager.default.createDirectory(
|
||||
at: url,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: nil
|
||||
)
|
||||
} catch {
|
||||
Logger.error("Failed to create directory", error: error)
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
}
|
||||
@ -250,7 +219,9 @@ actor FileStorageHelper {
|
||||
|
||||
do {
|
||||
try data.write(to: url, options: options)
|
||||
Logger.debug("Successfully wrote \(data.count) bytes to \(url.lastPathComponent)")
|
||||
} catch {
|
||||
Logger.error("Failed to write to \(url.path)", error: error)
|
||||
throw StorageError.fileError(error)
|
||||
}
|
||||
}
|
||||
@ -308,21 +279,27 @@ actor FileStorageHelper {
|
||||
guard let url = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: identifier
|
||||
) else {
|
||||
Logger.error("Invalid App Group identifier: \(identifier)")
|
||||
throw StorageError.invalidAppGroupIdentifier(identifier)
|
||||
}
|
||||
Logger.debug("Resolved App Group container: \(url.path)")
|
||||
return url
|
||||
}
|
||||
|
||||
private func resolveDirectoryURL(baseURL: URL, directory: FileDirectory) throws -> URL {
|
||||
private func resolveDirectoryURL(baseURL: URL? = nil, directory: FileDirectory) throws -> URL {
|
||||
let base: URL
|
||||
switch directory {
|
||||
case .documents:
|
||||
base = baseURL.appending(path: "Documents")
|
||||
case .caches:
|
||||
base = baseURL.appending(path: "Library/Caches")
|
||||
case .custom(let url):
|
||||
let relativePath = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
return baseURL.appending(path: relativePath)
|
||||
if let baseURL = baseURL {
|
||||
switch directory {
|
||||
case .documents:
|
||||
base = baseURL.appending(path: "Documents")
|
||||
case .caches:
|
||||
base = baseURL.appending(path: "Library/Caches")
|
||||
case .custom(let url):
|
||||
let relativePath = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
return baseURL.appending(path: relativePath)
|
||||
}
|
||||
} else {
|
||||
base = directory.url()
|
||||
}
|
||||
|
||||
if let subDirectory = configuration.subDirectory {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(watchOS)
|
||||
import WatchConnectivity
|
||||
#endif
|
||||
@ -66,6 +67,7 @@ public actor StorageRouter: StorageProviding {
|
||||
/// - key: The storage key defining where and how to store.
|
||||
/// - Throws: Various errors depending on the storage domain and security policy.
|
||||
public func set<Key: StorageKey>(_ value: Key.Value, for key: Key) async throws {
|
||||
Logger.debug(">>> [STORAGE] SET: \(key.name) [Domain: \(key.domain)]")
|
||||
try validateCatalogRegistration(for: key)
|
||||
try validatePlatformAvailability(for: key)
|
||||
|
||||
@ -74,6 +76,7 @@ public actor StorageRouter: StorageProviding {
|
||||
|
||||
try await store(securedData, for: key)
|
||||
try await handleSync(key, data: securedData)
|
||||
Logger.debug("<<< [STORAGE] SET SUCCESS: \(key.name)")
|
||||
}
|
||||
|
||||
/// Retrieves a value for the given key.
|
||||
@ -81,15 +84,19 @@ public actor StorageRouter: StorageProviding {
|
||||
/// - Returns: The stored value.
|
||||
/// - Throws: `StorageError.notFound` if no value exists, plus domain-specific errors.
|
||||
public func get<Key: StorageKey>(_ key: Key) async throws -> Key.Value {
|
||||
Logger.debug(">>> [STORAGE] GET: \(key.name)")
|
||||
try validateCatalogRegistration(for: key)
|
||||
try validatePlatformAvailability(for: key)
|
||||
|
||||
guard let securedData = try await retrieve(for: key) else {
|
||||
Logger.debug("<<< [STORAGE] GET NOT FOUND: \(key.name)")
|
||||
throw StorageError.notFound
|
||||
}
|
||||
|
||||
let data = try await applySecurity(securedData, for: key, isEncrypt: false)
|
||||
return try deserialize(data, with: key.serializer)
|
||||
let result = try deserialize(data, with: key.serializer)
|
||||
Logger.debug("<<< [STORAGE] GET SUCCESS: \(key.name)")
|
||||
return result
|
||||
}
|
||||
|
||||
/// Removes the value for the given key.
|
||||
@ -352,15 +359,19 @@ public actor StorageRouter: StorageProviding {
|
||||
|
||||
private func resolveService(_ service: String?) throws -> String {
|
||||
guard let resolved = service ?? storageConfiguration.defaultKeychainService else {
|
||||
Logger.error("No keychain service provided and no default configured")
|
||||
throw StorageError.keychainError(errSecBadReq) // Or a more specific error
|
||||
}
|
||||
Logger.debug("Resolved Keychain Service: \(resolved)")
|
||||
return resolved
|
||||
}
|
||||
|
||||
private func resolveIdentifier(_ identifier: String?) throws -> String {
|
||||
guard let resolved = identifier ?? storageConfiguration.defaultAppGroupIdentifier else {
|
||||
Logger.error("No App Group identifier provided and no default configured")
|
||||
throw StorageError.invalidAppGroupIdentifier("none")
|
||||
}
|
||||
Logger.debug("Resolved App Group ID: \(resolved)")
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
|
||||
26
Sources/LocalData/Utilities/Logger.swift
Normal file
26
Sources/LocalData/Utilities/Logger.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
/// Internal logging utility for the LocalData package.
|
||||
enum Logger {
|
||||
static var isLoggingEnabled = true
|
||||
|
||||
static func debug(_ message: String) {
|
||||
#if DEBUG
|
||||
if isLoggingEnabled {
|
||||
print(" {LOCAL_DATA} ℹ️ \(message)")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static func error(_ message: String, error: Error? = nil) {
|
||||
#if DEBUG
|
||||
var logMessage = " {LOCAL_DATA} ❌ \(message)"
|
||||
if let error = error {
|
||||
logMessage += " | Error: \(error.localizedDescription)"
|
||||
}
|
||||
if isLoggingEnabled {
|
||||
print(logMessage)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user