Skip to content

Instantly share code, notes, and snippets.

@piggeldi
Last active February 20, 2019 09:02
Show Gist options
  • Save piggeldi/52f6d110ee3136e13ae229af7a35275d to your computer and use it in GitHub Desktop.
Save piggeldi/52f6d110ee3136e13ae229af7a35275d to your computer and use it in GitHub Desktop.
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