Skip to content

Instantly share code, notes, and snippets.

@khanlou
Last active November 22, 2024 16:01
Show Gist options
  • Save khanlou/d5328cf31fe681e027385d75ef335d13 to your computer and use it in GitHub Desktop.
Save khanlou/d5328cf31fe681e027385d75ef335d13 to your computer and use it in GitHub Desktop.
//
// ObjectStorage.swift
// Chet
//
// Created by Soroush Khanlou on 3/8/19.
// Copyright © 2019 Hirsh Group LLC. All rights reserved.
//
import Foundation
protocol ObjectStorageProtocol {
associatedtype Stored
init(location: StorageLocation)
func save(object: Stored)
func fetchObject() -> Stored?
func deleteObject()
}
final class CodableObjectStorage<T: Codable>: ObjectStorageProtocol {
private let fileManager = FileManager.default
let storageLocation: StorageLocation
var filePresenter: NSFilePresenter
init(location: StorageLocation) {
self.storageLocation = location
self.filePresenter = InnerFilePresenter(presentedItemURL: storageLocation.storageLocation)
}
func save(object: T) {
guard let storageURL = storageURL else { return }
var error: NSError?
NSFileCoordinator(filePresenter: filePresenter).coordinate(writingItemAt: storageURL, options: .forReplacing, error: &error, byAccessor: { url in
do {
try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
let encoder = JSONEncoder()
let data = try encoder.encode(object)
try data.write(to: url)
} catch {
print("Error writing the object: \(error)")
}
})
}
func fetchObject() -> T? {
var error: NSError?
var result: T?
guard let storageURL = storageURL else { return nil }
NSFileCoordinator(filePresenter: filePresenter).coordinate(readingItemAt: storageURL, options: .withoutChanges, error: &error, byAccessor: { url in
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
result = try decoder.decode(T.self, from: data)
} catch {
if (error as NSError).code != NSFileReadNoSuchFileError {
print("Error reading the object: \(error)")
}
}
})
return result
}
func mutatingObject(default defaultAutoclosure: @autoclosure () -> T, _ block: (inout T) -> Void) {
guard let storageURL = storageURL else { return }
var error: NSError?
NSFileCoordinator(filePresenter: filePresenter).coordinate(writingItemAt: storageURL, options: .forMerging, error: &error, byAccessor: { url in
var result: T
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
result = try decoder.decode(T.self, from: data)
} catch {
if (error as NSError).code == NSFileReadNoSuchFileError {
result = defaultAutoclosure()
} else {
print("Error fetching for mutation: \(error)")
return
}
}
block(&result)
do {
try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
let encoder = JSONEncoder()
let data = try encoder.encode(result)
try data.write(to: url)
} catch {
print("Error writing the object for mutation: \(error)")
}
})
}
func deleteObject() {
var error: NSError?
guard let storageURL = storageURL else { return }
NSFileCoordinator(filePresenter: filePresenter).coordinate(writingItemAt: storageURL, options: .forDeleting, error: &error, byAccessor: { url in
do {
try fileManager.removeItem(at: url)
} catch {
guard (error as NSError).code != NSFileReadNoSuchFileError else { return }
print("Error deleting object file.")
}
})
}
private var storageURL: URL? {
return storageLocation.storageLocation
}
class InnerFilePresenter: NSObject, NSFilePresenter {
var presentedItemURL: URL?
var presentedItemOperationQueue: OperationQueue
init(presentedItemURL: URL?) {
self.presentedItemURL = presentedItemURL
self.presentedItemOperationQueue = OperationQueue()
}
}
}
final class DisabledObjectStorage<T>: ObjectStorageProtocol {
init(location: StorageLocation) {
}
func save(object: T) {
}
func fetchObject() -> T? {
return nil
}
func deleteObject() {
}
}
//
// StorageLocation.swift
//
// Created by Soroush Khanlou on 9/20/16.
//
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("urbanarchive", 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