Update Services, Utilities

Summary:
- Sources: update Services, Utilities

Stats:
- 3 files changed, 82 insertions(+), 68 deletions(-)
This commit is contained in:
Matt Bruce 2026-01-14 16:13:02 -06:00
parent deff5abd21
commit 71e142f36d
3 changed files with 82 additions and 68 deletions

View File

@ -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 {

View File

@ -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
}
}

View 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
}
}