Last active
February 20, 2019 09:02
-
-
Save piggeldi/52f6d110ee3136e13ae229af7a35275d to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import Foundation | |
import CoreData | |
public enum CodableStorage { | |
public enum Errors: Error { | |
case itemNotFound | |
} | |
public enum Group { | |
case `default` | |
case plugin(plugin: Plugin) | |
case offlineQueue | |
case cache | |
/// the identifier for the dataBase columns | |
var identifier: String { | |
switch self { | |
case .default: | |
return "Default" | |
case let .plugin(plugin: plugin): | |
return plugin.rawValue | |
case .offlineQueue: | |
return "offlineQueue" | |
case .cache: | |
return "Cache" | |
} | |
} | |
var searchPathDirectory: FileManager.SearchPathDirectory { | |
switch self { | |
case .cache: | |
return .cachesDirectory | |
default: | |
return .documentDirectory | |
} | |
} | |
} | |
public enum Expiry { | |
case never | |
case seconds(TimeInterval) | |
case minutes(UInt) | |
case hours(UInt) | |
case days(UInt) | |
case weeks(UInt) | |
case years(UInt) | |
var inSeconds: TimeInterval { | |
let maxLifeTimeInSeconds: TimeInterval | |
switch self { | |
case let .seconds(seconds): | |
maxLifeTimeInSeconds = seconds | |
case let .minutes(minutes): | |
maxLifeTimeInSeconds = Double(minutes) * 60 | |
case let .hours(hours): | |
maxLifeTimeInSeconds = Double(hours) * 3600 | |
case let .days(days): | |
maxLifeTimeInSeconds = Double(days) * 3600 * 24 | |
case let .weeks(weeks): | |
maxLifeTimeInSeconds = Double(weeks) * 3600 * 24 * 7 | |
case let .years(years): | |
maxLifeTimeInSeconds = Double(years) * 3600 * 24 * 7 * 365 | |
case .never: | |
maxLifeTimeInSeconds = Double(5) * 3600 * 24 * 7 * 365 // 5 Years, lol | |
} | |
return maxLifeTimeInSeconds | |
} | |
} | |
private var fileManager: FileManager { | |
return FileManager.default | |
} | |
// MARK: - Cases - | |
case `default` | |
// MARK: - Public methods - | |
/// Stores the entity ansychron in the coreData | |
/// | |
/// - Parameters: | |
/// - entity: entity to store | |
/// - name: name of the entity | |
/// - group: optional group. | |
@discardableResult | |
public func store<Entity: Codable>(entity: Entity, for name: String, in group: Group = .default) -> Bool { | |
let jsonEncoder = JSONEncoder() | |
do { | |
let jsonData = try jsonEncoder.encode(entity) | |
let fileURL = self.fileURL(itemName: name, group: group) | |
if fileManager.fileExists(atPath: fileURL.path) { | |
try fileManager.removeItem(at: fileURL) | |
} | |
fileManager.createFile(atPath: fileURL.path, contents: jsonData, attributes: nil) | |
return true | |
} catch let error { | |
Log.error("Failed to encode entity" + String(describing: Entity.self) + ".Error: " + error.localizedDescription) | |
} | |
return false | |
} | |
/// Fetch the item from the storage with completion block. The completion does get called everytime. When we found some data, we the value in completion is set else nil | |
/// | |
/// - Parameters: | |
/// - type: Codable type | |
/// - name: name of the type | |
/// - group: group | |
/// - expiry: expiry of the data | |
public func item<Entity: Codable>(type _: Entity.Type, name: String, group: Group, expiredAfter expiry: Expiry = .never) -> Entity? { | |
guard let jsonData = fetchData(itemName: name, group: group) else { | |
Log.info("Item \(String(describing: Entity.self)) not found in group \(group)") | |
return nil | |
} | |
guard isFileExpired(itemName: name, group: group, expiry: expiry) else { | |
Log.info("Item in group \(group) has expired") | |
remove(itemName: name, in: group) | |
return nil | |
} | |
let jsonDecode = JSONDecoder() | |
do { | |
let entity: Entity = try jsonDecode.decode(Entity.self, from: jsonData) | |
return entity | |
} catch let error { | |
Log.error("Failed to decode entity " + String(describing: Entity.self) + ". Error: " + error.localizedDescription) | |
return nil | |
} | |
} | |
/// Checks if the given item is expired. | |
/// | |
/// - Parameters: | |
/// - _: type | |
/// - name: name | |
/// - group: group to check | |
/// - expiry: expiry time to check | |
/// - Returns: true=expired / false=expired | |
/// - Throws: Error when item not found | |
public func isItemExpired<Entity: Codable>(type _: Entity.Type, name: String, group: Group, expiredAfter expiry: Expiry) throws -> Bool { | |
return isFileExpired(itemName: name, group: group, expiry: expiry) | |
} | |
/// Fetch the item from the storage with completion block. The completion does get called everytime. When we found some data, we the value in completion is set else nil | |
/// | |
/// - Parameters: | |
/// - type: Codable type | |
/// - name: name of the type | |
/// - group: group | |
/// - completion: completion with the restored item | |
/// - expiry: expiry of the data | |
public func item<Entity: Codable>(type: Entity.Type, name: String) -> Entity? { | |
return item(type: type, name: name, group: .default) | |
} | |
/// Removes the item from the store | |
/// | |
/// - Parameters: | |
/// - itemName: name of the item to remove | |
/// - group: group of the item | |
@discardableResult | |
public func remove(itemName: String, in group: Group = .default) -> Bool { | |
let fileURL = self.fileURL(itemName: itemName, group: group) | |
if fileManager.fileExists(atPath: fileURL.path) { | |
do { | |
try fileManager.removeItem(at: fileURL) | |
} catch let error { | |
Log.error(error.localizedDescription) | |
} | |
} | |
return true | |
} | |
public func items<Entity: Codable>(type _: Entity.Type, group: Group) -> [Entity] { | |
var items = [Entity]() | |
do { | |
let fileURLs = try fileManager.contentsOfDirectory(at: groupURL(group: group), includingPropertiesForKeys: nil, options: []) | |
for fileURL in fileURLs { | |
if fileURL.lastPathComponent.starts(with: group.identifier) { | |
guard fileManager.fileExists(atPath: fileURL.path) else { | |
continue | |
} | |
guard let JSONData = fileManager.contents(atPath: fileURL.path) else { | |
continue | |
} | |
let jsonDecode = JSONDecoder() | |
let entity: Entity = try jsonDecode.decode(Entity.self, from: JSONData) | |
items.append(entity) | |
} | |
} | |
} catch let error { | |
Log.error(error.localizedDescription) | |
} | |
return items | |
} | |
public func truncate(group: Group) { | |
do { | |
let fileURLs = try fileManager.contentsOfDirectory(at: groupURL(group: group), includingPropertiesForKeys: nil, options: []) | |
for fileURL in fileURLs { | |
if fileURL.lastPathComponent.starts(with: group.identifier) { | |
try fileManager.removeItem(at: fileURL) | |
} | |
} | |
} catch let error { | |
Log.error(error.localizedDescription) | |
} | |
} | |
/// Deletes all data which was stored by the CodableStorage | |
func truncateStorage() { | |
let searchPaths = [ | |
FileManager.SearchPathDirectory.documentDirectory, | |
FileManager.SearchPathDirectory.cachesDirectory | |
] | |
searchPaths.forEach { searchPath in | |
let files = fileManager.urls(for: searchPath, in: .userDomainMask) | |
files.forEach { groupURL in | |
do { | |
try fileManager.contentsOfDirectory(at: groupURL, includingPropertiesForKeys: nil, options: FileManager.DirectoryEnumerationOptions.skipsHiddenFiles) | |
.forEach { fileURL in | |
if fileManager.fileExists(atPath: fileURL.path) && fileURL.pathExtension == "json" { | |
try fileManager.removeItem(at: fileURL) | |
} | |
} | |
} catch let error { | |
Log.error(error.localizedDescription) | |
} | |
} | |
} | |
Log.info("Successfull truncate CodableStorage") | |
} | |
//Method to migrate from coreData | |
func store(itemName: String, groupName: String, data: NSData?) { | |
//Determine the group | |
let fileName = groupName + itemName + ".json" | |
var searchDirectory = FileManager.SearchPathDirectory.documentDirectory | |
if groupName == "Cache" { | |
searchDirectory = .cachesDirectory | |
} | |
guard let JSONData = data as Data? else { | |
return | |
} | |
guard var fileURL = fileManager.urls(for: searchDirectory, in: .userDomainMask).first else { | |
fatalError("Could not create URL for specified directory!") | |
} | |
fileURL.appendPathComponent(fileName, isDirectory: false) | |
do { | |
if fileManager.fileExists(atPath: fileURL.path) { | |
try fileManager.removeItem(at: fileURL) | |
} | |
fileManager.createFile(atPath: fileURL.path, contents: JSONData, attributes: nil) | |
} catch let error { | |
Log.error(error.localizedDescription) | |
} | |
} | |
/// Checks if the given item have been expired | |
/// | |
/// - Parameters: | |
/// - item: item to check | |
/// - expiry: expiry time | |
/// - Returns: true = expired/false other | |
private func isExpired(item: CodableEntity, expiry: Expiry) -> Bool { | |
guard let dateAdded = item.addedDate as Date? else { | |
Log.error("CodableEntity with out addedDate. Corrupt data?") | |
return true | |
} | |
return Date() >= dateAdded.addingTimeInterval(expiry.inSeconds) | |
} | |
private func isFileExpired(itemName: String, group: Group, expiry: Expiry) -> Bool { | |
let fileURL = self.fileURL(itemName: itemName, group: group) | |
var _fileDate: Date? | |
do { | |
if fileManager.fileExists(atPath: fileURL.path) == false { | |
return true | |
} | |
let attributes = try fileManager.attributesOfFileSystem(forPath: fileURL.path) | |
if let modificationDate = attributes[FileAttributeKey.creationDate] as? Date { | |
_fileDate = modificationDate | |
} else if let creationDate = attributes[FileAttributeKey.modificationDate] as? Date { | |
_fileDate = creationDate | |
} | |
} catch let error { | |
Log.error(error.localizedDescription) | |
} | |
guard let fileDate = _fileDate else { | |
return true | |
} | |
return Date() >= fileDate.addingTimeInterval(expiry.inSeconds) | |
} | |
private func fileURL(itemName: String, group: Group) -> URL { | |
let fileName = group.identifier + itemName + ".json" | |
return self.groupURL(group: group).appendingPathComponent(fileName, isDirectory: false) | |
} | |
private func groupURL(group: Group) -> URL { | |
guard let directoryURL = fileManager.urls(for: group.searchPathDirectory, in: .userDomainMask).first else { | |
fatalError("Could not create URL for specified directory!") | |
} | |
return directoryURL | |
} | |
private func fetchData(itemName: String, group: Group) -> Data? { | |
let fileURL = self.fileURL(itemName: itemName, group: group) | |
if fileManager.fileExists(atPath: fileURL.path) == false { | |
return nil | |
} | |
return fileManager.contents(atPath: fileURL.path) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment