-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
TabDataStore.swift
200 lines (170 loc) · 7.83 KB
/
TabDataStore.swift
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import Foundation
import Common
public protocol TabDataStore {
/// Fetches the previously saved window data matching the provided UUID,
/// if it exists. This data contains the list of tabs.
/// - Returns: The window data object if one was previously saved
func fetchWindowData(uuid: UUID) async -> WindowData?
/// Saves the window data (contains the list of tabs) to disk
/// - Parameter window: the window data object to be saved
func saveWindowData(window: WindowData, forced: Bool) async
/// Erases all window data on disk
func clearAllWindowsData() async
/// Synchronous function that lists UUIDs for all WindowData currently saved
/// to disk. Because this requires no decoding (we can just check the list of
/// saved files in the directory) it is faster than fetchWindowData() and is
/// preferable when only the UUIDs are needed.
/// - Returns: a list of UUIDs for any saved WindowData.
func fetchWindowDataUUIDs() -> [UUID]
}
public actor DefaultTabDataStore: TabDataStore {
enum TabDataError: Error {
case failedToFetchData
}
private let logger: Logger
private let fileManager: TabFileManager
private let throttleTime: UInt64
private var windowDataToSave: WindowData?
private var nextSaveIsScheduled = false
private let filePrefix = "window-"
public init(logger: Logger = DefaultLogger.shared,
fileManager: TabFileManager = DefaultTabFileManager(),
throttleTime: UInt64 = 2 * NSEC_PER_SEC) {
self.logger = logger
self.fileManager = fileManager
self.throttleTime = throttleTime
}
// MARK: Fetching Window Data
public func fetchWindowData(uuid: UUID) async -> WindowData? {
logger.log("Attempting to fetch window/tab data", level: .debug, category: .tabs)
do {
guard let fileURL = windowURLPath(for: uuid, isBackup: false),
fileManager.fileExists(atPath: fileURL),
let windowData = parseWindowDataFile(fromURL: fileURL) else {
logger.log("Failed to open window/tab data for UUID: \(uuid)", level: .fatal, category: .tabs)
throw TabDataError.failedToFetchData
}
return windowData
} catch {
logger.log("Error fetching window data: UUID = \(uuid) Error = \(error)", level: .warning, category: .tabs)
guard let backupURL = windowURLPath(for: uuid, isBackup: true),
fileManager.fileExists(atPath: backupURL),
let backupWindowData = parseWindowDataFile(fromURL: backupURL) else {
return nil
}
return backupWindowData
}
}
nonisolated public func fetchWindowDataUUIDs() -> [UUID] {
guard let directoryURL = fileManager.windowDataDirectory(isBackup: false) else {
logger.log("Could not resolve window data directory", level: .warning, category: .tabs)
return []
}
let fileURLs = fileManager.contentsOfDirectory(at: directoryURL)
return fileURLs.compactMap {
let file = $0.lastPathComponent
guard file.hasPrefix(filePrefix) else { return nil }
let uuidString = String(file.dropFirst(filePrefix.count))
return UUID(uuidString: uuidString)
}
}
private func parseWindowDataFile(fromURL url: URL) -> WindowData? {
return parseWindowDataFiles(fromURLs: [url]).first
}
private func parseWindowDataFiles(fromURLs urlList: [URL]) -> [WindowData] {
var windowsData: [WindowData] = []
for fileURL in urlList {
do {
if let windowData = try? fileManager.getWindowDataFromPath(path: fileURL) {
windowsData.append(windowData)
}
}
}
return windowsData
}
// MARK: - Saving Data
public func saveWindowData(window: WindowData, forced: Bool) async {
guard let windowSavingPath = windowURLPath(for: window.id, isBackup: false) else { return }
// Hold onto a copy of the latest window data so whenever the save happens it is using the latest
windowDataToSave = window
if let windowDataDirectoryURL = fileManager.windowDataDirectory(isBackup: false),
!fileManager.fileExists(atPath: windowDataDirectoryURL) {
fileManager.createDirectoryAtPath(path: windowDataDirectoryURL)
}
logger.log("Save window data, is forced: \(forced)",
level: .debug,
category: .tabs)
if forced {
await writeWindowDataToFile(path: windowSavingPath)
} else {
await writeWindowDataToFileWithThrottle(path: windowSavingPath)
}
}
private func createWindowDataBackup(windowPath: URL) {
guard let windowID = windowDataToSave?.id,
let backupWindowSavingPath = windowURLPath(for: windowID, isBackup: true),
let backupDirectoryPath = fileManager.windowDataDirectory(isBackup: true)
else { return }
if !fileManager.fileExists(atPath: backupDirectoryPath) {
fileManager.createDirectoryAtPath(path: backupDirectoryPath)
}
do {
try fileManager.copyItem(at: windowPath, to: backupWindowSavingPath)
} catch {
logger.log("Failed to create window data backup: \(error)",
level: .warning,
category: .tabs)
}
}
// Throttles the saving of the data so that it happens every 'throttleTime' nanoseconds
// as long as their is new data to be saved
private func writeWindowDataToFileWithThrottle(path: URL) async {
// Ignore the request because a save is already scheduled to happen
guard !nextSaveIsScheduled else { return }
// Set the guard bool to true so no new saves can be initiated while waiting
nextSaveIsScheduled = true
// Dispatch to a task so as not to block the caller
Task {
// Once the throttle time has passed initiate the save and reset the bool
try? await Task.sleep(nanoseconds: throttleTime)
nextSaveIsScheduled = false
if fileManager.fileExists(atPath: path) {
createWindowDataBackup(windowPath: path)
}
await writeWindowDataToFile(path: path)
}
}
private func writeWindowDataToFile(path: URL) async {
do {
guard let windowDataToSave = windowDataToSave else {
logger.log("Tried to save window data but found nil",
level: .fatal,
category: .tabs)
return
}
try fileManager.writeWindowData(windowData: windowDataToSave, to: path)
} catch {
logger.log("Failed to save window data: \(error)",
level: .warning,
category: .tabs)
}
}
// MARK: - Deleting Window Data
public func clearAllWindowsData() async {
guard let directoryURL = fileManager.windowDataDirectory(isBackup: false),
let backupURL = fileManager.windowDataDirectory(isBackup: true) else {
return
}
fileManager.removeAllFilesAt(directory: directoryURL)
fileManager.removeAllFilesAt(directory: backupURL)
}
// MARK: - URL Utils
private func windowURLPath(for windowID: UUID, isBackup: Bool) -> URL? {
guard let baseURL = fileManager.windowDataDirectory(isBackup: isBackup) else { return nil }
let baseFilePath = filePrefix + windowID.uuidString
return baseURL.appendingPathComponent(baseFilePath)
}
}