-
-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathDatabaseHelper.swift
More file actions
257 lines (240 loc) · 9.46 KB
/
Copy pathDatabaseHelper.swift
File metadata and controls
257 lines (240 loc) · 9.46 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
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//
// DatabaseHelper.swift
// CryptomatorFileProvider
//
// Created by Philipp Schmid on 21.07.20.
// Copyright © 2020 Skymatic GmbH. All rights reserved.
//
import CryptomatorCloudAccessCore
import FileProvider
import Foundation
import GRDB
public protocol DatabaseHelping {
func getDatabaseURL(for domain: NSFileProviderDomain) -> URL
func getMigratedDB(at databaseURL: URL, purposeIdentifier: String) throws -> DatabaseWriter
}
public struct DatabaseHelper: DatabaseHelping {
public static let `default` = DatabaseHelper()
public func getDatabaseURL(for domain: NSFileProviderDomain) -> URL {
let documentStorageURL = NSFileProviderManager.default.documentStorageURL
let domainRootURL = documentStorageURL.appendingPathComponent(domain.pathRelativeToDocumentStorage)
return domainRootURL.appendingPathComponent("db.sqlite")
}
public func getMigratedDB(at databaseURL: URL, purposeIdentifier: String) throws -> DatabaseWriter {
let fileCoordinator = NSFileCoordinator()
fileCoordinator.purposeIdentifier = purposeIdentifier
let dbPool = try Self.openSharedDatabase(at: databaseURL, fileCoordinator: fileCoordinator)
try Self.migrate(dbPool)
return dbPool
}
// swiftlint:disable:next function_body_length
static func migrate(_ dbWriter: DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("v1") { db in
try db.create(table: "itemMetadata") { table in
table.autoIncrementedPrimaryKey("id")
table.column("name", .text).notNull()
table.column("type", .text).notNull()
table.column("size", .integer)
table.column("parentID", .integer).references("itemMetadata", onDelete: .cascade)
table.column("lastModifiedDate", .date)
table.column("statusCode", .text).notNull()
table.column("cloudPath", .text).unique()
table.column("isPlaceholderItem", .boolean).notNull().defaults(to: false)
table.column("isMaybeOutdated", .boolean).notNull().defaults(to: false)
}
try db.execute(sql: """
INSERT INTO itemMetadata (id, name, type, size, parentID, lastModifiedDate, statusCode, cloudPath, isPlaceholderItem, isMaybeOutdated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
arguments: [1, "Home", "folder", nil, 1, nil, "isUploaded", "/", true, false])
try db.create(table: "cachedFiles") { table in
table.column("correspondingItem", .integer).primaryKey(onConflict: .replace).references("itemMetadata")
table.column("lastModifiedDate", .text)
table.column("localLastModifiedDate", .date).notNull()
table.column("localURL", .text).unique()
}
try db.create(table: "uploadTasks") { table in
table.column("correspondingItem", .integer).primaryKey().references("itemMetadata", onDelete: .cascade)
table.column(
"lastFailedUploadDate", .date
)
table.column("uploadErrorCode", .integer)
table.column("uploadErrorDomain", .text)
table.check(sql: "(lastFailedUploadDate is NULL and uploadErrorCode is NULL and uploadErrorDomain is NULL) OR (lastFailedUploadDate is NOT NULL and uploadErrorCode is NOT NULL and uploadErrorDomain is NOT NULL)")
}
try db.create(table: "reparentTasks") { table in
table.column("correspondingItem", .integer).primaryKey(onConflict: .replace).references("itemMetadata", onDelete: .cascade)
table.column("sourceCloudPath", .text).notNull()
table.column("targetCloudPath", .text).notNull()
table.column("oldParentId", .integer).notNull()
table.column("newParentId", .integer).notNull()
}
try db.create(table: "deletionTasks") { table in
table.column("correspondingItem", .integer).primaryKey(onConflict: .replace).references("itemMetadata", onDelete: .cascade)
table.column("cloudPath", .text).notNull()
table.column("parentID", .integer).notNull()
table.column("itemType", .text).notNull()
}
try db.create(table: "itemEnumerationTasks") { table in
table.column("correspondingItem", .integer).primaryKey(onConflict: .replace).references("itemMetadata", onDelete: .cascade)
table.column("pageToken", .text)
}
try db.create(table: "downloadTasks") { table in
table.column("correspondingItem", .integer).primaryKey(onConflict: .replace).references("itemMetadata", onDelete: .cascade)
table.column("replaceExisting", .boolean).notNull()
table.column("localURL", .text).notNull()
}
// Single-Row Table (see: https://github.com/groue/GRDB.swift/blob/32b2923e890df320906e64cbd0faca22a8bfda14/Documentation/SingleRowTables.md)
try db.create(table: "maintenanceMode") { table in
table.column("id", .integer).primaryKey(onConflict: .replace).check { $0 == 1 }
table.column("flag", .boolean).notNull()
}
try db.execute(sql: """
CREATE TRIGGER uploadTasks_prevent_insert_on_maintenance_mode
BEFORE INSERT
ON uploadTasks
BEGIN
SELECT RAISE(ABORT, 'Maintenance Mode')
WHERE EXISTS (SELECT 1 FROM maintenanceMode WHERE flag = 1);
END;
""")
try db.execute(sql: """
CREATE TRIGGER reparentTasks_prevent_insert_on_maintenance_mode
BEFORE INSERT
ON reparentTasks
BEGIN
SELECT RAISE(ABORT, 'Maintenance Mode')
WHERE EXISTS (SELECT 1 FROM maintenanceMode WHERE flag = 1);
END;
""")
try db.execute(sql: """
CREATE TRIGGER deletionTasks_prevent_insert_on_maintenance_mode
BEFORE INSERT
ON deletionTasks
BEGIN
SELECT RAISE(ABORT, 'Maintenance Mode')
WHERE EXISTS (SELECT 1 FROM maintenanceMode WHERE flag = 1);
END;
""")
try db.execute(sql: """
CREATE TRIGGER itemEnumerationTasks_prevent_insert_on_maintenance_mode
BEFORE INSERT
ON itemEnumerationTasks
BEGIN
SELECT RAISE(ABORT, 'Maintenance Mode')
WHERE EXISTS (SELECT 1 FROM maintenanceMode WHERE flag = 1);
END;
""")
try db.execute(sql: """
CREATE TRIGGER downloadTasks_prevent_insert_on_maintenance_mode
BEFORE INSERT
ON downloadTasks
BEGIN
SELECT RAISE(ABORT, 'Maintenance Mode')
WHERE EXISTS (SELECT 1 FROM maintenanceMode WHERE flag = 1);
END;
""")
try db.execute(sql: """
CREATE TRIGGER maintenanceMode_prevent_insert_true_on_running_task
BEFORE INSERT
ON maintenanceMode
FOR EACH ROW
WHEN NEW.flag = 1
BEGIN
SELECT RAISE(ABORT, 'Running Task')
WHERE EXISTS (SELECT 1 FROM uploadTasks WHERE uploadErrorCode IS NULL)
OR EXISTS (SELECT 1 FROM reparentTasks)
OR EXISTS (SELECT 1 FROM deletionTasks)
OR EXISTS (SELECT 1 FROM itemEnumerationTasks)
OR EXISTS (SELECT 1 FROM downloadTasks);
END;
""")
try db.execute(sql: """
CREATE TRIGGER maintenanceMode_prevent_update_to_true_on_running_task
BEFORE UPDATE
ON maintenanceMode
FOR EACH ROW
WHEN NEW.flag = 1
BEGIN
SELECT RAISE(ABORT, 'Running Task')
WHERE EXISTS (SELECT 1 FROM uploadTasks WHERE uploadErrorCode IS NULL)
OR EXISTS (SELECT 1 FROM reparentTasks)
OR EXISTS (SELECT 1 FROM deletionTasks)
OR EXISTS (SELECT 1 FROM itemEnumerationTasks)
OR EXISTS (SELECT 1 FROM downloadTasks);
END;
""")
}
migrator.registerMigration("v2") { db in
try db.alter(table: "itemMetadata", body: { table in
table.add(column: "favoriteRank", .integer)
table.add(column: "tagData", .blob)
})
}
migrator.registerMigration("v3") { db in
try db.alter(table: "uploadTasks") { table in
table.add(column: "uploadStartedAt", .date)
}
}
migrator.registerMigration("v4") { db in
try db.alter(table: "itemMetadata") { table in
table.add(column: "lastEnumeratedAt", .date)
}
try db.execute(sql: """
UPDATE itemMetadata
SET lastEnumeratedAt = CURRENT_TIMESTAMP
WHERE type = 'folder'
AND EXISTS (
SELECT 1
FROM itemMetadata AS child
WHERE child.parentID = itemMetadata.id
AND child.id != itemMetadata.id
)
""")
}
migrator.registerMigration("v5", foreignKeyChecks: .deferred) { db in
// SQLite refuses DROP COLUMN on UNIQUE-constrained columns, so we must rebuild to drop `cloudPath`.
try db.execute(sql: """
CREATE TABLE itemMetadata_tmp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
size INTEGER,
parentID INTEGER REFERENCES itemMetadata(id) ON DELETE CASCADE,
lastModifiedDate DATE,
statusCode TEXT NOT NULL,
isPlaceholderItem BOOLEAN NOT NULL DEFAULT 0,
isMaybeOutdated BOOLEAN NOT NULL DEFAULT 0,
favoriteRank INTEGER,
tagData BLOB,
lastEnumeratedAt DATE
)
""")
try db.execute(sql: """
INSERT INTO itemMetadata_tmp (id, name, type, size, parentID, lastModifiedDate, statusCode, isPlaceholderItem, isMaybeOutdated, favoriteRank, tagData, lastEnumeratedAt)
SELECT id, name, type, size, parentID, lastModifiedDate, statusCode, isPlaceholderItem, isMaybeOutdated, favoriteRank, tagData, lastEnumeratedAt FROM itemMetadata
""")
try db.execute(sql: "DROP TABLE itemMetadata")
try db.execute(sql: "ALTER TABLE itemMetadata_tmp RENAME TO itemMetadata")
try db.execute(sql: "CREATE INDEX itemMetadata_parentID_idx ON itemMetadata(parentID)")
}
try migrator.migrate(dbWriter)
}
private static func openSharedDatabase(at databaseURL: URL, fileCoordinator: NSFileCoordinator) throws -> DatabasePool {
var coordinatorError: NSError?
var dbPool: DatabasePool?
var dbError: Error?
fileCoordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { _ in
do {
dbPool = try DatabasePool(path: databaseURL.path)
} catch {
dbError = error
}
})
if let error = dbError ?? coordinatorError {
throw error
}
return dbPool!
}
}