@telecrypt-io/storage 0.5.23 → 0.5.25
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,14 @@ 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));
|
|
234
252
|
}
|
|
235
253
|
catch {
|
|
236
254
|
throw new StorageError("could not enumerate storage files safely");
|
|
@@ -480,8 +498,6 @@ async function relinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
480
498
|
}
|
|
481
499
|
}
|
|
482
500
|
}
|
|
483
|
-
const deletedTreeIds = new WeakMap();
|
|
484
|
-
const deletedRoomIds = new WeakMap();
|
|
485
501
|
function isGoneError(error) {
|
|
486
502
|
return (error instanceof MatrixError &&
|
|
487
503
|
(error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
|
|
@@ -540,12 +556,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
540
556
|
throw error;
|
|
541
557
|
}
|
|
542
558
|
removeRoomFromLocalStore(client, roomId);
|
|
543
|
-
|
|
544
|
-
if (!deleted) {
|
|
545
|
-
deleted = new Set();
|
|
546
|
-
deletedRoomIds.set(client, deleted);
|
|
547
|
-
}
|
|
548
|
-
deleted.add(roomId);
|
|
559
|
+
markTreeDeleted(client, roomId);
|
|
549
560
|
}
|
|
550
561
|
/**
|
|
551
562
|
* The SDK normally evicts a room as part of forget(). Keep the local-store
|
|
@@ -693,6 +704,8 @@ export async function listPendingInvites(storage, options) {
|
|
|
693
704
|
const invites = [];
|
|
694
705
|
for (const room of rooms) {
|
|
695
706
|
ensureOperationActive(signal);
|
|
707
|
+
if (isMarkedTreeDeleted(storage, room.roomId))
|
|
708
|
+
continue;
|
|
696
709
|
if (room.getMyMembership() !== "invite")
|
|
697
710
|
continue;
|
|
698
711
|
if (hasActiveSpaceParent(room))
|
|
@@ -991,7 +1004,9 @@ export async function listFiles(storage, treeId, options) {
|
|
|
991
1004
|
try {
|
|
992
1005
|
await storage.refreshRoomState(treeId, { signal });
|
|
993
1006
|
ensureOperationActive(signal);
|
|
994
|
-
const files = tree
|
|
1007
|
+
const files = tree
|
|
1008
|
+
.listFiles()
|
|
1009
|
+
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
|
|
995
1010
|
if (files.length > MAX_LIST_ITEMS)
|
|
996
1011
|
throw new StorageError("file list is too large");
|
|
997
1012
|
return files.map((f) => ({ id: f.id, name: f.getName() }));
|
|
@@ -1011,7 +1026,9 @@ export async function listSubfolders(storage, parentId, options) {
|
|
|
1011
1026
|
const tree = await resolveTree(storage, parentId, signal);
|
|
1012
1027
|
await storage.refreshRoomState(parentId, { signal });
|
|
1013
1028
|
ensureOperationActive(signal);
|
|
1014
|
-
const directories = tree
|
|
1029
|
+
const directories = tree
|
|
1030
|
+
.getDirectories()
|
|
1031
|
+
.filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
|
|
1015
1032
|
if (directories.length > MAX_LIST_ITEMS)
|
|
1016
1033
|
throw new StorageError("folder list is too large");
|
|
1017
1034
|
return directories.map((d) => ({ id: d.id, name: d.room.name }));
|
|
@@ -1084,21 +1101,16 @@ export async function renameFolder(storage, folderId, name, options) {
|
|
|
1084
1101
|
async function deleteTree(storage, treeId, options) {
|
|
1085
1102
|
const operation = createOperationDeadline(options);
|
|
1086
1103
|
const client = storage.getClient();
|
|
1087
|
-
let deleted = deletedTreeIds.get(client);
|
|
1088
|
-
if (!deleted) {
|
|
1089
|
-
deleted = new Set();
|
|
1090
|
-
deletedTreeIds.set(client, deleted);
|
|
1091
|
-
}
|
|
1092
1104
|
try {
|
|
1093
1105
|
ensureOperationActive(operation.signal);
|
|
1094
1106
|
const pending = withTreeMutation(client, async () => {
|
|
1095
|
-
if (
|
|
1107
|
+
if (isTreeDeleted(client, treeId))
|
|
1096
1108
|
return { id: treeId, deleted: true };
|
|
1097
1109
|
const tree = await resolveTree(storage, treeId, operation.signal);
|
|
1098
1110
|
// Read reconciliation state after this operation reaches the single
|
|
1099
1111
|
// client queue; a preceding delete may have populated the set while we
|
|
1100
1112
|
// were waiting for its turn.
|
|
1101
|
-
const removedRooms =
|
|
1113
|
+
const removedRooms = getDeletedTreeIds(client);
|
|
1102
1114
|
let graph;
|
|
1103
1115
|
try {
|
|
1104
1116
|
// Re-read every locally visible room before taking the immutable graph
|
|
@@ -1110,12 +1122,11 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1110
1122
|
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
1111
1123
|
if (!ids.has(tree.id)) {
|
|
1112
1124
|
if (removedRooms.has(tree.id)) {
|
|
1113
|
-
deleted.add(treeId);
|
|
1114
1125
|
return { id: treeId, deleted: true };
|
|
1115
1126
|
}
|
|
1116
1127
|
throw new StorageError("delete graph is unsafe");
|
|
1117
1128
|
}
|
|
1118
|
-
assertTreeEmptyForDeletion(tree, activeSpaces);
|
|
1129
|
+
assertTreeEmptyForDeletion(storage, tree, activeSpaces);
|
|
1119
1130
|
graph = validateDeletionGraph(client, tree, ids, removedRooms);
|
|
1120
1131
|
}
|
|
1121
1132
|
catch (error) {
|
|
@@ -1165,7 +1176,6 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1165
1176
|
throw error;
|
|
1166
1177
|
throw new StorageError("delete failed");
|
|
1167
1178
|
}
|
|
1168
|
-
deleted.add(treeId);
|
|
1169
1179
|
return { id: treeId, deleted: true };
|
|
1170
1180
|
}, operation.signal);
|
|
1171
1181
|
return await raceOperationDeadline(operation, pending, "mutation");
|
|
@@ -1186,7 +1196,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
|
|
|
1186
1196
|
validateName(name, "file name");
|
|
1187
1197
|
return withOperationDeadline(options, async (signal) => {
|
|
1188
1198
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1189
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1199
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1190
1200
|
try {
|
|
1191
1201
|
const client = typeof storage.getClient === "function" ? storage.getClient() : undefined;
|
|
1192
1202
|
await withRateLimitRetry(() => client ? withMatrixMutationAbort(client, () => branch.setName(name), signal) : branch.setName(name), signal);
|
|
@@ -1291,7 +1301,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
|
|
|
1291
1301
|
export async function deleteFile(storage, treeId, fileId, options) {
|
|
1292
1302
|
return withOperationDeadline(options, async (signal) => {
|
|
1293
1303
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1294
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1304
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1295
1305
|
const versions = await resolveFileVersions(branch, signal);
|
|
1296
1306
|
const mediaIds = [...new Set(versions.map(({ mediaId }) => mediaId))];
|
|
1297
1307
|
await deleteFileMedia(storage, mediaIds, signal);
|
|
@@ -1328,6 +1338,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1328
1338
|
const detail = "Matrix deletion completed but the inactive file state could not be verified";
|
|
1329
1339
|
throw new MutationPartialError("delete file", completedIds, detail);
|
|
1330
1340
|
}
|
|
1341
|
+
for (const { branch: version } of versions) {
|
|
1342
|
+
markFileDeleted(storage.getClient(), tree.id, version.id);
|
|
1343
|
+
}
|
|
1331
1344
|
ensureOperationActive(signal);
|
|
1332
1345
|
return { id: fileId, deleted: true };
|
|
1333
1346
|
}, "mutation");
|
|
@@ -1370,7 +1383,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1370
1383
|
export async function downloadFile(storage, treeId, fileId, options) {
|
|
1371
1384
|
return withOperationDeadline(options, async (signal) => {
|
|
1372
1385
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1373
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1386
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1374
1387
|
let result;
|
|
1375
1388
|
try {
|
|
1376
1389
|
result = await storage.downloadFile(branch, signal);
|
|
@@ -1404,7 +1417,7 @@ function tsToIso(ts) {
|
|
|
1404
1417
|
export async function getFileDetails(storage, treeId, fileId, options) {
|
|
1405
1418
|
return withOperationDeadline(options, async (signal) => {
|
|
1406
1419
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1407
|
-
const branch = await resolveFile(tree, fileId, signal);
|
|
1420
|
+
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1408
1421
|
const name = branch.getName();
|
|
1409
1422
|
let mimetype = null;
|
|
1410
1423
|
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
|
+
}
|