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