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
|
//
// RelayCache.swift
// RelayCache
//
// Created by pronebird on 06/09/2021.
// Copyright © 2021 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadREST
public final class RelayCache {
/// Cache file location.
let cacheFileURL: URL
/// Location of pre-bundled relays file.
let prebundledRelaysFileURL: URL
/// Initialize cache with default cache file location in app group container.
public init?(securityGroupIdentifier: String) {
guard let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: securityGroupIdentifier
), let prebundledRelaysFileURL = Bundle(for: Self.self)
.url(forResource: "relays", withExtension: "json") else { return nil }
cacheFileURL = containerURL.appendingPathComponent("relays.json", isDirectory: false)
self.prebundledRelaysFileURL = prebundledRelaysFileURL
}
/// Safely read the cache file from disk using file coordinator and fallback to prebundled
/// relays in case if the relay cache file is missing.
public func read() throws -> CachedRelays {
do {
return try readDiskCache()
} catch {
if error is DecodingError || (error as? CocoaError)?.code == .fileReadNoSuchFile {
return try readPrebundledRelays()
} else {
throw error
}
}
}
/// Safely write the cache file on disk using file coordinator.
public func write(record: CachedRelays) throws {
var result: Result<Void, Error>?
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
let accessor = { (fileURLForWriting: URL) in
result = Result {
let data = try JSONEncoder().encode(record)
try data.write(to: fileURLForWriting)
}
}
var error: NSError?
fileCoordinator.coordinate(
writingItemAt: cacheFileURL,
options: [.forReplacing],
error: &error,
byAccessor: accessor
)
if let error = error {
result = .failure(error)
}
try result?.get()
}
/// Safely read the cache file from disk using file coordinator.
private func readDiskCache() throws -> CachedRelays {
var result: Result<CachedRelays, Error>?
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
let accessor = { (fileURLForReading: URL) in
result = Result {
let data = try Data(contentsOf: fileURLForReading)
return try JSONDecoder().decode(CachedRelays.self, from: data)
}
}
var error: NSError?
fileCoordinator.coordinate(
readingItemAt: cacheFileURL,
options: [.withoutChanges],
error: &error,
byAccessor: accessor
)
if let error = error {
result = .failure(error)
}
return try result!.get()
}
/// Read pre-bundled relays file from disk.
private func readPrebundledRelays() throws -> CachedRelays {
let data = try Data(contentsOf: prebundledRelaysFileURL)
let relays = try REST.Coding.makeJSONDecoder()
.decode(REST.ServerRelaysResponse.self, from: data)
return CachedRelays(
relays: relays,
updatedAt: Date(timeIntervalSince1970: 0)
)
}
}
|