@telecrypt-io/storage 0.5.23 → 0.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -7,6 +7,7 @@ import { FileTooLargeError, RecoveryAlreadyConfiguredError, RecoveryRestoreError
|
|
|
7
7
|
import { MAX_MEDIA_FILE_BYTES, MAX_MATRIX_IDENTIFIER_LENGTH, validateCanonicalMatrixUserId, validateMatrixDeviceId, validateMatrixEventId, validateMatrixRoomId, } from "./core/constants.js";
|
|
8
8
|
import { raceWithAbort, readBoundedResponseBody } from "./core/http.js";
|
|
9
9
|
import { validateName } from "./core/validation.js";
|
|
10
|
+
import { isTreeDeleted } from "./deletion-markers.js";
|
|
10
11
|
const recoveryMutationQueues = new WeakMap();
|
|
11
12
|
const treeMutationQueues = new WeakMap();
|
|
12
13
|
const TREE_SYNC_TIMEOUT_MS = 15000;
|
|
@@ -1244,6 +1245,8 @@ export class TeleCryptIOStorage {
|
|
|
1244
1245
|
for (const room of rooms) {
|
|
1245
1246
|
if (signal?.aborted)
|
|
1246
1247
|
throw new StorageError("operation cancelled");
|
|
1248
|
+
if (isTreeDeleted(this.client, room.roomId))
|
|
1249
|
+
continue;
|
|
1247
1250
|
const tree = this.client.unstableGetFileTreeSpace(room.roomId);
|
|
1248
1251
|
if (tree)
|
|
1249
1252
|
trees.push(this.decorateTreeSpace(tree));
|
|
@@ -1373,6 +1376,8 @@ export class TeleCryptIOStorage {
|
|
|
1373
1376
|
}, signal);
|
|
1374
1377
|
}
|
|
1375
1378
|
getTree(roomId) {
|
|
1379
|
+
if (isTreeDeleted(this.client, roomId))
|
|
1380
|
+
return null;
|
|
1376
1381
|
const tree = this.client.unstableGetFileTreeSpace(roomId);
|
|
1377
1382
|
return tree ? this.decorateTreeSpace(tree) : null;
|
|
1378
1383
|
}
|
package/dist/core/operations.js
CHANGED
|
@@ -18,6 +18,7 @@ import { FileTooLargeError, MutationPartialError, MutationOutcomeUnknownError, N
|
|
|
18
18
|
import { waitForCondition } from "./poll.js";
|
|
19
19
|
import { validateMatrixEventId } from "./constants.js";
|
|
20
20
|
import { validateName } from "./validation.js";
|
|
21
|
+
import { getDeletedTreeIds, isFileDeleted, isTreeDeleted, markFileDeleted, markTreeDeleted, } from "../deletion-markers.js";
|
|
21
22
|
/**
|
|
22
23
|
* Production Synapse enforces the built-in rc_messages budget (per-account
|
|
23
24
|
* burst 10, refill 1/5s) on every room/state mutation the tree operations
|
|
@@ -175,12 +176,27 @@ async function resolveTree(storage, treeId, signal) {
|
|
|
175
176
|
throw new StorageError("storage space not found");
|
|
176
177
|
}
|
|
177
178
|
}
|
|
179
|
+
function isMarkedTreeDeleted(storage, treeId) {
|
|
180
|
+
if (typeof storage.getClient !== "function")
|
|
181
|
+
return false;
|
|
182
|
+
return isTreeDeleted(storage.getClient(), treeId);
|
|
183
|
+
}
|
|
184
|
+
function isMarkedFileDeleted(storage, treeId, fileId) {
|
|
185
|
+
if (typeof storage.getClient !== "function")
|
|
186
|
+
return false;
|
|
187
|
+
return isFileDeleted(storage.getClient(), treeId, fileId);
|
|
188
|
+
}
|
|
178
189
|
/** As `resolveTree`, but for a specific file within an already-resolved
|
|
179
190
|
* vault or folder — covers the same settling window for a file another
|
|
180
191
|
* process/session just uploaded. */
|
|
181
|
-
async function resolveFile(tree, fileId, signal) {
|
|
192
|
+
async function resolveFile(storage, tree, fileId, signal) {
|
|
193
|
+
if (signal?.aborted)
|
|
194
|
+
throw new StorageError("operation cancelled");
|
|
195
|
+
if (isMarkedFileDeleted(storage, tree.id, fileId)) {
|
|
196
|
+
throw new StorageError("file not found");
|
|
197
|
+
}
|
|
182
198
|
try {
|
|
183
|
-
return await waitForCondition(() => tree.getFile(fileId), { timeoutMs: 15000, signal });
|
|
199
|
+
return await waitForCondition(() => (isMarkedFileDeleted(storage, tree.id, fileId) ? null : tree.getFile(fileId)), { timeoutMs: 15000, signal });
|
|
184
200
|
}
|
|
185
201
|
catch {
|
|
186
202
|
if (signal?.aborted)
|
|
@@ -225,12 +241,15 @@ function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large")
|
|
|
225
241
|
* observe each result before removing their containing room. This also
|
|
226
242
|
* prevents a folder/vault delete from silently deleting a nested shared tree.
|
|
227
243
|
*/
|
|
228
|
-
function assertTreeEmptyForDeletion(tree, spaces) {
|
|
244
|
+
function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
229
245
|
if (spaces.length !== 1)
|
|
230
246
|
throw new NonEmptyTreeError(tree.id);
|
|
231
247
|
let files;
|
|
232
248
|
try {
|
|
233
|
-
files = tree
|
|
249
|
+
files = tree
|
|
250
|
+
.listAllFiles()
|
|
251
|
+
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
|
|
252
|
+
!isConfirmedDeletedFile(storage.getClient(), tree.id, file));
|
|
234
253
|
}
|
|
235
254
|
catch {
|
|
236
255
|
throw new StorageError("could not enumerate storage files safely");
|
|
@@ -315,6 +334,26 @@ function isActiveRelationEvent(event) {
|
|
|
315
334
|
!Array.isArray(content) &&
|
|
316
335
|
Object.keys(content).length > 0);
|
|
317
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* A deleted MSC3089 branch is represented by an authoritative empty state
|
|
339
|
+
* event. Inactive branches with metadata are historical versions, and a
|
|
340
|
+
* missing or malformed state cannot prove that the file was deleted.
|
|
341
|
+
*/
|
|
342
|
+
function isConfirmedDeletedFile(client, treeId, file) {
|
|
343
|
+
if (file.isActive)
|
|
344
|
+
return false;
|
|
345
|
+
const stateEvents = readRelationEvents(client, treeId, UNSTABLE_MSC3089_BRANCH.name, file.id);
|
|
346
|
+
if (!stateEvents || stateEvents.length !== 1)
|
|
347
|
+
return false;
|
|
348
|
+
const stateEvent = stateEvents[0];
|
|
349
|
+
if (relationStateKey(stateEvent) !== file.id)
|
|
350
|
+
return false;
|
|
351
|
+
const content = stateEvent.getContent?.();
|
|
352
|
+
return (typeof content === "object" &&
|
|
353
|
+
content !== null &&
|
|
354
|
+
!Array.isArray(content) &&
|
|
355
|
+
Object.keys(content).length === 0);
|
|
356
|
+
}
|
|
318
357
|
/**
|
|
319
358
|
* Deletion is intentionally narrower than MSC3089TreeSpace.delete(): a local
|
|
320
359
|
* child graph is not proof that the rooms are exclusively owned by this tree.
|
|
@@ -480,8 +519,6 @@ async function relinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
480
519
|
}
|
|
481
520
|
}
|
|
482
521
|
}
|
|
483
|
-
const deletedTreeIds = new WeakMap();
|
|
484
|
-
const deletedRoomIds = new WeakMap();
|
|
485
522
|
function isGoneError(error) {
|
|
486
523
|
return (error instanceof MatrixError &&
|
|
487
524
|
(error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
|
|
@@ -540,12 +577,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
540
577
|
throw error;
|
|
541
578
|
}
|
|
542
579
|
removeRoomFromLocalStore(client, roomId);
|
|
543
|
-
|
|
544
|
-
if (!deleted) {
|
|
545
|
-
deleted = new Set();
|
|
546
|
-
deletedRoomIds.set(client, deleted);
|
|
547
|
-
}
|
|
548
|
-
deleted.add(roomId);
|
|
580
|
+
markTreeDeleted(client, roomId);
|
|
549
581
|
}
|
|
550
582
|
/**
|
|
551
583
|
* The SDK normally evicts a room as part of forget(). Keep the local-store
|
|
@@ -693,6 +725,8 @@ export async function listPendingInvites(storage, options) {
|
|
|
693
725
|
const invites = [];
|
|
694
726
|
for (const room of rooms) {
|
|
695
727
|
ensureOperationActive(signal);
|
|
728
|
+
if (isMarkedTreeDeleted(storage, room.roomId))
|
|
729
|
+
continue;
|
|
696
730
|
if (room.getMyMembership() !== "invite")
|
|
697
731
|
continue;
|
|
698
732
|
if (hasActiveSpaceParent(room))
|
|
@@ -991,7 +1025,9 @@ export async function listFiles(storage, treeId, options) {
|
|
|
991
1025
|
try {
|
|
992
1026
|
await storage.refreshRoomState(treeId, { signal });
|
|
993
1027
|
ensureOperationActive(signal);
|
|
994
|
-
const files = tree
|
|
1028
|
+
const files = tree
|
|
1029
|
+
.listFiles()
|
|
1030
|
+
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
|
|
995
1031
|
if (files.length > MAX_LIST_ITEMS)
|
|
996
1032
|
throw new StorageError("file list is too large");
|
|
997
1033
|
return files.map((f) => ({ id: f.id, name: f.getName() }));
|
|
@@ -1011,7 +1047,9 @@ export async function listSubfolders(storage, parentId, options) {
|
|
|
1011
1047
|
const tree = await resolveTree(storage, parentId, signal);
|
|
1012
1048
|
await storage.refreshRoomState(parentId, { signal });
|
|
1013
1049
|
ensureOperationActive(signal);
|
|
1014
|
-
const directories = tree
|
|
1050
|
+
const directories = tree
|
|
1051
|
+
.getDirectories()
|
|
1052
|
+
.filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
|
|
1015
1053
|
if (directories.length > MAX_LIST_ITEMS)
|
|
1016
1054
|
throw new StorageError("folder list is too large");
|
|
1017
1055
|
return directories.map((d) => ({ id: d.id, name: d.room.name }));
|
|
@@ -1084,21 +1122,16 @@ export async function renameFolder(storage, folderId, name, options) {
|
|
|
1084
1122
|
async function deleteTree(storage, treeId, options) {
|
|
1085
1123
|
const operation = createOperationDeadline(options);
|
|
1086
1124
|
const client = storage.getClient();
|
|
1087
|
-
let deleted = deletedTreeIds.get(client);
|
|
1088
|
-
if (!deleted) {
|
|
1089
|
-
deleted = new Set();
|
|
1090
|
-
deletedTreeIds.set(client, deleted);
|
|
1091
|
-
}
|
|
1092
1125
|
try {
|
|
1093
1126
|
ensureOperationActive(operation.signal);
|
|
1094
1127
|
const pending = withTreeMutation(client, async () => {
|
|
1095
|
-
if (
|
|
1128
|
+
if (isTreeDeleted(client, treeId))
|
|
1096
1129
|
return { id: treeId, deleted: true };
|
|
1097
1130
|
const tree = await resolveTree(storage, treeId, operation.signal);
|
|
1098
1131
|
// Read reconciliation state after this operation reaches the single
|
|
1099
1132
|
// client queue; a preceding delete may have populated the set while we
|
|
1100
1133
|
// were waiting for its turn.
|
|
1101
|
-
const removedRooms =
|
|
1134
|
+
const removedRooms = getDeletedTreeIds(client);
|
|
1102
1135
|
let graph;
|
|
1103
1136
|
try {
|
|
1104
1137
|
// Re-read every locally visible room before taking the immutable graph
|
|
@@ -1110,12 +1143,11 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1110
1143
|
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
1111
1144
|
if (!ids.has(tree.id)) {
|
|
1112
1145
|
if (removedRooms.has(tree.id)) {
|
|
1113
|
-
deleted.add(treeId);
|
|
1114
1146
|
return { id: treeId, deleted: true };
|
|
1115
1147
|
}
|
|
1116
1148
|
throw new StorageError("delete graph is unsafe");
|
|
1117
1149
|
}
|
|
1118
|
-
assertTreeEmptyForDeletion(tree, activeSpaces);
|
|
1150
|
+
assertTreeEmptyForDeletion(storage, tree, activeSpaces);
|
|
1119
1151
|
graph = validateDeletionGraph(client, tree, ids, removedRooms);
|
|
1120
1152
|
}
|
|
1121
1153
|
catch (error) {
|
|
@@ -1165,7 +1197,6 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1165
1197
|
throw error;
|
|
1166
1198
|
throw new StorageError("delete failed");
|
|
1167
1199
|
}
|
|
1168
|
-
deleted.add(treeId);
|
|
1169
1200
|
return { id: treeId, deleted: true };
|
|
1170
1201
|
}, operation.signal);
|
|
1171
1202
|
return await raceOperationDeadline(operation, pending, "mutation");
|
|
@@ -1186,7 +1217,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
|
|
|
1186
1217
|
validateName(name, "file name");
|
|
1187
1218
|
return withOperationDeadline(options, async (signal) => {
|
|
1188
1219
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1189
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1220
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1190
1221
|
try {
|
|
1191
1222
|
const client = typeof storage.getClient === "function" ? storage.getClient() : undefined;
|
|
1192
1223
|
await withRateLimitRetry(() => client ? withMatrixMutationAbort(client, () => branch.setName(name), signal) : branch.setName(name), signal);
|
|
@@ -1291,7 +1322,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
|
|
|
1291
1322
|
export async function deleteFile(storage, treeId, fileId, options) {
|
|
1292
1323
|
return withOperationDeadline(options, async (signal) => {
|
|
1293
1324
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1294
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1325
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1295
1326
|
const versions = await resolveFileVersions(branch, signal);
|
|
1296
1327
|
const mediaIds = [...new Set(versions.map(({ mediaId }) => mediaId))];
|
|
1297
1328
|
await deleteFileMedia(storage, mediaIds, signal);
|
|
@@ -1328,6 +1359,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1328
1359
|
const detail = "Matrix deletion completed but the inactive file state could not be verified";
|
|
1329
1360
|
throw new MutationPartialError("delete file", completedIds, detail);
|
|
1330
1361
|
}
|
|
1362
|
+
for (const { branch: version } of versions) {
|
|
1363
|
+
markFileDeleted(storage.getClient(), tree.id, version.id);
|
|
1364
|
+
}
|
|
1331
1365
|
ensureOperationActive(signal);
|
|
1332
1366
|
return { id: fileId, deleted: true };
|
|
1333
1367
|
}, "mutation");
|
|
@@ -1370,7 +1404,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1370
1404
|
export async function downloadFile(storage, treeId, fileId, options) {
|
|
1371
1405
|
return withOperationDeadline(options, async (signal) => {
|
|
1372
1406
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1373
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1407
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1374
1408
|
let result;
|
|
1375
1409
|
try {
|
|
1376
1410
|
result = await storage.downloadFile(branch, signal);
|
|
@@ -1404,7 +1438,7 @@ function tsToIso(ts) {
|
|
|
1404
1438
|
export async function getFileDetails(storage, treeId, fileId, options) {
|
|
1405
1439
|
return withOperationDeadline(options, async (signal) => {
|
|
1406
1440
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1407
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1441
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1408
1442
|
const name = branch.getName();
|
|
1409
1443
|
let mimetype = null;
|
|
1410
1444
|
let size = null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MatrixClient } from "matrix-js-sdk";
|
|
2
|
+
/** Records a confirmed room deletion for this MatrixClient's lifetime. */
|
|
3
|
+
export declare function markTreeDeleted(client: MatrixClient, roomId: string): void;
|
|
4
|
+
/** Records a confirmed file deletion for this MatrixClient's lifetime. */
|
|
5
|
+
export declare function markFileDeleted(client: MatrixClient, roomId: string, fileId: string): void;
|
|
6
|
+
export declare function isTreeDeleted(client: MatrixClient, roomId: string): boolean;
|
|
7
|
+
export declare function getDeletedTreeIds(client: MatrixClient): ReadonlySet<string>;
|
|
8
|
+
export declare function isFileDeleted(client: MatrixClient, roomId: string, fileId: string): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const markersByClient = new WeakMap();
|
|
2
|
+
function markersFor(client) {
|
|
3
|
+
let markers = markersByClient.get(client);
|
|
4
|
+
if (!markers) {
|
|
5
|
+
markers = { trees: new Set(), files: new Map() };
|
|
6
|
+
markersByClient.set(client, markers);
|
|
7
|
+
}
|
|
8
|
+
return markers;
|
|
9
|
+
}
|
|
10
|
+
/** Records a confirmed room deletion for this MatrixClient's lifetime. */
|
|
11
|
+
export function markTreeDeleted(client, roomId) {
|
|
12
|
+
markersFor(client).trees.add(roomId);
|
|
13
|
+
}
|
|
14
|
+
/** Records a confirmed file deletion for this MatrixClient's lifetime. */
|
|
15
|
+
export function markFileDeleted(client, roomId, fileId) {
|
|
16
|
+
const markers = markersFor(client);
|
|
17
|
+
let files = markers.files.get(roomId);
|
|
18
|
+
if (!files) {
|
|
19
|
+
files = new Set();
|
|
20
|
+
markers.files.set(roomId, files);
|
|
21
|
+
}
|
|
22
|
+
files.add(fileId);
|
|
23
|
+
}
|
|
24
|
+
export function isTreeDeleted(client, roomId) {
|
|
25
|
+
return markersFor(client).trees.has(roomId);
|
|
26
|
+
}
|
|
27
|
+
export function getDeletedTreeIds(client) {
|
|
28
|
+
return markersFor(client).trees;
|
|
29
|
+
}
|
|
30
|
+
export function isFileDeleted(client, roomId, fileId) {
|
|
31
|
+
return markersFor(client).files.get(roomId)?.has(fileId) ?? false;
|
|
32
|
+
}
|