-
-
Notifications
You must be signed in to change notification settings - Fork 155
Expand file tree
/
Copy pathFileCache.swift
More file actions
153 lines (130 loc) · 4.64 KB
/
FileCache.swift
File metadata and controls
153 lines (130 loc) · 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//
// FileCache.swift
// Siesta
//
// Created by Paul on 2017/11/22.
// Copyright © 2017 Bust Out Solutions. All rights reserved.
//
#if COCOAPODS
import CommonCryptoModule
#else
import Siesta
import CommonCrypto
#endif
private typealias File = URL
private let fileCacheFormatVersion: [UInt8] = [0]
public struct FileCache<ContentType>: EntityCache
where ContentType: Codable
{
private let keyPrefix: Data
private let cacheDir: File
private let encoder = PropertyListEncoder()
private let decoder = PropertyListDecoder()
public init<T>(poolName: String = "Default", userIdentity: T?) throws
where T: Encodable
{
encoder.outputFormat = .binary
self.keyPrefix = try
fileCacheFormatVersion // prevents us from parsing old cache entries using some new future format
+ encoder.encode(userIdentity) // prevents one user from seeing another’s cached requests
+ [0] // separator for URL
cacheDir = try
FileManager.default.url(
for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(Bundle.main.bundleIdentifier ?? "")
.appendingPathComponent("Siesta")
.appendingPathComponent(poolName)
try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
}
// MARK: - Keys and filenames
public func key(for resource: Resource) -> Key?
{ return Key(resource: resource, prefix: keyPrefix) }
public struct Key: CustomStringConvertible
{
fileprivate var url, hash: String
fileprivate init(resource: Resource, prefix: Data)
{
url = resource.url.absoluteString
hash = Data(prefix + url.utf8)
.sha256
.urlSafeBase64EncodedString
}
public var description: String
{ return "FileCache.Key(\(url))" }
}
private func file(for key: Key) -> File
{ return cacheDir.appendingPathComponent(key.hash + ".plist") }
// MARK: - Reading and writing
public func readEntity(forKey key: Key) -> Entity<ContentType>?
{
do {
return try
decoder.decode(EncodableEntity<ContentType>.self,
from: Data(contentsOf: file(for: key)))
.entity
}
catch CocoaError.fileReadNoSuchFile
{ } // a cache miss is just fine
catch
{ log(.cache, ["WARNING: FileCache unable to read cached entity for", key, ":", error]) }
return nil
}
public func writeEntity(_ entity: Entity<ContentType>, forKey key: Key)
{
do {
try encoder.encode(EncodableEntity(entity))
.write(to: file(for: key), options: [.atomic, .completeFileProtection])
}
catch
{ log(.cache, ["WARNING: FileCache unable to write entity for", key, ":", error]) }
}
public func removeEntity(forKey key: Key)
{
do {
try FileManager.default.removeItem(at: file(for: key))
}
catch
{ log(.cache, ["WARNING: FileCache unable to clear cache entity for", key, ":", error]) }
}
}
/// Ideally, Entity itself would be codable when its ContentType is codable. To do this, Swift would need to:
///
/// 1. allow conditional conformance, and
/// 2. allow extensions to synthesize encode/decode.
///
/// This struct is a stopgap until the language can do all that.
///
private struct EncodableEntity<ContentType>: Codable
where ContentType: Codable
{
var timestamp: TimeInterval
var headers: [String:String]
var charset: String?
var content: ContentType
init(_ entity: Entity<ContentType>)
{
timestamp = entity.timestamp
headers = entity.headers
charset = entity.charset
content = entity.content
}
var entity: Entity<ContentType>
{ return Entity(content: content, charset: charset, headers: headers, timestamp: timestamp) }
}
private extension Data
{
var sha256: Data
{
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = withUnsafeBytes
{ CC_SHA256($0, CC_LONG(count), &hash) }
return Data(hash)
}
var urlSafeBase64EncodedString: String
{
return base64EncodedString()
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "=", with: "")
}
}