Created
April 1, 2019 21:02
-
-
Save khanlou/a60471d59f57f2b6210daa50772823b2 to your computer and use it in GitHub Desktop.
ObjectStorage 3.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ObjectStorage.swift | |
// Soroush Khanlou | |
// | |
// Created by Soroush Khanlou on 3/8/19. | |
// Copyright © 2019 Soroush Khanlou. All rights reserved. | |
// | |
import Foundation | |
enum ErrorStrategy { | |
case ignore | |
case crash | |
case log | |
func handle(_ message: String) { | |
switch self { | |
case .crash: | |
fatalError(message) | |
case .log: | |
print(message) | |
case .ignore: | |
break | |
} | |
} | |
} | |
final class FileCoordinator { | |
private static let queueAccessQueue = DispatchQueue(label: "queueAccessQueue") | |
private static var queues: [URL: DispatchQueue] = [:] | |
private func queue(for url: URL) -> DispatchQueue { | |
return FileCoordinator.queueAccessQueue.sync { () -> DispatchQueue in | |
if let queue = FileCoordinator.queues[url] { return queue } | |
let queue = DispatchQueue(label: "queue-for-\(url)", attributes: .concurrent) | |
FileCoordinator.queues[url] = queue | |
return queue | |
} | |
} | |
func coordinateReading<T>(at url: URL, _ block: (URL) throws -> T) rethrows -> T { | |
return try queue(for: url).sync { | |
return try block(url) | |
} | |
} | |
func coordinateWriting(at url: URL, _ block: (URL) throws -> Void) rethrows { | |
try queue(for: url).sync(flags: .barrier) { | |
try block(url) | |
} | |
} | |
} | |
final class CodableObjectStorage<T: Codable> { | |
private let fileManager = FileManager.default | |
let storageLocation: StorageLocation | |
init(location: StorageLocation) { | |
self.storageLocation = location | |
} | |
func save(object: T, codingFailedStrategy: ErrorStrategy = .log, writingFailedStrategy: ErrorStrategy = .log) { | |
guard let storageURL = storageURL else { return } | |
FileCoordinator().coordinateWriting(at: storageURL, { url in | |
_save(object: object, at: url, codingFailedStrategy: codingFailedStrategy, writingFailedStrategy: writingFailedStrategy) | |
}) | |
} | |
func fetchObject(fileNotFoundStrategy: ErrorStrategy = .ignore, otherReadingErrorStrategy: ErrorStrategy = .log, codingFailedStrategy: ErrorStrategy = .log) -> T? { | |
guard let storageURL = storageURL else { return nil } | |
return FileCoordinator().coordinateReading(at: storageURL, { url in | |
_fetchObject(at: url, fileNotFoundStrategy: fileNotFoundStrategy, otherReadingErrorStrategy: otherReadingErrorStrategy, codingFailedStrategy: codingFailedStrategy) | |
}) | |
} | |
func mutatingObject(fileNotFoundStrategy: ErrorStrategy = .ignore, otherReadingErrorStrategy: ErrorStrategy = .log, codingFailedStrategy: ErrorStrategy = .log, writeFailedStrategy: ErrorStrategy = .log, default defaultAutoclosure: @autoclosure () -> T, _ block: (inout T) -> Void) { | |
guard let storageURL = storageURL else { return } | |
FileCoordinator().coordinateWriting(at: storageURL, { url in | |
var result = _fetchObject(at: url, fileNotFoundStrategy: fileNotFoundStrategy, otherReadingErrorStrategy: otherReadingErrorStrategy, codingFailedStrategy: codingFailedStrategy) ?? defaultAutoclosure() | |
block(&result) | |
_save(object: result, at: url, codingFailedStrategy: codingFailedStrategy, writingFailedStrategy: writeFailedStrategy) | |
}) | |
} | |
func deleteObject() { | |
guard let storageURL = storageURL else { return } | |
FileCoordinator().coordinateWriting(at: storageURL, { url in | |
do { | |
try fileManager.removeItem(at: url) | |
} catch { | |
guard (error as NSError).code != NSFileReadNoSuchFileError else { return } | |
print("Error deleting object file.") | |
} | |
}) | |
} | |
private func _save(object: T, at url: URL, codingFailedStrategy: ErrorStrategy = .log, writingFailedStrategy: ErrorStrategy = .log) { | |
do { | |
try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) | |
let encoder = JSONEncoder() | |
let data = try encoder.encode(object) | |
try data.write(to: url) | |
} catch let error as DecodingError { | |
codingFailedStrategy.handle("Decoding object failed: \(error)") | |
} catch { | |
writingFailedStrategy.handle("Error reading the object: \(error)") | |
} | |
} | |
private func _fetchObject(at url: URL, fileNotFoundStrategy: ErrorStrategy = .ignore, otherReadingErrorStrategy: ErrorStrategy = .log, codingFailedStrategy: ErrorStrategy = .log) -> T? { | |
do { | |
let data = try Data(contentsOf: url) | |
let decoder = JSONDecoder() | |
return try decoder.decode(T.self, from: data) | |
} catch CocoaError.fileNoSuchFile { | |
fileNotFoundStrategy.handle("File not found at url: \(url)") | |
} catch let error as DecodingError { | |
codingFailedStrategy.handle("Decoding object failed: \(error)") | |
} catch { | |
otherReadingErrorStrategy.handle("Error reading the object: \(error)") | |
} | |
return nil | |
} | |
private func _deleteObject(at url: URL, fileNotFoundStrategy: ErrorStrategy = .ignore, deletingFailedStrategy: ErrorStrategy = .log) { | |
do { | |
try fileManager.removeItem(at: url) | |
} catch CocoaError.fileNoSuchFile { | |
fileNotFoundStrategy.handle("File not found at url: \(url)") | |
} catch { | |
deletingFailedStrategy.handle("Error deleting the object: \(error)") | |
} | |
} | |
private var storageURL: URL? { | |
return storageLocation.storageLocation | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// StorageLocation.swift | |
// Soroush Khanlou | |
// | |
// Created by Soroush Khanlou on 3/8/19. | |
// Copyright © 2019 Soroush Khanlou. All rights reserved. | |
// | |
import Foundation | |
enum StorageLocation { | |
case cache(name: String) | |
case userData(name: String) | |
var name: String { | |
switch self { | |
case .cache(let name): | |
return name | |
case .userData(let name): | |
return name | |
} | |
} | |
var searchPathDirectory: FileManager.SearchPathDirectory { | |
switch self { | |
case .cache: | |
return .cachesDirectory | |
case .userData: | |
return .libraryDirectory | |
} | |
} | |
var fileExtension: String { | |
switch self { | |
case .cache: | |
return ".cache" | |
case .userData: | |
return ".userData" | |
} | |
} | |
var fileManager: FileManager { | |
return FileManager.default | |
} | |
var path: String { | |
return storageLocation?.path ?? "" | |
} | |
var storageLocation: URL? { | |
return appStorageDirectory?.appendingPathComponent(filename) | |
} | |
var appStorageDirectory: URL? { | |
return generalStoreDirectory?.appendingPathComponent("objectstorage", isDirectory: true) | |
} | |
private var generalStoreDirectory: URL? { | |
let URLs = fileManager.urls(for: searchPathDirectory, in: .userDomainMask) | |
return URLs.first | |
} | |
private var filename: String { | |
return sanitizedName + fileExtension | |
} | |
private var sanitizedName: String { | |
let invalidFilenameCharacters = CharacterSet(charactersIn: ":/\\?%*|\"<>") | |
let validName = name.components(separatedBy: invalidFilenameCharacters).joined() | |
return validName.replacingOccurrences(of: " ", with: "-") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment