Skip to content

Commit

Permalink
Merge pull request #10 from mergesort/2.0
Browse files Browse the repository at this point in the history
BYOD: Bring Your Own Database
  • Loading branch information
mergesort authored Jul 27, 2022
2 parents 1b72eef + 594d120 commit 9cd6eec
Show file tree
Hide file tree
Showing 136 changed files with 1,521 additions and 162 deletions.
23 changes: 23 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"pins" : [
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift.git",
"state" : {
"revision" : "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54",
"version" : "0.13.3"
}
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
"version" : "1.0.0"
}
}
],
"version" : 2
}
8 changes: 6 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.5
// swift-tools-version:5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -16,11 +16,15 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/stephencelis/SQLite.swift.git", exact: Version(0, 13, 3)),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(
name: "Bodega",
dependencies: []),
dependencies: [
.productItem(name: "SQLite", package: "SQLite.swift", condition: nil)
]),
.testTarget(
name: "BodegaTests",
dependencies: ["Bodega"]),
Expand Down
13 changes: 11 additions & 2 deletions Sources/Bodega/CacheKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public struct CacheKey: Codable, Equatable, Hashable {
/// The `String` representation of your `CacheKey`.
public let value: String

/// The `String` that was passed in to any initializer, regardless of whether it was hashed afterwards or not.
/// Currently this is used in `StorageEngine`s that are not file system based
/// and will be deprecated in the future, when `CacheKey` is [deprecated](https://github.com/mergesort/Bodega/issues/9).
internal let rawValue: String

/// Initializes a `CacheKey` from a `URL`. This initializer is useful if you plan on using
/// `CacheKey`s for storing files on disk because file have many limitations about
/// which characters that are allowed in file names, and the maximum length of a file name.
Expand All @@ -17,8 +22,10 @@ public struct CacheKey: Codable, Equatable, Hashable {
/// before generating a cache key, so note that https://redpanda.club and https://www.redpanda.club
/// will generate a `CacheKey` with the same underlying value.
public init(url: URL) {
self.rawValue = url.absoluteString

let md5HashedURLString = Self.sanitizedURLString(url).md5
self.init(verbatim: md5HashedURLString.uuidFormatted ?? md5HashedURLString)
self.value = md5HashedURLString.uuidFormatted ?? md5HashedURLString
}

/// Initializes a `CacheKey` from a `String`, creating a hashed version of the input `String`.
Expand All @@ -27,12 +34,14 @@ public struct CacheKey: Codable, Equatable, Hashable {
/// and the maximum length of a file name.
/// - Parameter value: The `String` which will serve as the underlying value for this `CacheKey`.
public init(_ value: String) {
self.init(verbatim: value.md5.uuidFormatted ?? value.md5)
self.rawValue = value
self.value = value.md5.uuidFormatted ?? value.md5
}

/// Initializes a `CacheKey` from a `String`, using the exact `String` as the value of the `CacheKey`.
/// - Parameter value: The `String` which will serve as the underlying value for this `CacheKey`.
public init(verbatim value: String) {
self.rawValue = value
self.value = value
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import Foundation

public actor DiskStorage {
public actor DiskStorageEngine: StorageEngine {

private let folder: URL
/// A directory on the filesystem where your `StorageEngine`s data will be stored.
public var directory: FileManager.Directory

/// Initializes a new `DiskStorage` object for persisting `Data` to disk.
/// - Parameter storagePath: A URL representing the folder on disk that your files will be written to.
/// Constructed as a URL for those that wish to use features like shared containers, rather than as traditionally in the Documents or Caches directory.
/// Initializes a new `DiskStorageEngine` for persisting `Data` to disk.
/// - Parameter directory: A directory on the filesystem where your files will be written to.
/// `FileManager.Directory` is a type-safe wrapper around URL that provides sensible defaults like
/// `.documents(appendingPath:)`, `.caches(appendingPath:)`, and more.
public init(directory: FileManager.Directory) {
self.folder = directory.url
self.directory = directory
}

/// Writes `Data` to disk based on the associated `CacheKey`.
Expand Down Expand Up @@ -44,15 +46,6 @@ public actor DiskStorage {
return try? Data(contentsOf: self.concatenatedPath(key: key.value))
}

/// Reads `Data` items from disk based on the associated array of `CacheKey`s provided as a parameter.
/// - Parameters:
/// - keys: A `[CacheKey]` for matching multiple `Codable` objects to their a location on disk.
/// - Returns: An array of `[Data]` stored on disk if the `CacheKey`s exist,
/// and an `[]` if there is no `Data` matching the `keys` passed in.
public func read(keys: [CacheKey]) -> [Data] {
return keys.compactMap({ self.read(key: $0) })
}

/// Reads `Data` from disk based on the associated array of `CacheKey`s provided as a parameter
/// and returns an array `[(CacheKey, Data)]` associated with the passed in `CacheKey`s.
///
Expand All @@ -72,14 +65,12 @@ public actor DiskStorage {
).map { ($0, $1) }
}

/// Reads all the `[Data]` located at the `storagePath`.
/// - Returns: An array of the `[Data]` contained in a directory.
public func readAllData() -> [Data] {
let allKeys = self.allKeys()
return self.read(keys: allKeys)
}

/// Reads all the `Data` located at the `storagePath` and returns an array
/// Reads all the `Data` located in the `directory` and returns an array
/// of `[(CacheKey, Data)]` tuples associated with the `CacheKey`.
///
/// This method returns the `CacheKey` and `Data` together in an array of `[(CacheKey, Data)]`
Expand All @@ -106,37 +97,35 @@ public actor DiskStorage {
}
}

/// Removes `Data` items from disk based on the associated array of `[CacheKey]`s provided as a parameter.
/// - Parameters:
/// - keys: A `[CacheKey]` for matching multiple `Data` items to their a location on disk.
public func remove(keys: [CacheKey]) throws {
for key in keys {
try self.remove(key: key)
}
}

/// Removes all the `Data` items located at the `storagePath`.
/// Removes all the `Data` items located in the `directory`.
public func removeAllData() throws {
do {
try FileManager.default.removeItem(at: self.folder)
try FileManager.default.removeItem(at: self.directory.url)
} catch CocoaError.fileNoSuchFile {
// No-op, we treat deleting a non-existent file/folder as a successful removal rather than throwing
} catch {
throw error
}
}

/// Iterates through a directory to find the total number of files.
/// Checks whether a value with a key is persisted.
/// - Parameter key: The key to for existence.
/// - Returns: If the key exists the function returns true, false if it does not.
public func keyExists(_ key: CacheKey) -> Bool {
self.allKeys().contains(key)
}

/// Iterates through a directory to find the total number of `Data` items.
/// - Returns: The file/key count.
public func keyCount() -> Int {
return self.allKeys().count
}

/// Iterates through `storagePath` to find all of the files and their respective keys.
/// Iterates through a `directory` to find all of the keys.
/// - Returns: An array of the keys contained in a directory.
public func allKeys() -> [CacheKey] {
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: self.folder, includingPropertiesForKeys: nil)
let directoryContents = try FileManager.default.contentsOfDirectory(at: self.directory.url, includingPropertiesForKeys: nil)
let fileOnlyKeys = directoryContents.lazy.filter({ !$0.hasDirectoryPath }).map(\.lastPathComponent)

return fileOnlyKeys.map(CacheKey.init(verbatim:))
Expand All @@ -154,27 +143,27 @@ public actor DiskStorage {
.resourceValues(forKeys: [.creationDateKey]).creationDate
}

/// Returns the last access date of the file for the `CacheKey`, if it exists.
/// Returns the updatedAt date for the file represented by the `CacheKey`, if it exists.
/// - Parameters:
/// - key: A `CacheKey` for matching `Data` to a location on disk.
/// - Returns: The last access date of the `Data` on disk if it exists, nil if there is no `Data` stored for the `CacheKey`.
public func lastAccessed(key: CacheKey) -> Date? {
/// - Returns: The updatedAt date of the `Data` on disk if it exists, nil if there is no `Data` stored for the `CacheKey`.
public func updatedAt(key: CacheKey) -> Date? {
return try? self.concatenatedPath(key: key.value)
.resourceValues(forKeys: [.contentAccessDateKey]).contentAccessDate
.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate
}

/// Returns the modification date for the file represented by the `CacheKey`, if it exists.
/// Returns the last access date of the file for the `CacheKey`, if it exists.
/// - Parameters:
/// - key: A `CacheKey` for matching `Data` to a location on disk.
/// - Returns: The modification date of the `Data` on disk if it exists, nil if there is no `Data` stored for the `CacheKey`.
public func lastModified(key: CacheKey) -> Date? {
/// - Returns: The last access date of the `Data` on disk if it exists, nil if there is no `Data` stored for the `CacheKey`.
public func lastAccessed(key: CacheKey) -> Date? {
return try? self.concatenatedPath(key: key.value)
.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate
.resourceValues(forKeys: [.contentAccessDateKey]).contentAccessDate
}

}

private extension DiskStorage {
private extension DiskStorageEngine {

static func createDirectory(url: URL) throws {
try FileManager.default
Expand All @@ -192,7 +181,7 @@ private extension DiskStorage {
}

func concatenatedPath(key: String) -> URL {
return self.folder.appendingPathComponent(key)
return self.directory.url.appendingPathComponent(key)
}

}
11 changes: 11 additions & 0 deletions Sources/Bodega/FileManager.Directory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ public extension FileManager {

public extension FileManager.Directory {

/// A directory that varies based on the OS the user is running code on.
/// - Parameter pathComponent: A path to append to the platform's default directory.
/// - Returns: On macOS this returns the `Application Support` directory, otherwise `Documents`.
static func defaultStorageDirectory(appendingPath pathComponent: String) -> Self {
#if os(macOS)
.applicationSupport(appendingPath: pathComponent)
#else
.documents(appendingPath: pathComponent)
#endif
}

/// Returns a URL to a subfolder created in the documents directory based on the `pathComponent`.
/// - Parameter pathComponent: A path to append to the platform's documents directory.
static func documents(appendingPath pathComponent: String) -> Self {
Expand Down
Loading

0 comments on commit 9cd6eec

Please sign in to comment.