Skip to content

Instantly share code, notes, and snippets.

@khanlou
Created April 1, 2019 21:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save khanlou/a60471d59f57f2b6210daa50772823b2 to your computer and use it in GitHub Desktop.
Save khanlou/a60471d59f57f2b6210daa50772823b2 to your computer and use it in GitHub Desktop.
ObjectStorage 3.0
//
// 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
}
}
//
// 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