@ai-matrx/media 0.7.16 → 0.9.0
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.
- package/CHANGELOG.md +25 -0
- package/dist/files/engine/api/assets.d.ts +223 -0
- package/dist/files/engine/api/assets.js +124 -0
- package/dist/files/engine/api/assets.js.map +1 -0
- package/dist/files/engine/api/direct.d.ts +43 -0
- package/dist/files/engine/api/direct.js +73 -0
- package/dist/files/engine/api/direct.js.map +1 -0
- package/dist/files/engine/api/fileOrganization.d.ts +34 -0
- package/dist/files/engine/api/fileOrganization.js +38 -0
- package/dist/files/engine/api/fileOrganization.js.map +1 -0
- package/dist/files/engine/api/files.d.ts +249 -0
- package/dist/files/engine/api/files.js +231 -0
- package/dist/files/engine/api/files.js.map +1 -0
- package/dist/files/engine/api/folders.d.ts +63 -0
- package/dist/files/engine/api/folders.js +56 -0
- package/dist/files/engine/api/folders.js.map +1 -0
- package/dist/files/engine/api/office.d.ts +27 -0
- package/dist/files/engine/api/office.js +19 -0
- package/dist/files/engine/api/office.js.map +1 -0
- package/dist/files/engine/api/permissions.d.ts +37 -0
- package/dist/files/engine/api/permissions.js +109 -0
- package/dist/files/engine/api/permissions.js.map +1 -0
- package/dist/files/engine/api/versions.d.ts +26 -0
- package/dist/files/engine/api/versions.js +37 -0
- package/dist/files/engine/api/versions.js.map +1 -0
- package/dist/files/engine/cache/idb-store.d.ts +95 -0
- package/dist/files/engine/cache/idb-store.js +172 -0
- package/dist/files/engine/cache/idb-store.js.map +1 -0
- package/dist/files/engine/cache/policy.d.ts +14 -0
- package/dist/files/engine/cache/policy.js +40 -0
- package/dist/files/engine/cache/policy.js.map +1 -0
- package/dist/files/engine/cache/register-service-worker.d.ts +57 -0
- package/dist/files/engine/cache/register-service-worker.js +80 -0
- package/dist/files/engine/cache/register-service-worker.js.map +1 -0
- package/dist/files/engine/db-types.d.ts +48865 -0
- package/dist/files/engine/db-types.js +1 -0
- package/dist/files/engine/db-types.js.map +1 -0
- package/dist/files/engine/filesDb.d.ts +1913 -0
- package/dist/files/engine/filesDb.js +28 -0
- package/dist/files/engine/filesDb.js.map +1 -0
- package/dist/files/engine/handler/handler.d.ts +82 -0
- package/dist/files/engine/handler/handler.js +124 -0
- package/dist/files/engine/handler/handler.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFile.d.ts +17 -0
- package/dist/files/engine/handler/hooks/useFile.js +75 -0
- package/dist/files/engine/handler/hooks/useFile.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFileUpload.d.ts +76 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js +67 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js.map +1 -0
- package/dist/files/engine/handler/input/normalize.d.ts +14 -0
- package/dist/files/engine/handler/input/normalize.js +365 -0
- package/dist/files/engine/handler/input/normalize.js.map +1 -0
- package/dist/files/engine/handler/intelligence/access.d.ts +35 -0
- package/dist/files/engine/handler/intelligence/access.js +85 -0
- package/dist/files/engine/handler/intelligence/access.js.map +1 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.d.ts +21 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js +67 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js.map +1 -0
- package/dist/files/engine/handler/output/target.d.ts +19 -0
- package/dist/files/engine/handler/output/target.js +222 -0
- package/dist/files/engine/handler/output/target.js.map +1 -0
- package/dist/files/engine/handler/resolver.d.ts +34 -0
- package/dist/files/engine/handler/resolver.js +133 -0
- package/dist/files/engine/handler/resolver.js.map +1 -0
- package/dist/files/engine/handler/types.d.ts +367 -0
- package/dist/files/engine/handler/types.js +1 -0
- package/dist/files/engine/handler/types.js.map +1 -0
- package/dist/files/engine/handler/upload.d.ts +32 -0
- package/dist/files/engine/handler/upload.js +295 -0
- package/dist/files/engine/handler/upload.js.map +1 -0
- package/dist/files/engine/handler/utils/classify.d.ts +14 -0
- package/dist/files/engine/handler/utils/classify.js +27 -0
- package/dist/files/engine/handler/utils/classify.js.map +1 -0
- package/dist/files/engine/handler/utils/prefer-locator.d.ts +32 -0
- package/dist/files/engine/handler/utils/prefer-locator.js +34 -0
- package/dist/files/engine/handler/utils/prefer-locator.js.map +1 -0
- package/dist/files/engine/handler/utils/python-base.d.ts +129 -0
- package/dist/files/engine/handler/utils/python-base.js +60 -0
- package/dist/files/engine/handler/utils/python-base.js.map +1 -0
- package/dist/files/engine/hooks/blob-cache.d.ts +130 -0
- package/dist/files/engine/hooks/blob-cache.js +162 -0
- package/dist/files/engine/hooks/blob-cache.js.map +1 -0
- package/dist/files/engine/hooks/office-extraction-cache.d.ts +28 -0
- package/dist/files/engine/hooks/office-extraction-cache.js +67 -0
- package/dist/files/engine/hooks/office-extraction-cache.js.map +1 -0
- package/dist/files/engine/host/configure.d.ts +166 -0
- package/dist/files/engine/host/configure.js +30 -0
- package/dist/files/engine/host/configure.js.map +1 -0
- package/dist/files/engine/host/org.d.ts +8 -0
- package/dist/files/engine/host/org.js +16 -0
- package/dist/files/engine/host/org.js.map +1 -0
- package/dist/files/engine/host/python-client.d.ts +60 -0
- package/dist/files/engine/host/python-client.js +46 -0
- package/dist/files/engine/host/python-client.js.map +1 -0
- package/dist/files/engine/host/share-links.d.ts +20 -0
- package/dist/files/engine/host/share-links.js +32 -0
- package/dist/files/engine/host/share-links.js.map +1 -0
- package/dist/files/engine/host/store.d.ts +24 -0
- package/dist/files/engine/host/store.js +23 -0
- package/dist/files/engine/host/store.js.map +1 -0
- package/dist/files/engine/host/supabase.d.ts +3808 -0
- package/dist/files/engine/host/supabase.js +33 -0
- package/dist/files/engine/host/supabase.js.map +1 -0
- package/dist/files/engine/host/toast.d.ts +14 -0
- package/dist/files/engine/host/toast.js +20 -0
- package/dist/files/engine/host/toast.js.map +1 -0
- package/dist/files/engine/host/typed-client.d.ts +136 -0
- package/dist/files/engine/host/typed-client.js +66 -0
- package/dist/files/engine/host/typed-client.js.map +1 -0
- package/dist/files/engine/index.d.ts +22 -0
- package/dist/files/engine/index.js +49 -0
- package/dist/files/engine/index.js.map +1 -0
- package/dist/files/engine/media/our-file-sources.d.ts +39 -0
- package/dist/files/engine/media/our-file-sources.js +83 -0
- package/dist/files/engine/media/our-file-sources.js.map +1 -0
- package/dist/files/engine/media/signed-url.d.ts +1 -0
- package/dist/files/engine/media/signed-url.js +6 -0
- package/dist/files/engine/media/signed-url.js.map +1 -0
- package/dist/files/engine/redux/converters.d.ts +73 -0
- package/dist/files/engine/redux/converters.js +286 -0
- package/dist/files/engine/redux/converters.js.map +1 -0
- package/dist/files/engine/redux/file-hydration.d.ts +15 -0
- package/dist/files/engine/redux/file-hydration.js +46 -0
- package/dist/files/engine/redux/file-hydration.js.map +1 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.d.ts +5 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js +28 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js.map +1 -0
- package/dist/files/engine/redux/file-tree-timeout.d.ts +32 -0
- package/dist/files/engine/redux/file-tree-timeout.js +73 -0
- package/dist/files/engine/redux/file-tree-timeout.js.map +1 -0
- package/dist/files/engine/redux/mutation-toast-middleware.d.ts +27 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js +103 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js.map +1 -0
- package/dist/files/engine/redux/realtime-middleware.d.ts +61 -0
- package/dist/files/engine/redux/realtime-middleware.js +431 -0
- package/dist/files/engine/redux/realtime-middleware.js.map +1 -0
- package/dist/files/engine/redux/request-ledger.d.ts +68 -0
- package/dist/files/engine/redux/request-ledger.js +66 -0
- package/dist/files/engine/redux/request-ledger.js.map +1 -0
- package/dist/files/engine/redux/selectors.d.ts +3254 -0
- package/dist/files/engine/redux/selectors.js +428 -0
- package/dist/files/engine/redux/selectors.js.map +1 -0
- package/dist/files/engine/redux/slice.d.ts +157 -0
- package/dist/files/engine/redux/slice.js +836 -0
- package/dist/files/engine/redux/slice.js.map +1 -0
- package/dist/files/engine/redux/thunks.d.ts +449 -0
- package/dist/files/engine/redux/thunks.js +1684 -0
- package/dist/files/engine/redux/thunks.js.map +1 -0
- package/dist/files/engine/redux/tree-utils.d.ts +90 -0
- package/dist/files/engine/redux/tree-utils.js +200 -0
- package/dist/files/engine/redux/tree-utils.js.map +1 -0
- package/dist/files/engine/support/claimsUser.d.ts +77 -0
- package/dist/files/engine/support/claimsUser.js +48 -0
- package/dist/files/engine/support/claimsUser.js.map +1 -0
- package/dist/files/engine/support/datetime.d.ts +6 -0
- package/dist/files/engine/support/datetime.js +15 -0
- package/dist/files/engine/support/datetime.js.map +1 -0
- package/dist/files/engine/support/document-visibility.d.ts +14 -0
- package/dist/files/engine/support/document-visibility.js +28 -0
- package/dist/files/engine/support/document-visibility.js.map +1 -0
- package/dist/files/engine/support/logger.d.ts +28 -0
- package/dist/files/engine/support/logger.js +36 -0
- package/dist/files/engine/support/logger.js.map +1 -0
- package/dist/files/engine/types.d.ts +1081 -0
- package/dist/files/engine/types.js +30 -0
- package/dist/files/engine/types.js.map +1 -0
- package/dist/files/engine/upload/cloudUpload.d.ts +182 -0
- package/dist/files/engine/upload/cloudUpload.js +418 -0
- package/dist/files/engine/upload/cloudUpload.js.map +1 -0
- package/dist/files/engine/upload/tusUpload.d.ts +100 -0
- package/dist/files/engine/upload/tusUpload.js +258 -0
- package/dist/files/engine/upload/tusUpload.js.map +1 -0
- package/dist/files/engine/upload/uploadDedupGuard.d.ts +58 -0
- package/dist/files/engine/upload/uploadDedupGuard.js +41 -0
- package/dist/files/engine/upload/uploadDedupGuard.js.map +1 -0
- package/dist/files/engine/upload/uploadGuardOpeners.d.ts +70 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js +54 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -0
- package/dist/files/engine/utils/file-types.d.ts +243 -0
- package/dist/files/engine/utils/file-types.js +1722 -0
- package/dist/files/engine/utils/file-types.js.map +1 -0
- package/dist/files/engine/utils/user-visible.d.ts +133 -0
- package/dist/files/engine/utils/user-visible.js +115 -0
- package/dist/files/engine/utils/user-visible.js.map +1 -0
- package/dist/files.cjs +1298 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +1171 -0
- package/dist/files.d.ts +1171 -0
- package/dist/files.js +1275 -0
- package/dist/files.js.map +1 -0
- package/package.json +404 -9
|
@@ -0,0 +1,1684 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
3
|
+
import { supabase } from "../host/supabase.js";
|
|
4
|
+
import { filesDb, FILE_VERSIONS_TABLE_COLUMNS } from "../filesDb.js";
|
|
5
|
+
import { pgErrorToError } from "@ai-matrx/data";
|
|
6
|
+
import * as Files from "../api/files.js";
|
|
7
|
+
import {
|
|
8
|
+
claimUpload,
|
|
9
|
+
uploadDedupKey
|
|
10
|
+
} from "../upload/uploadDedupGuard.js";
|
|
11
|
+
import * as Folders from "../api/folders.js";
|
|
12
|
+
import * as Permissions from "../api/permissions.js";
|
|
13
|
+
import * as Versions from "../api/versions.js";
|
|
14
|
+
import {
|
|
15
|
+
createShareLink as createCanonicalShareLink,
|
|
16
|
+
listShareLinks as listCanonicalShareLinks,
|
|
17
|
+
revokeShareLink as revokeCanonicalShareLink
|
|
18
|
+
} from "../host/share-links.js";
|
|
19
|
+
import {
|
|
20
|
+
restoreFileDirect,
|
|
21
|
+
restoreFolderDirect,
|
|
22
|
+
softDeleteFileDirect,
|
|
23
|
+
softDeleteFolderDirect
|
|
24
|
+
} from "../api/direct.js";
|
|
25
|
+
import { fileHandler } from "../handler/handler.js";
|
|
26
|
+
import { newRequestId } from "../host/python-client.js";
|
|
27
|
+
import { extractErrorMessage } from "@ai-matrx/data/net";
|
|
28
|
+
import {
|
|
29
|
+
apiFileRecordToCloudFile,
|
|
30
|
+
dbRowToCloudFilePermission,
|
|
31
|
+
dbRowToCloudFileVersion,
|
|
32
|
+
dbRowToCloudFolder,
|
|
33
|
+
canonicalShareLinkToCloudShareLink,
|
|
34
|
+
parseCloudTreeRows,
|
|
35
|
+
rpcPermissionRowToCloudFilePermission
|
|
36
|
+
} from "./converters.js";
|
|
37
|
+
import {
|
|
38
|
+
areCloudFileFieldsLoaded,
|
|
39
|
+
FILE_DB_RECORD_FIELDS,
|
|
40
|
+
fileHintToCloudFilePartial,
|
|
41
|
+
needsOnlyRenderFields
|
|
42
|
+
} from "./file-hydration.js";
|
|
43
|
+
import {
|
|
44
|
+
registerRequest,
|
|
45
|
+
releaseRequest,
|
|
46
|
+
beginResourceOp,
|
|
47
|
+
isLatestResourceOp,
|
|
48
|
+
hasInFlight
|
|
49
|
+
} from "./request-ledger.js";
|
|
50
|
+
import { toast } from "../host/toast.js";
|
|
51
|
+
import { buildTreeState } from "./tree-utils.js";
|
|
52
|
+
import {
|
|
53
|
+
isUserVisibleFilePath,
|
|
54
|
+
isUserVisibleFolderPath
|
|
55
|
+
} from "../utils/user-visible.js";
|
|
56
|
+
import {
|
|
57
|
+
hasMatchingFileTreeSession,
|
|
58
|
+
runFileTreeSessionOperation
|
|
59
|
+
} from "./file-tree-auth-boundary.js";
|
|
60
|
+
import {
|
|
61
|
+
isFileTreeLoadDeferred,
|
|
62
|
+
runWithFileTreeLoadTimeout
|
|
63
|
+
} from "./file-tree-timeout.js";
|
|
64
|
+
import {
|
|
65
|
+
invalidate as invalidateBlobCache,
|
|
66
|
+
replaceWithSavedBytes
|
|
67
|
+
} from "../hooks/blob-cache.js";
|
|
68
|
+
import { invalidateOfficeExtraction } from "../hooks/office-extraction-cache.js";
|
|
69
|
+
import {
|
|
70
|
+
addFilePendingRequest,
|
|
71
|
+
attachChildToFolder,
|
|
72
|
+
clearUpload,
|
|
73
|
+
detachChildFromFolder,
|
|
74
|
+
markFileSaved,
|
|
75
|
+
markFolderFullyLoaded,
|
|
76
|
+
removeFile,
|
|
77
|
+
removeFilePendingRequest,
|
|
78
|
+
removeFolder,
|
|
79
|
+
removePermissionForResource,
|
|
80
|
+
removeShareLink,
|
|
81
|
+
replaceTree,
|
|
82
|
+
rollbackFileOptimisticUpdate,
|
|
83
|
+
setFileError,
|
|
84
|
+
setFileField,
|
|
85
|
+
setFileLoading,
|
|
86
|
+
setTreeStatus,
|
|
87
|
+
trackUploadStart,
|
|
88
|
+
updateUploadProgress,
|
|
89
|
+
updateUploadStatus,
|
|
90
|
+
upsertFile,
|
|
91
|
+
upsertFiles,
|
|
92
|
+
upsertFolder,
|
|
93
|
+
upsertFolders,
|
|
94
|
+
upsertPermissionsForResource,
|
|
95
|
+
upsertShareLinksForResource,
|
|
96
|
+
upsertVersionsForFile
|
|
97
|
+
} from "./slice.js";
|
|
98
|
+
import { getFileFromState } from "./selectors.js";
|
|
99
|
+
import { getClaimsUser } from "../host/supabase.js";
|
|
100
|
+
const fileFieldHydrationInFlight = /* @__PURE__ */ new Map();
|
|
101
|
+
async function fetchCloudFileFields(fileId, fields) {
|
|
102
|
+
const key = `${fileId}:${[...fields].sort().join(",")}`;
|
|
103
|
+
const existing = fileFieldHydrationInFlight.get(key);
|
|
104
|
+
if (existing) return existing;
|
|
105
|
+
const request = (async () => {
|
|
106
|
+
const { data } = await Files.getFileMetadata(fileId);
|
|
107
|
+
const record = apiFileRecordToCloudFile(data);
|
|
108
|
+
if (record.deletedAt) return null;
|
|
109
|
+
if (needsOnlyRenderFields(fields)) {
|
|
110
|
+
return {
|
|
111
|
+
id: record.id,
|
|
112
|
+
fileName: record.fileName,
|
|
113
|
+
mimeType: record.mimeType,
|
|
114
|
+
fileSize: record.fileSize,
|
|
115
|
+
visibility: record.visibility
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return record;
|
|
119
|
+
})().finally(() => fileFieldHydrationInFlight.delete(key));
|
|
120
|
+
fileFieldHydrationInFlight.set(key, request);
|
|
121
|
+
return request;
|
|
122
|
+
}
|
|
123
|
+
const ensureCloudFileFields = createAsyncThunk(
|
|
124
|
+
"cloudFiles/ensureCloudFileFields",
|
|
125
|
+
async ({ fileId, fields = FILE_DB_RECORD_FIELDS, hint }, { dispatch, getState }) => {
|
|
126
|
+
dispatch(upsertFile(fileHintToCloudFilePartial(fileId, hint)));
|
|
127
|
+
if (areCloudFileFieldsLoaded(getFileFromState(getState(), fileId), fields)) {
|
|
128
|
+
return "ready";
|
|
129
|
+
}
|
|
130
|
+
dispatch(setFileLoading({ id: fileId, loading: true }));
|
|
131
|
+
dispatch(setFileError({ id: fileId, error: null }));
|
|
132
|
+
try {
|
|
133
|
+
const partial = await fetchCloudFileFields(fileId, fields);
|
|
134
|
+
if (!partial) return "missing";
|
|
135
|
+
dispatch(upsertFile(partial));
|
|
136
|
+
return "ready";
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const normalized = pgErrorToError(error);
|
|
139
|
+
dispatch(setFileError({ id: fileId, error: normalized.message }));
|
|
140
|
+
throw normalized;
|
|
141
|
+
} finally {
|
|
142
|
+
dispatch(setFileLoading({ id: fileId, loading: false }));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
const TREE_PAGE_SIZE = 5e3;
|
|
147
|
+
const TREE_MAX_PAGES = 20;
|
|
148
|
+
let _treeInFlight = null;
|
|
149
|
+
let _treeInFlightUserId = null;
|
|
150
|
+
const loadUserFileTree = createAsyncThunk("cloudFiles/loadUserFileTree", async ({ userId }, { dispatch, getState }) => {
|
|
151
|
+
if (!await hasMatchingFileTreeSession(userId)) return;
|
|
152
|
+
if (_treeInFlight && _treeInFlightUserId === userId) return _treeInFlight;
|
|
153
|
+
const run = (async () => {
|
|
154
|
+
dispatch(setTreeStatus({ status: "loading" }));
|
|
155
|
+
try {
|
|
156
|
+
const rows = await runWithFileTreeLoadTimeout(async (signal) => {
|
|
157
|
+
const rows2 = [];
|
|
158
|
+
for (let page = 0; page < TREE_MAX_PAGES; page += 1) {
|
|
159
|
+
const { data, error } = await runFileTreeSessionOperation(
|
|
160
|
+
() => supabase.rpc("get_user_file_tree", {
|
|
161
|
+
p_user_id: userId,
|
|
162
|
+
p_limit: TREE_PAGE_SIZE,
|
|
163
|
+
p_offset: page * TREE_PAGE_SIZE,
|
|
164
|
+
p_include_folders: true,
|
|
165
|
+
p_include_deleted: false
|
|
166
|
+
}).abortSignal(signal)
|
|
167
|
+
);
|
|
168
|
+
if (error) {
|
|
169
|
+
if (!signal.aborted) {
|
|
170
|
+
dispatch(
|
|
171
|
+
setTreeStatus({ status: "error", error: error.message })
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const pageRows = parseCloudTreeRows(data);
|
|
177
|
+
rows2.push(...pageRows);
|
|
178
|
+
if (pageRows.length < TREE_PAGE_SIZE) break;
|
|
179
|
+
}
|
|
180
|
+
return rows2;
|
|
181
|
+
});
|
|
182
|
+
const files = [];
|
|
183
|
+
const folders = [];
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
if (row.kind === "file") {
|
|
186
|
+
files.push({
|
|
187
|
+
id: row.id,
|
|
188
|
+
ownerId: row.owner_id,
|
|
189
|
+
filePath: row.file_path,
|
|
190
|
+
fileName: row.file_name,
|
|
191
|
+
parentFolderId: row.parent_folder_id,
|
|
192
|
+
mimeType: row.mime_type,
|
|
193
|
+
// Phase 0 rename: `file_size` → `size_bytes`. The
|
|
194
|
+
// `CloudTreeFileRow` shape and Supabase row both carry the new
|
|
195
|
+
// name now. See docs/PYTHON_UPDATES.md §3.
|
|
196
|
+
fileSize: row.size_bytes,
|
|
197
|
+
visibility: row.visibility,
|
|
198
|
+
currentVersion: row.current_version,
|
|
199
|
+
createdAt: row.created_at,
|
|
200
|
+
updatedAt: row.updated_at,
|
|
201
|
+
deletedAt: row.deleted_at,
|
|
202
|
+
originDeviceId: row.origin_device_id,
|
|
203
|
+
metadata: row.metadata
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
folders.push({
|
|
207
|
+
id: row.id,
|
|
208
|
+
ownerId: row.owner_id,
|
|
209
|
+
folderPath: row.folder_path,
|
|
210
|
+
folderName: row.folder_name,
|
|
211
|
+
parentId: row.parent_id,
|
|
212
|
+
visibility: row.visibility,
|
|
213
|
+
createdAt: row.created_at,
|
|
214
|
+
updatedAt: row.updated_at,
|
|
215
|
+
deletedAt: row.deleted_at,
|
|
216
|
+
metadata: row.metadata
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
dispatch(upsertFiles(files));
|
|
221
|
+
dispatch(upsertFolders(folders));
|
|
222
|
+
const prior = getState().cloudFiles;
|
|
223
|
+
for (const [id, rec] of Object.entries(prior.filesById)) {
|
|
224
|
+
if (rec && !isUserVisibleFilePath(rec.filePath)) {
|
|
225
|
+
dispatch(removeFile({ id }));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const [id, rec] of Object.entries(prior.foldersById)) {
|
|
229
|
+
if (rec && rec.source.kind === "real" && !isUserVisibleFolderPath(rec.folderPath)) {
|
|
230
|
+
dispatch(removeFolder({ id }));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const fileIds = files.map((f) => f.id).filter(Boolean);
|
|
234
|
+
const folderIds = folders.map((f) => f.id).filter(Boolean);
|
|
235
|
+
const tree = buildTreeState({
|
|
236
|
+
fileIds,
|
|
237
|
+
folderIds,
|
|
238
|
+
filesById: Object.fromEntries(
|
|
239
|
+
files.map((f) => [
|
|
240
|
+
f.id,
|
|
241
|
+
{
|
|
242
|
+
id: f.id,
|
|
243
|
+
ownerId: f.ownerId,
|
|
244
|
+
filePath: f.filePath,
|
|
245
|
+
fileName: f.fileName,
|
|
246
|
+
mimeType: f.mimeType ?? null,
|
|
247
|
+
fileSize: f.fileSize ?? null,
|
|
248
|
+
checksum: null,
|
|
249
|
+
visibility: f.visibility,
|
|
250
|
+
currentVersion: f.currentVersion,
|
|
251
|
+
parentFolderId: f.parentFolderId ?? null,
|
|
252
|
+
metadata: f.metadata ?? {},
|
|
253
|
+
createdAt: f.createdAt,
|
|
254
|
+
updatedAt: f.updatedAt,
|
|
255
|
+
deletedAt: f.deletedAt,
|
|
256
|
+
// Tree-spine reconstruction is internal — the computed URL fields
|
|
257
|
+
// and the Phase-1b backend thumbnail_url are only populated when
|
|
258
|
+
// records arrive via the REST API. Default all to null here; the
|
|
259
|
+
// file grid (`MediaThumbnail`) falls through to `useFileAsset` →
|
|
260
|
+
// `Asset.variants["thumbnail_url"]` when these are absent.
|
|
261
|
+
publicUrl: null,
|
|
262
|
+
url: null,
|
|
263
|
+
cdnUrl: null,
|
|
264
|
+
downloadUrl: null,
|
|
265
|
+
thumbnailUrl: null,
|
|
266
|
+
source: { kind: "real" },
|
|
267
|
+
_dirty: false,
|
|
268
|
+
_dirtyFields: {},
|
|
269
|
+
_fieldHistory: {},
|
|
270
|
+
_loadedFields: {},
|
|
271
|
+
_loading: false,
|
|
272
|
+
_error: null,
|
|
273
|
+
_pendingRequestIds: []
|
|
274
|
+
}
|
|
275
|
+
])
|
|
276
|
+
),
|
|
277
|
+
foldersById: Object.fromEntries(
|
|
278
|
+
folders.map((f) => [
|
|
279
|
+
f.id,
|
|
280
|
+
{
|
|
281
|
+
id: f.id,
|
|
282
|
+
ownerId: f.ownerId,
|
|
283
|
+
folderPath: f.folderPath,
|
|
284
|
+
folderName: f.folderName,
|
|
285
|
+
parentId: f.parentId ?? null,
|
|
286
|
+
visibility: f.visibility,
|
|
287
|
+
metadata: f.metadata ?? {},
|
|
288
|
+
createdAt: f.createdAt,
|
|
289
|
+
updatedAt: f.updatedAt,
|
|
290
|
+
deletedAt: f.deletedAt,
|
|
291
|
+
// Tree-spine reconstruction is internal — public_url is only
|
|
292
|
+
// populated when records arrive via the API. Default to null
|
|
293
|
+
// here; surfaces that need a CDN URL fetch via useFileSrc.
|
|
294
|
+
publicUrl: null,
|
|
295
|
+
source: { kind: "real" },
|
|
296
|
+
_dirty: false,
|
|
297
|
+
_dirtyFields: {},
|
|
298
|
+
_fieldHistory: {},
|
|
299
|
+
_loadedFields: {},
|
|
300
|
+
_loading: false,
|
|
301
|
+
_error: null,
|
|
302
|
+
_pendingRequestIds: []
|
|
303
|
+
}
|
|
304
|
+
])
|
|
305
|
+
)
|
|
306
|
+
});
|
|
307
|
+
dispatch(
|
|
308
|
+
replaceTree({
|
|
309
|
+
rootFolderIds: tree.rootFolderIds,
|
|
310
|
+
rootFileIds: tree.rootFileIds,
|
|
311
|
+
childrenByFolderId: tree.childrenByFolderId,
|
|
312
|
+
// This RPC returns the complete discoverable tree, not one lazy level.
|
|
313
|
+
// Mark every real folder so navigation never re-downloads up to 100k
|
|
314
|
+
// rows merely because the user opened a different folder.
|
|
315
|
+
fullyLoadedFolderIds: Object.fromEntries(
|
|
316
|
+
folderIds.map((id) => [id, true])
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (isFileTreeLoadDeferred(error)) {
|
|
322
|
+
dispatch(setTreeStatus({ status: "idle" }));
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
const message = error instanceof Error ? error.message : "Could not load your file library.";
|
|
326
|
+
dispatch(setTreeStatus({ status: "error", error: message }));
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
})();
|
|
330
|
+
_treeInFlight = run;
|
|
331
|
+
_treeInFlightUserId = userId;
|
|
332
|
+
try {
|
|
333
|
+
await run;
|
|
334
|
+
} finally {
|
|
335
|
+
if (_treeInFlight === run) {
|
|
336
|
+
_treeInFlight = null;
|
|
337
|
+
_treeInFlightUserId = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const reconcileTree = createAsyncThunk("cloudFiles/reconcileTree", async ({ userId }, { dispatch }) => {
|
|
342
|
+
await dispatch(loadUserFileTree({ userId })).unwrap();
|
|
343
|
+
});
|
|
344
|
+
const loadFolderContents = createAsyncThunk("cloudFiles/loadFolderContents", async ({ folderId }, { dispatch }) => {
|
|
345
|
+
if (folderId.startsWith("vfs:")) return;
|
|
346
|
+
const { data, error } = await getClaimsUser(supabase);
|
|
347
|
+
if (error) throw error;
|
|
348
|
+
if (!data.user?.id)
|
|
349
|
+
throw new Error("Cannot load folder contents without a user");
|
|
350
|
+
await dispatch(loadUserFileTree({ userId: data.user.id })).unwrap();
|
|
351
|
+
dispatch(markFolderFullyLoaded({ folderId }));
|
|
352
|
+
});
|
|
353
|
+
function isVirtualResourceId(id) {
|
|
354
|
+
return id.startsWith("vfs:");
|
|
355
|
+
}
|
|
356
|
+
const loadFileVersions = createAsyncThunk("cloudFiles/loadFileVersions", async ({ fileId }, { dispatch }) => {
|
|
357
|
+
if (isVirtualResourceId(fileId)) return;
|
|
358
|
+
const { data, error } = await filesDb(supabase).from("file_versions").select(FILE_VERSIONS_TABLE_COLUMNS).eq("file_id", fileId).order("version_number", { ascending: false });
|
|
359
|
+
if (error) throw pgErrorToError(error);
|
|
360
|
+
const versions = (data ?? []).map(
|
|
361
|
+
dbRowToCloudFileVersion
|
|
362
|
+
);
|
|
363
|
+
dispatch(upsertVersionsForFile({ fileId, versions }));
|
|
364
|
+
});
|
|
365
|
+
const loadPermissions = createAsyncThunk("cloudFiles/loadPermissions", async ({ resourceId }, { dispatch }) => {
|
|
366
|
+
if (isVirtualResourceId(resourceId)) return;
|
|
367
|
+
const { data, error } = await supabase.schema("iam").from("permissions").select("*").eq("resource_type", "file").eq("resource_id", resourceId);
|
|
368
|
+
if (error) throw pgErrorToError(error);
|
|
369
|
+
const permissions = (data ?? []).map(
|
|
370
|
+
dbRowToCloudFilePermission
|
|
371
|
+
);
|
|
372
|
+
dispatch(upsertPermissionsForResource({ resourceId, permissions }));
|
|
373
|
+
});
|
|
374
|
+
const loadShareLinks = createAsyncThunk(
|
|
375
|
+
"cloudFiles/loadShareLinks",
|
|
376
|
+
async ({ resourceId, resourceType = "file" }, { dispatch }) => {
|
|
377
|
+
if (isVirtualResourceId(resourceId)) return;
|
|
378
|
+
const links = await listCanonicalShareLinks(resourceType, resourceId);
|
|
379
|
+
const shareLinks = links.filter((l) => l.isActive).map(
|
|
380
|
+
(l) => canonicalShareLinkToCloudShareLink(l, resourceType, resourceId)
|
|
381
|
+
);
|
|
382
|
+
dispatch(upsertShareLinksForResource({ resourceId, shareLinks }));
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
const createFolder = createAsyncThunk("cloudFiles/createFolder", async (arg, { dispatch, getState }) => {
|
|
386
|
+
const folderName = arg.folderName.trim();
|
|
387
|
+
if (!folderName) {
|
|
388
|
+
throw new Error("Folder name cannot be empty.");
|
|
389
|
+
}
|
|
390
|
+
if (/[/\\]/.test(folderName)) {
|
|
391
|
+
throw new Error("Folder names cannot contain '/' or '\\'.");
|
|
392
|
+
}
|
|
393
|
+
if (/[\x00-\x1f\x7f]/.test(folderName)) {
|
|
394
|
+
throw new Error("Folder names cannot contain control characters.");
|
|
395
|
+
}
|
|
396
|
+
if (folderName.endsWith(".")) {
|
|
397
|
+
throw new Error("Folder names cannot end with '.'.");
|
|
398
|
+
}
|
|
399
|
+
{
|
|
400
|
+
const state = getState().cloudFiles;
|
|
401
|
+
const lowered = folderName.toLowerCase();
|
|
402
|
+
const collision = Object.values(state.foldersById).some(
|
|
403
|
+
(f) => !!f && !f.deletedAt && (f.parentId ?? null) === (arg.parentId ?? null) && f.folderName.toLowerCase() === lowered
|
|
404
|
+
);
|
|
405
|
+
if (collision) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`A folder named '${folderName}' already exists in this location.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const requestId = newRequestId();
|
|
412
|
+
registerRequest({
|
|
413
|
+
requestId,
|
|
414
|
+
kind: "folder-create",
|
|
415
|
+
resourceId: null,
|
|
416
|
+
resourceType: "folder"
|
|
417
|
+
});
|
|
418
|
+
let folderPath = folderName;
|
|
419
|
+
if (arg.parentId !== null) {
|
|
420
|
+
const parent = getState().cloudFiles.foldersById[arg.parentId];
|
|
421
|
+
if (!parent) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
"We couldn't create the folder here \u2014 this view is out of date. Refresh and try again."
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
folderPath = `${parent.folderPath}/${folderName}`;
|
|
427
|
+
}
|
|
428
|
+
const body = {
|
|
429
|
+
folder_path: folderPath,
|
|
430
|
+
visibility: arg.visibility ?? "personal",
|
|
431
|
+
metadata: arg.metadata ?? null
|
|
432
|
+
};
|
|
433
|
+
try {
|
|
434
|
+
const { data: row } = await Folders.createFolder(body, { requestId });
|
|
435
|
+
const folder = dbRowToCloudFolder(row);
|
|
436
|
+
dispatch(upsertFolder(folder));
|
|
437
|
+
dispatch(
|
|
438
|
+
attachChildToFolder({
|
|
439
|
+
parentFolderId: folder.parentId,
|
|
440
|
+
kind: "folder",
|
|
441
|
+
id: folder.id
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
return folder.id;
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (true) {
|
|
447
|
+
const e = err;
|
|
448
|
+
console.error("[createFolder] backend error", {
|
|
449
|
+
status: e.status ?? null,
|
|
450
|
+
code: e.code,
|
|
451
|
+
detail: e.detail,
|
|
452
|
+
userMessage: e.userMessage,
|
|
453
|
+
details: e.details,
|
|
454
|
+
sentBody: body
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
throw err;
|
|
458
|
+
} finally {
|
|
459
|
+
releaseRequest(requestId);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
const updateFolder = createAsyncThunk("cloudFiles/updateFolder", async (arg, { dispatch, getState }) => {
|
|
463
|
+
const state = getState();
|
|
464
|
+
const folder = state.cloudFiles.foldersById[arg.folderId];
|
|
465
|
+
if (!folder)
|
|
466
|
+
throw new Error(
|
|
467
|
+
"We couldn't update that folder \u2014 this view is out of date. Refresh and try again."
|
|
468
|
+
);
|
|
469
|
+
const requestId = newRequestId();
|
|
470
|
+
registerRequest({
|
|
471
|
+
requestId,
|
|
472
|
+
kind: "folder-update",
|
|
473
|
+
resourceId: arg.folderId,
|
|
474
|
+
resourceType: "folder"
|
|
475
|
+
});
|
|
476
|
+
const patch = arg.patch;
|
|
477
|
+
const optimistic = {
|
|
478
|
+
...folder,
|
|
479
|
+
...patch.folderName !== void 0 ? { folderName: patch.folderName } : {},
|
|
480
|
+
...patch.parentId !== void 0 ? { parentId: patch.parentId } : {},
|
|
481
|
+
...patch.visibility !== void 0 ? { visibility: patch.visibility } : {},
|
|
482
|
+
...patch.metadata !== void 0 ? { metadata: patch.metadata } : {}
|
|
483
|
+
};
|
|
484
|
+
dispatch(upsertFolder(optimistic));
|
|
485
|
+
if (patch.parentId !== void 0 && patch.parentId !== folder.parentId) {
|
|
486
|
+
dispatch(
|
|
487
|
+
detachChildFromFolder({
|
|
488
|
+
parentFolderId: folder.parentId,
|
|
489
|
+
kind: "folder",
|
|
490
|
+
id: folder.id
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
dispatch(
|
|
494
|
+
attachChildToFolder({
|
|
495
|
+
parentFolderId: patch.parentId,
|
|
496
|
+
kind: "folder",
|
|
497
|
+
id: folder.id
|
|
498
|
+
})
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
let targetFolderPath;
|
|
502
|
+
if (patch.folderName !== void 0 || patch.parentId !== void 0) {
|
|
503
|
+
const newName = patch.folderName ?? folder.folderName;
|
|
504
|
+
const newParentId = patch.parentId !== void 0 ? patch.parentId : folder.parentId;
|
|
505
|
+
if (newParentId) {
|
|
506
|
+
const newParent = state.cloudFiles.foldersById[newParentId];
|
|
507
|
+
if (!newParent) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"We couldn't move the folder there \u2014 this view is out of date. Refresh and try again."
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
targetFolderPath = `${newParent.folderPath}/${newName}`;
|
|
513
|
+
} else {
|
|
514
|
+
targetFolderPath = newName;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const { data: row } = await Folders.patchFolder(
|
|
519
|
+
arg.folderId,
|
|
520
|
+
{
|
|
521
|
+
folder_path: targetFolderPath,
|
|
522
|
+
visibility: patch.visibility,
|
|
523
|
+
metadata: patch.metadata ?? null
|
|
524
|
+
},
|
|
525
|
+
{ requestId }
|
|
526
|
+
);
|
|
527
|
+
dispatch(upsertFolder(dbRowToCloudFolder(row)));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
dispatch(upsertFolder(toCloudFolderPartial(folder)));
|
|
530
|
+
if (patch.parentId !== void 0 && patch.parentId !== folder.parentId) {
|
|
531
|
+
dispatch(
|
|
532
|
+
detachChildFromFolder({
|
|
533
|
+
parentFolderId: patch.parentId,
|
|
534
|
+
kind: "folder",
|
|
535
|
+
id: folder.id
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
dispatch(
|
|
539
|
+
attachChildToFolder({
|
|
540
|
+
parentFolderId: folder.parentId,
|
|
541
|
+
kind: "folder",
|
|
542
|
+
id: folder.id
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
throw err;
|
|
547
|
+
} finally {
|
|
548
|
+
releaseRequest(requestId);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
const deleteFolder = createAsyncThunk("cloudFiles/deleteFolder", async (arg, { dispatch, getState }) => {
|
|
552
|
+
const state = getState();
|
|
553
|
+
const folder = state.cloudFiles.foldersById[arg.folderId];
|
|
554
|
+
if (!folder) return;
|
|
555
|
+
const requestId = newRequestId();
|
|
556
|
+
registerRequest({
|
|
557
|
+
requestId,
|
|
558
|
+
kind: "folder-delete",
|
|
559
|
+
resourceId: arg.folderId,
|
|
560
|
+
resourceType: "folder"
|
|
561
|
+
});
|
|
562
|
+
try {
|
|
563
|
+
await softDeleteFolderDirect(arg.folderId);
|
|
564
|
+
dispatch(removeFolder({ id: arg.folderId }));
|
|
565
|
+
} finally {
|
|
566
|
+
releaseRequest(requestId);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
const ensureFolderPath = createAsyncThunk("cloudFiles/ensureFolderPath", async (arg, { dispatch, getState }) => {
|
|
570
|
+
const normalizedPath = arg.folderPath.split("/").map((s) => s.trim()).filter(Boolean).join("/");
|
|
571
|
+
if (!normalizedPath) {
|
|
572
|
+
throw new Error("folderPath cannot be empty.");
|
|
573
|
+
}
|
|
574
|
+
const existing = Object.values(getState().cloudFiles.foldersById).find(
|
|
575
|
+
(folder) => folder.folderPath === normalizedPath && !folder.deletedAt
|
|
576
|
+
);
|
|
577
|
+
if (existing) {
|
|
578
|
+
return existing.id;
|
|
579
|
+
}
|
|
580
|
+
const requestId = newRequestId();
|
|
581
|
+
registerRequest({
|
|
582
|
+
requestId,
|
|
583
|
+
kind: "folder-create",
|
|
584
|
+
resourceId: null,
|
|
585
|
+
resourceType: "folder"
|
|
586
|
+
});
|
|
587
|
+
try {
|
|
588
|
+
const { data: row } = await Folders.ensureFolderPath(
|
|
589
|
+
normalizedPath,
|
|
590
|
+
arg.visibility ?? "personal",
|
|
591
|
+
{ requestId }
|
|
592
|
+
);
|
|
593
|
+
const folder = dbRowToCloudFolder(row);
|
|
594
|
+
dispatch(upsertFolder(folder));
|
|
595
|
+
dispatch(
|
|
596
|
+
attachChildToFolder({
|
|
597
|
+
parentFolderId: folder.parentId,
|
|
598
|
+
kind: "folder",
|
|
599
|
+
id: folder.id
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
return folder.id;
|
|
603
|
+
} finally {
|
|
604
|
+
releaseRequest(requestId);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
function announceUploadFailures(failed) {
|
|
608
|
+
if (failed.length === 0) return;
|
|
609
|
+
const reasons = Array.from(
|
|
610
|
+
new Set(failed.map((f) => f.error).filter((e) => e && e.trim().length > 0))
|
|
611
|
+
);
|
|
612
|
+
const title = failed.length === 1 ? `Couldn't upload ${failed[0].name}` : `Couldn't upload ${failed.length} files`;
|
|
613
|
+
toast.error(title, {
|
|
614
|
+
// The server's own sentence, never a sentence of ours. When several
|
|
615
|
+
// distinct things went wrong, each one gets said.
|
|
616
|
+
description: reasons.length > 0 ? reasons.join(" \xB7 ") : "The server refused the upload and gave no reason.",
|
|
617
|
+
id: `upload-failed:${failed.map((f) => f.name).join("|")}`,
|
|
618
|
+
duration: 12e3
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const uploadFiles = createAsyncThunk("cloudFiles/uploadFiles", async (arg, { dispatch, getState }) => {
|
|
622
|
+
const concurrency = Math.max(1, arg.concurrency ?? 3);
|
|
623
|
+
const uploaded = [];
|
|
624
|
+
const failed = [];
|
|
625
|
+
const state = getState();
|
|
626
|
+
let prefix = "";
|
|
627
|
+
if (arg.folderPath) {
|
|
628
|
+
prefix = `${arg.folderPath.replace(/^\/+|\/+$/g, "")}/`;
|
|
629
|
+
} else if (arg.parentFolderId) {
|
|
630
|
+
const parentFolder = state.cloudFiles.foldersById[arg.parentFolderId];
|
|
631
|
+
if (parentFolder) {
|
|
632
|
+
prefix = `${parentFolder.folderPath}/`;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const existingNames = (() => {
|
|
636
|
+
const out = /* @__PURE__ */ new Set();
|
|
637
|
+
const targetPrefix = prefix;
|
|
638
|
+
for (const f of Object.values(state.cloudFiles.filesById)) {
|
|
639
|
+
if (!f) continue;
|
|
640
|
+
if (f.deletedAt) continue;
|
|
641
|
+
if (!f.filePath) continue;
|
|
642
|
+
const normalized = f.filePath.replace(/^\/+/, "");
|
|
643
|
+
if (!normalized.startsWith(targetPrefix)) continue;
|
|
644
|
+
const tail = normalized.slice(targetPrefix.length);
|
|
645
|
+
if (tail.includes("/")) continue;
|
|
646
|
+
if (tail) out.add(tail.toLowerCase());
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
})();
|
|
650
|
+
function uniqueName(originalName) {
|
|
651
|
+
const lowered = originalName.toLowerCase();
|
|
652
|
+
if (!existingNames.has(lowered)) {
|
|
653
|
+
existingNames.add(lowered);
|
|
654
|
+
return originalName;
|
|
655
|
+
}
|
|
656
|
+
const dot = originalName.lastIndexOf(".");
|
|
657
|
+
const isHidden = originalName.startsWith(".");
|
|
658
|
+
const stem = dot > 0 && !isHidden ? originalName.slice(0, dot) : originalName;
|
|
659
|
+
const ext = dot > 0 && !isHidden ? originalName.slice(dot) : "";
|
|
660
|
+
for (let i = 1; i < 1e3; i++) {
|
|
661
|
+
const candidate = `${stem} (${i})${ext}`;
|
|
662
|
+
if (!existingNames.has(candidate.toLowerCase())) {
|
|
663
|
+
existingNames.add(candidate.toLowerCase());
|
|
664
|
+
return candidate;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return originalName;
|
|
668
|
+
}
|
|
669
|
+
const overrides = arg.filenameOverrides ?? {};
|
|
670
|
+
const skipSet = new Set(arg.skipIndices ?? []);
|
|
671
|
+
const forceNewCopySet = new Set(arg.forceNewCopyIndices ?? []);
|
|
672
|
+
for (const [, name] of Object.entries(overrides)) {
|
|
673
|
+
if (typeof name === "string") existingNames.add(name.toLowerCase());
|
|
674
|
+
}
|
|
675
|
+
const queue = arg.files.map((file, index) => ({ index, file })).filter(({ index }) => !skipSet.has(index));
|
|
676
|
+
async function worker() {
|
|
677
|
+
while (queue.length) {
|
|
678
|
+
const next = queue.shift();
|
|
679
|
+
if (!next) return;
|
|
680
|
+
const { file, index } = next;
|
|
681
|
+
const overrideName = overrides[index];
|
|
682
|
+
const forcedCopy = forceNewCopySet.has(index);
|
|
683
|
+
const dedupKey = !overrideName && !forcedCopy ? uploadDedupKey(prefix, file.name, file.size) : null;
|
|
684
|
+
let dedupClaim = null;
|
|
685
|
+
if (dedupKey) {
|
|
686
|
+
const claim = claimUpload(dedupKey);
|
|
687
|
+
if (claim.shared) {
|
|
688
|
+
try {
|
|
689
|
+
const outcome = await claim.promise;
|
|
690
|
+
uploaded.push(outcome.fileId);
|
|
691
|
+
continue;
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
dedupClaim = claim;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const requestId = newRequestId();
|
|
699
|
+
const targetName = typeof overrideName === "string" && overrideName ? overrideName : uniqueName(file.name);
|
|
700
|
+
dispatch(
|
|
701
|
+
trackUploadStart({
|
|
702
|
+
requestId,
|
|
703
|
+
fileName: targetName,
|
|
704
|
+
fileSize: file.size,
|
|
705
|
+
parentFolderId: arg.parentFolderId ?? null,
|
|
706
|
+
folderPath: arg.folderPath ?? null
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
registerRequest({
|
|
710
|
+
requestId,
|
|
711
|
+
kind: "upload",
|
|
712
|
+
resourceId: null,
|
|
713
|
+
resourceType: "file"
|
|
714
|
+
});
|
|
715
|
+
try {
|
|
716
|
+
const { data } = await Files.uploadFileWithProgress(
|
|
717
|
+
{
|
|
718
|
+
file,
|
|
719
|
+
filePath: `${prefix}${targetName}`,
|
|
720
|
+
visibility: arg.visibility ?? "personal",
|
|
721
|
+
shareWith: arg.shareWith,
|
|
722
|
+
shareLevel: arg.shareLevel,
|
|
723
|
+
changeSummary: arg.changeSummary,
|
|
724
|
+
metadata: arg.metadata,
|
|
725
|
+
options: arg.options,
|
|
726
|
+
...forceNewCopySet.has(index) ? {
|
|
727
|
+
intent: "force_new_copy",
|
|
728
|
+
reason: "User selected Make a copy in the duplicate upload dialog"
|
|
729
|
+
} : {}
|
|
730
|
+
},
|
|
731
|
+
(ev) => dispatch(
|
|
732
|
+
updateUploadProgress({
|
|
733
|
+
requestId,
|
|
734
|
+
bytesUploaded: ev.loaded
|
|
735
|
+
})
|
|
736
|
+
),
|
|
737
|
+
{
|
|
738
|
+
requestId,
|
|
739
|
+
// Idempotency key — same value across automatic retries of the
|
|
740
|
+
// same intended upload. The backend stores it in
|
|
741
|
+
// `metadata._idempotency_key` so retries don't create duplicate
|
|
742
|
+
// version rows. We reuse `requestId` because it's generated
|
|
743
|
+
// once per intended upload, before any retry.
|
|
744
|
+
idempotencyKey: requestId
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
dispatch(
|
|
748
|
+
upsertFile(
|
|
749
|
+
apiFileRecordToCloudFile({
|
|
750
|
+
id: data.file_id,
|
|
751
|
+
owner_id: "",
|
|
752
|
+
file_path: data.file_path,
|
|
753
|
+
file_name: data.file_path.split("/").pop() ?? targetName,
|
|
754
|
+
mime_type: file.type || null,
|
|
755
|
+
// Phase 0 rename — see docs/PYTHON_UPDATES.md §3.
|
|
756
|
+
size_bytes: data.size_bytes,
|
|
757
|
+
checksum: data.checksum,
|
|
758
|
+
visibility: arg.visibility ?? "personal",
|
|
759
|
+
current_version: data.version_number,
|
|
760
|
+
parent_folder_id: arg.parentFolderId ?? null,
|
|
761
|
+
metadata: arg.metadata ?? {},
|
|
762
|
+
// P1-7: stamp the optimistic row with the current time instead of
|
|
763
|
+
// null. The upload just happened, so "now" is accurate for the
|
|
764
|
+
// recents filter / "sort by modified" until the realtime echo
|
|
765
|
+
// delivers the authoritative server timestamp. Null sorted the
|
|
766
|
+
// freshly-uploaded file to the wrong end of the list.
|
|
767
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
768
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
769
|
+
deleted_at: null
|
|
770
|
+
})
|
|
771
|
+
)
|
|
772
|
+
);
|
|
773
|
+
invalidateBlobCache(data.file_id);
|
|
774
|
+
invalidateOfficeExtraction(data.file_id);
|
|
775
|
+
dispatch(
|
|
776
|
+
attachChildToFolder({
|
|
777
|
+
parentFolderId: arg.parentFolderId ?? null,
|
|
778
|
+
kind: "file",
|
|
779
|
+
id: data.file_id
|
|
780
|
+
})
|
|
781
|
+
);
|
|
782
|
+
dispatch(
|
|
783
|
+
updateUploadStatus({
|
|
784
|
+
requestId,
|
|
785
|
+
status: "success",
|
|
786
|
+
fileId: data.file_id
|
|
787
|
+
})
|
|
788
|
+
);
|
|
789
|
+
uploaded.push(data.file_id);
|
|
790
|
+
dedupClaim?.settle(
|
|
791
|
+
Promise.resolve({ fileId: data.file_id, filePath: data.file_path })
|
|
792
|
+
);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
const message = extractErrorMessage(err);
|
|
795
|
+
dispatch(
|
|
796
|
+
updateUploadStatus({
|
|
797
|
+
requestId,
|
|
798
|
+
status: "error",
|
|
799
|
+
error: message
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
failed.push({ name: file.name, error: message });
|
|
803
|
+
dedupClaim?.settle(Promise.reject(err));
|
|
804
|
+
} finally {
|
|
805
|
+
releaseRequest(requestId);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
await Promise.all(
|
|
810
|
+
Array.from(
|
|
811
|
+
{ length: Math.min(concurrency, arg.files.length) },
|
|
812
|
+
() => worker()
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
announceUploadFailures(failed);
|
|
816
|
+
return { uploaded, failed };
|
|
817
|
+
});
|
|
818
|
+
const clearUploadEntry = createAsyncThunk("cloudFiles/clearUpload", async ({ requestId }, { dispatch }) => {
|
|
819
|
+
dispatch(clearUpload({ requestId }));
|
|
820
|
+
});
|
|
821
|
+
const renameFile = createAsyncThunk(
|
|
822
|
+
"cloudFiles/rename",
|
|
823
|
+
async ({ fileId, newName }, { dispatch, getState }) => {
|
|
824
|
+
const record = getFileFromState(getState(), fileId);
|
|
825
|
+
if (!record)
|
|
826
|
+
throw new Error(
|
|
827
|
+
"We couldn't find that file in this view. Refresh and try again."
|
|
828
|
+
);
|
|
829
|
+
const requestId = newRequestId();
|
|
830
|
+
const seq = beginResourceOp(fileId);
|
|
831
|
+
const snapshot = {
|
|
832
|
+
fileName: record.fileName,
|
|
833
|
+
filePath: record.filePath
|
|
834
|
+
};
|
|
835
|
+
const pathParts = record.filePath.split("/");
|
|
836
|
+
pathParts[pathParts.length - 1] = newName;
|
|
837
|
+
const newPath = pathParts.join("/");
|
|
838
|
+
dispatch(setFileField({ id: fileId, field: "fileName", value: newName }));
|
|
839
|
+
dispatch(setFileField({ id: fileId, field: "filePath", value: newPath }));
|
|
840
|
+
dispatch(addFilePendingRequest({ id: fileId, requestId }));
|
|
841
|
+
registerRequest({
|
|
842
|
+
requestId,
|
|
843
|
+
kind: "rename",
|
|
844
|
+
resourceId: fileId,
|
|
845
|
+
resourceType: "file"
|
|
846
|
+
});
|
|
847
|
+
try {
|
|
848
|
+
const { data } = await Files.renameFile(
|
|
849
|
+
fileId,
|
|
850
|
+
{ new_path: newPath },
|
|
851
|
+
{ requestId }
|
|
852
|
+
);
|
|
853
|
+
if (isLatestResourceOp(fileId, seq)) {
|
|
854
|
+
dispatch(upsertFile(apiFileRecordToCloudFile(data)));
|
|
855
|
+
dispatch(markFileSaved({ id: fileId }));
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (isLatestResourceOp(fileId, seq)) {
|
|
859
|
+
dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));
|
|
860
|
+
const msg = extractErrorMessage(err);
|
|
861
|
+
dispatch(setFileError({ id: fileId, error: msg }));
|
|
862
|
+
}
|
|
863
|
+
throw err;
|
|
864
|
+
} finally {
|
|
865
|
+
dispatch(removeFilePendingRequest({ id: fileId, requestId }));
|
|
866
|
+
releaseRequest(requestId);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
const moveFile = createAsyncThunk(
|
|
871
|
+
"cloudFiles/move",
|
|
872
|
+
async ({ fileId, newParentFolderId }, { dispatch, getState }) => {
|
|
873
|
+
const record = getFileFromState(getState(), fileId);
|
|
874
|
+
if (!record)
|
|
875
|
+
throw new Error(
|
|
876
|
+
"We couldn't find that file in this view. Refresh and try again."
|
|
877
|
+
);
|
|
878
|
+
const oldParentId = record.parentFolderId;
|
|
879
|
+
const requestId = newRequestId();
|
|
880
|
+
const seq = beginResourceOp(fileId);
|
|
881
|
+
const snapshot = {
|
|
882
|
+
parentFolderId: oldParentId
|
|
883
|
+
};
|
|
884
|
+
dispatch(
|
|
885
|
+
setFileField({
|
|
886
|
+
id: fileId,
|
|
887
|
+
field: "parentFolderId",
|
|
888
|
+
value: newParentFolderId
|
|
889
|
+
})
|
|
890
|
+
);
|
|
891
|
+
dispatch(
|
|
892
|
+
detachChildFromFolder({
|
|
893
|
+
parentFolderId: oldParentId,
|
|
894
|
+
kind: "file",
|
|
895
|
+
id: fileId
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
dispatch(
|
|
899
|
+
attachChildToFolder({
|
|
900
|
+
parentFolderId: newParentFolderId,
|
|
901
|
+
kind: "file",
|
|
902
|
+
id: fileId
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
dispatch(addFilePendingRequest({ id: fileId, requestId }));
|
|
906
|
+
registerRequest({
|
|
907
|
+
requestId,
|
|
908
|
+
kind: "move",
|
|
909
|
+
resourceId: fileId,
|
|
910
|
+
resourceType: "file"
|
|
911
|
+
});
|
|
912
|
+
try {
|
|
913
|
+
const targetFolder = newParentFolderId === null ? null : getState().cloudFiles.foldersById[newParentFolderId] ?? null;
|
|
914
|
+
const targetPrefix = targetFolder ? `${targetFolder.folderPath}/` : "";
|
|
915
|
+
const newPath = `${targetPrefix}${record.fileName}`;
|
|
916
|
+
const { data } = await Files.renameFile(
|
|
917
|
+
fileId,
|
|
918
|
+
{ new_path: newPath },
|
|
919
|
+
{ requestId }
|
|
920
|
+
);
|
|
921
|
+
if (isLatestResourceOp(fileId, seq)) {
|
|
922
|
+
dispatch(upsertFile(apiFileRecordToCloudFile(data)));
|
|
923
|
+
dispatch(markFileSaved({ id: fileId }));
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
if (isLatestResourceOp(fileId, seq)) {
|
|
927
|
+
dispatch(
|
|
928
|
+
detachChildFromFolder({
|
|
929
|
+
parentFolderId: newParentFolderId,
|
|
930
|
+
kind: "file",
|
|
931
|
+
id: fileId
|
|
932
|
+
})
|
|
933
|
+
);
|
|
934
|
+
dispatch(
|
|
935
|
+
attachChildToFolder({
|
|
936
|
+
parentFolderId: oldParentId,
|
|
937
|
+
kind: "file",
|
|
938
|
+
id: fileId
|
|
939
|
+
})
|
|
940
|
+
);
|
|
941
|
+
dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));
|
|
942
|
+
const msg = extractErrorMessage(err);
|
|
943
|
+
dispatch(setFileError({ id: fileId, error: msg }));
|
|
944
|
+
}
|
|
945
|
+
throw err;
|
|
946
|
+
} finally {
|
|
947
|
+
dispatch(removeFilePendingRequest({ id: fileId, requestId }));
|
|
948
|
+
releaseRequest(requestId);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
const updateFileMetadata = createAsyncThunk(
|
|
953
|
+
"cloudFiles/updateMetadata",
|
|
954
|
+
async ({ fileId, patch }, { dispatch, getState }) => {
|
|
955
|
+
const record = getFileFromState(getState(), fileId);
|
|
956
|
+
if (!record)
|
|
957
|
+
throw new Error(
|
|
958
|
+
"We couldn't find that file in this view. Refresh and try again."
|
|
959
|
+
);
|
|
960
|
+
const requestId = newRequestId();
|
|
961
|
+
const seq = beginResourceOp(fileId);
|
|
962
|
+
const snapshot = {
|
|
963
|
+
visibility: record.visibility,
|
|
964
|
+
metadata: record.metadata
|
|
965
|
+
};
|
|
966
|
+
if (patch.visibility !== void 0) {
|
|
967
|
+
dispatch(
|
|
968
|
+
setFileField({
|
|
969
|
+
id: fileId,
|
|
970
|
+
field: "visibility",
|
|
971
|
+
value: patch.visibility
|
|
972
|
+
})
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (patch.metadata !== void 0) {
|
|
976
|
+
dispatch(
|
|
977
|
+
setFileField({
|
|
978
|
+
id: fileId,
|
|
979
|
+
field: "metadata",
|
|
980
|
+
value: patch.metadata
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
dispatch(addFilePendingRequest({ id: fileId, requestId }));
|
|
985
|
+
registerRequest({
|
|
986
|
+
requestId,
|
|
987
|
+
kind: "update",
|
|
988
|
+
resourceId: fileId,
|
|
989
|
+
resourceType: "file"
|
|
990
|
+
});
|
|
991
|
+
try {
|
|
992
|
+
const { data } = await Files.patchFile(
|
|
993
|
+
fileId,
|
|
994
|
+
{
|
|
995
|
+
visibility: patch.visibility,
|
|
996
|
+
metadata: patch.metadata
|
|
997
|
+
},
|
|
998
|
+
{ requestId }
|
|
999
|
+
);
|
|
1000
|
+
if (isLatestResourceOp(fileId, seq)) {
|
|
1001
|
+
dispatch(upsertFile(apiFileRecordToCloudFile(data)));
|
|
1002
|
+
dispatch(markFileSaved({ id: fileId }));
|
|
1003
|
+
}
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
if (isLatestResourceOp(fileId, seq)) {
|
|
1006
|
+
dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));
|
|
1007
|
+
const msg = extractErrorMessage(err);
|
|
1008
|
+
dispatch(setFileError({ id: fileId, error: msg }));
|
|
1009
|
+
}
|
|
1010
|
+
throw err;
|
|
1011
|
+
} finally {
|
|
1012
|
+
dispatch(removeFilePendingRequest({ id: fileId, requestId }));
|
|
1013
|
+
releaseRequest(requestId);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
const deleteFile = createAsyncThunk(
|
|
1018
|
+
"cloudFiles/delete",
|
|
1019
|
+
async ({ fileId }, { dispatch, getState }) => {
|
|
1020
|
+
const record = getFileFromState(getState(), fileId);
|
|
1021
|
+
if (!record) return;
|
|
1022
|
+
const parentFolderId = record.parentFolderId;
|
|
1023
|
+
const requestId = newRequestId();
|
|
1024
|
+
dispatch(removeFile({ id: fileId }));
|
|
1025
|
+
dispatch(
|
|
1026
|
+
detachChildFromFolder({
|
|
1027
|
+
parentFolderId,
|
|
1028
|
+
kind: "file",
|
|
1029
|
+
id: fileId
|
|
1030
|
+
})
|
|
1031
|
+
);
|
|
1032
|
+
invalidateBlobCache(fileId);
|
|
1033
|
+
invalidateOfficeExtraction(fileId);
|
|
1034
|
+
registerRequest({
|
|
1035
|
+
requestId,
|
|
1036
|
+
kind: "delete",
|
|
1037
|
+
resourceId: fileId,
|
|
1038
|
+
resourceType: "file"
|
|
1039
|
+
});
|
|
1040
|
+
try {
|
|
1041
|
+
await softDeleteFileDirect(fileId);
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
dispatch(upsertFile(toCloudFilePartial(record)));
|
|
1044
|
+
dispatch(
|
|
1045
|
+
attachChildToFolder({
|
|
1046
|
+
parentFolderId,
|
|
1047
|
+
kind: "file",
|
|
1048
|
+
id: fileId
|
|
1049
|
+
})
|
|
1050
|
+
);
|
|
1051
|
+
throw err;
|
|
1052
|
+
} finally {
|
|
1053
|
+
releaseRequest(requestId);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
function toCloudFilePartial(record) {
|
|
1058
|
+
const {
|
|
1059
|
+
_dirty,
|
|
1060
|
+
_dirtyFields,
|
|
1061
|
+
_fieldHistory,
|
|
1062
|
+
_loadedFields,
|
|
1063
|
+
_loading,
|
|
1064
|
+
_error,
|
|
1065
|
+
_pendingRequestIds,
|
|
1066
|
+
...domain
|
|
1067
|
+
} = record;
|
|
1068
|
+
return domain;
|
|
1069
|
+
}
|
|
1070
|
+
function toCloudFolderPartial(record) {
|
|
1071
|
+
const {
|
|
1072
|
+
_dirty,
|
|
1073
|
+
_dirtyFields,
|
|
1074
|
+
_fieldHistory,
|
|
1075
|
+
_loadedFields,
|
|
1076
|
+
_loading,
|
|
1077
|
+
_error,
|
|
1078
|
+
_pendingRequestIds,
|
|
1079
|
+
...domain
|
|
1080
|
+
} = record;
|
|
1081
|
+
return domain;
|
|
1082
|
+
}
|
|
1083
|
+
const loadTrash = createAsyncThunk(
|
|
1084
|
+
"cloudFiles/loadTrash",
|
|
1085
|
+
async ({ userId }, { dispatch }) => {
|
|
1086
|
+
const rows = [];
|
|
1087
|
+
for (let page = 0; page < TREE_MAX_PAGES; page += 1) {
|
|
1088
|
+
const { data, error } = await supabase.rpc("get_user_file_tree", {
|
|
1089
|
+
p_user_id: userId,
|
|
1090
|
+
p_limit: TREE_PAGE_SIZE,
|
|
1091
|
+
p_offset: page * TREE_PAGE_SIZE,
|
|
1092
|
+
p_include_folders: true,
|
|
1093
|
+
p_include_deleted: true
|
|
1094
|
+
});
|
|
1095
|
+
if (error) throw error;
|
|
1096
|
+
const pageRows = parseCloudTreeRows(data);
|
|
1097
|
+
rows.push(...pageRows);
|
|
1098
|
+
if (pageRows.length < TREE_PAGE_SIZE) break;
|
|
1099
|
+
}
|
|
1100
|
+
const files = [];
|
|
1101
|
+
const folders = [];
|
|
1102
|
+
for (const row of rows) {
|
|
1103
|
+
if (!row.deleted_at) continue;
|
|
1104
|
+
if (row.kind === "file") {
|
|
1105
|
+
files.push({
|
|
1106
|
+
id: row.id,
|
|
1107
|
+
ownerId: row.owner_id,
|
|
1108
|
+
filePath: row.file_path,
|
|
1109
|
+
fileName: row.file_name,
|
|
1110
|
+
parentFolderId: row.parent_folder_id,
|
|
1111
|
+
mimeType: row.mime_type,
|
|
1112
|
+
fileSize: row.size_bytes,
|
|
1113
|
+
visibility: row.visibility,
|
|
1114
|
+
currentVersion: row.current_version,
|
|
1115
|
+
createdAt: row.created_at,
|
|
1116
|
+
updatedAt: row.updated_at,
|
|
1117
|
+
deletedAt: row.deleted_at,
|
|
1118
|
+
originDeviceId: row.origin_device_id
|
|
1119
|
+
});
|
|
1120
|
+
} else {
|
|
1121
|
+
folders.push({
|
|
1122
|
+
id: row.id,
|
|
1123
|
+
ownerId: row.owner_id,
|
|
1124
|
+
folderPath: row.folder_path,
|
|
1125
|
+
folderName: row.folder_name,
|
|
1126
|
+
parentId: row.parent_id,
|
|
1127
|
+
visibility: row.visibility,
|
|
1128
|
+
createdAt: row.created_at,
|
|
1129
|
+
updatedAt: row.updated_at,
|
|
1130
|
+
deletedAt: row.deleted_at
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
dispatch(upsertFiles(files));
|
|
1135
|
+
dispatch(upsertFolders(folders));
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
const restoreFile = createAsyncThunk(
|
|
1139
|
+
"cloudFiles/restoreFile",
|
|
1140
|
+
async ({ fileId }, { dispatch, getState }) => {
|
|
1141
|
+
const record = getFileFromState(getState(), fileId);
|
|
1142
|
+
if (!record?.deletedAt) return;
|
|
1143
|
+
const requestId = newRequestId();
|
|
1144
|
+
dispatch(upsertFile({ ...toCloudFilePartial(record), deletedAt: null }));
|
|
1145
|
+
dispatch(
|
|
1146
|
+
attachChildToFolder({
|
|
1147
|
+
parentFolderId: record.parentFolderId,
|
|
1148
|
+
kind: "file",
|
|
1149
|
+
id: fileId
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
registerRequest({
|
|
1153
|
+
requestId,
|
|
1154
|
+
kind: "file-restore",
|
|
1155
|
+
resourceId: fileId,
|
|
1156
|
+
resourceType: "file"
|
|
1157
|
+
});
|
|
1158
|
+
try {
|
|
1159
|
+
await restoreFileDirect(fileId);
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
dispatch(upsertFile(toCloudFilePartial(record)));
|
|
1162
|
+
dispatch(
|
|
1163
|
+
detachChildFromFolder({
|
|
1164
|
+
parentFolderId: record.parentFolderId,
|
|
1165
|
+
kind: "file",
|
|
1166
|
+
id: fileId
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
throw err;
|
|
1170
|
+
} finally {
|
|
1171
|
+
releaseRequest(requestId);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
);
|
|
1175
|
+
const restoreFolder = createAsyncThunk("cloudFiles/restoreFolder", async ({ folderId }, { dispatch, getState }) => {
|
|
1176
|
+
const folder = getState().cloudFiles.foldersById[folderId];
|
|
1177
|
+
if (!folder?.deletedAt) return;
|
|
1178
|
+
await restoreFolderDirect(folderId);
|
|
1179
|
+
dispatch(upsertFolder({ ...toCloudFolderPartial(folder), deletedAt: null }));
|
|
1180
|
+
const userId = folder.ownerId;
|
|
1181
|
+
await dispatch(loadUserFileTree({ userId }));
|
|
1182
|
+
await dispatch(loadTrash({ userId }));
|
|
1183
|
+
});
|
|
1184
|
+
const restoreVersion = createAsyncThunk(
|
|
1185
|
+
"cloudFiles/restoreVersion",
|
|
1186
|
+
async ({ fileId, versionNumber }, { dispatch }) => {
|
|
1187
|
+
if (isVirtualResourceId(fileId)) return;
|
|
1188
|
+
dispatch(setFileLoading({ id: fileId, loading: true }));
|
|
1189
|
+
const requestId = newRequestId();
|
|
1190
|
+
registerRequest({
|
|
1191
|
+
requestId,
|
|
1192
|
+
kind: "restore-version",
|
|
1193
|
+
resourceId: fileId,
|
|
1194
|
+
resourceType: "file"
|
|
1195
|
+
});
|
|
1196
|
+
try {
|
|
1197
|
+
const { data } = await Versions.restoreVersion(fileId, versionNumber, {
|
|
1198
|
+
requestId
|
|
1199
|
+
});
|
|
1200
|
+
dispatch(upsertFile(apiFileRecordToCloudFile(data)));
|
|
1201
|
+
invalidateBlobCache(fileId);
|
|
1202
|
+
invalidateOfficeExtraction(fileId);
|
|
1203
|
+
await dispatch(loadFileVersions({ fileId })).unwrap();
|
|
1204
|
+
} finally {
|
|
1205
|
+
dispatch(setFileLoading({ id: fileId, loading: false }));
|
|
1206
|
+
releaseRequest(requestId);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
const saveFileNewVersion = createAsyncThunk(
|
|
1211
|
+
"cloudFiles/saveFileNewVersion",
|
|
1212
|
+
async ({ fileId, content, changeSummary }, { dispatch, getState }) => {
|
|
1213
|
+
if (isVirtualResourceId(fileId)) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
"saveFileNewVersion is for stored files; virtual files save through writeAny."
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
let record = getState().cloudFiles.filesById[fileId];
|
|
1219
|
+
if (!record?.fileName) {
|
|
1220
|
+
const { data } = await Files.getFileMetadata(fileId);
|
|
1221
|
+
const fresh = apiFileRecordToCloudFile(data);
|
|
1222
|
+
dispatch(upsertFile(fresh));
|
|
1223
|
+
record = fresh;
|
|
1224
|
+
}
|
|
1225
|
+
const mimeType = record.mimeType ?? "text/plain";
|
|
1226
|
+
const body = typeof content === "string" ? new Blob([content], { type: mimeType }) : content;
|
|
1227
|
+
const upload = new File([body], record.fileName, { type: mimeType });
|
|
1228
|
+
dispatch(setFileLoading({ id: fileId, loading: true }));
|
|
1229
|
+
const requestId = newRequestId();
|
|
1230
|
+
registerRequest({
|
|
1231
|
+
requestId,
|
|
1232
|
+
kind: "upload",
|
|
1233
|
+
resourceId: fileId,
|
|
1234
|
+
resourceType: "file"
|
|
1235
|
+
});
|
|
1236
|
+
try {
|
|
1237
|
+
const reqOpts = { requestId, idempotencyKey: requestId };
|
|
1238
|
+
const summary = changeSummary ?? "Edited in place";
|
|
1239
|
+
const { data } = await Files.uploadNewVersion(
|
|
1240
|
+
fileId,
|
|
1241
|
+
{ file: upload, changeSummary: summary },
|
|
1242
|
+
reqOpts
|
|
1243
|
+
);
|
|
1244
|
+
if (data.file_id !== fileId || data.is_new) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
`Saving "${record.fileName}" did not update it \u2014 the files service wrote a separate file (${data.file_path}, id ${data.file_id}) instead of version ${record.currentVersion + 1}. Your original is unchanged; remove the extra file from the folder.`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
dispatch(
|
|
1250
|
+
upsertFile({
|
|
1251
|
+
id: fileId,
|
|
1252
|
+
currentVersion: data.version_number,
|
|
1253
|
+
fileSize: data.size_bytes,
|
|
1254
|
+
checksum: data.checksum,
|
|
1255
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1256
|
+
})
|
|
1257
|
+
);
|
|
1258
|
+
replaceWithSavedBytes(fileId, body, data.version_number);
|
|
1259
|
+
invalidateOfficeExtraction(fileId);
|
|
1260
|
+
void dispatch(loadFileVersions({ fileId })).unwrap().catch((err) => {
|
|
1261
|
+
console.error(
|
|
1262
|
+
"[saveFileNewVersion] saved, but the version list did not refresh:",
|
|
1263
|
+
err
|
|
1264
|
+
);
|
|
1265
|
+
});
|
|
1266
|
+
return { fileId, versionNumber: data.version_number };
|
|
1267
|
+
} finally {
|
|
1268
|
+
dispatch(setFileLoading({ id: fileId, loading: false }));
|
|
1269
|
+
releaseRequest(requestId);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
function requireByGranteeType(granteeType) {
|
|
1274
|
+
if (granteeType === "public") {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
"Public access is toggled via the resource's visibility, not the by-grantee grant/revoke path."
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return granteeType ?? "user";
|
|
1280
|
+
}
|
|
1281
|
+
const grantPermission = createAsyncThunk("cloudFiles/grantPermission", async (arg, { dispatch }) => {
|
|
1282
|
+
if (isVirtualResourceId(arg.resourceId)) {
|
|
1283
|
+
throw new Error("Sharing isn't available for this source yet.");
|
|
1284
|
+
}
|
|
1285
|
+
const requestId = newRequestId();
|
|
1286
|
+
registerRequest({
|
|
1287
|
+
requestId,
|
|
1288
|
+
kind: "grant-permission",
|
|
1289
|
+
resourceId: arg.resourceId,
|
|
1290
|
+
resourceType: arg.resourceType
|
|
1291
|
+
});
|
|
1292
|
+
try {
|
|
1293
|
+
const body = {
|
|
1294
|
+
grantee_id: arg.granteeId,
|
|
1295
|
+
level: arg.level,
|
|
1296
|
+
grantee_type: requireByGranteeType(arg.granteeType),
|
|
1297
|
+
expires_at: arg.expiresAt ?? null
|
|
1298
|
+
};
|
|
1299
|
+
const { data } = arg.resourceType === "folder" ? await Permissions.grantFolderPermission(arg.resourceId, body, {
|
|
1300
|
+
requestId
|
|
1301
|
+
}) : await Permissions.grantFilePermission(arg.resourceId, body, {
|
|
1302
|
+
requestId
|
|
1303
|
+
});
|
|
1304
|
+
dispatch(
|
|
1305
|
+
upsertPermissionsForResource({
|
|
1306
|
+
resourceId: arg.resourceId,
|
|
1307
|
+
permissions: [rpcPermissionRowToCloudFilePermission(data)]
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
void dispatch(loadPermissions({ resourceId: arg.resourceId }));
|
|
1311
|
+
} finally {
|
|
1312
|
+
releaseRequest(requestId);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
const revokePermission = createAsyncThunk("cloudFiles/revokePermission", async (arg, { dispatch }) => {
|
|
1316
|
+
if (isVirtualResourceId(arg.resourceId)) {
|
|
1317
|
+
throw new Error("Sharing isn't available for this source yet.");
|
|
1318
|
+
}
|
|
1319
|
+
const requestId = newRequestId();
|
|
1320
|
+
registerRequest({
|
|
1321
|
+
requestId,
|
|
1322
|
+
kind: "revoke-permission",
|
|
1323
|
+
resourceId: arg.resourceId,
|
|
1324
|
+
resourceType: arg.resourceType
|
|
1325
|
+
});
|
|
1326
|
+
try {
|
|
1327
|
+
const granteeType = requireByGranteeType(arg.granteeType);
|
|
1328
|
+
if (arg.resourceType === "folder") {
|
|
1329
|
+
await Permissions.revokeFolderPermission(
|
|
1330
|
+
arg.resourceId,
|
|
1331
|
+
arg.granteeId,
|
|
1332
|
+
{ granteeType },
|
|
1333
|
+
{ requestId }
|
|
1334
|
+
);
|
|
1335
|
+
} else {
|
|
1336
|
+
await Permissions.revokeFilePermission(
|
|
1337
|
+
arg.resourceId,
|
|
1338
|
+
arg.granteeId,
|
|
1339
|
+
{ granteeType },
|
|
1340
|
+
{ requestId }
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
dispatch(
|
|
1344
|
+
removePermissionForResource({
|
|
1345
|
+
resourceId: arg.resourceId,
|
|
1346
|
+
granteeId: arg.granteeId,
|
|
1347
|
+
granteeType
|
|
1348
|
+
})
|
|
1349
|
+
);
|
|
1350
|
+
} finally {
|
|
1351
|
+
releaseRequest(requestId);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
const createShareLink = createAsyncThunk("cloudFiles/createShareLink", async (arg, { dispatch, getState }) => {
|
|
1355
|
+
if (isVirtualResourceId(arg.resourceId)) {
|
|
1356
|
+
throw new Error("Share links aren't available for this source yet.");
|
|
1357
|
+
}
|
|
1358
|
+
if (hasInFlight(arg.resourceId, "create-share-link")) {
|
|
1359
|
+
throw new Error("A share link is already being created for this item.");
|
|
1360
|
+
}
|
|
1361
|
+
const requestId = newRequestId();
|
|
1362
|
+
registerRequest({
|
|
1363
|
+
requestId,
|
|
1364
|
+
kind: "create-share-link",
|
|
1365
|
+
resourceId: arg.resourceId,
|
|
1366
|
+
resourceType: arg.resourceType
|
|
1367
|
+
});
|
|
1368
|
+
try {
|
|
1369
|
+
const created = await createCanonicalShareLink({
|
|
1370
|
+
resourceType: arg.resourceType,
|
|
1371
|
+
resourceId: arg.resourceId,
|
|
1372
|
+
permissionLevel: arg.permissionLevel,
|
|
1373
|
+
expiresAt: arg.expiresAt ?? null,
|
|
1374
|
+
maxUses: arg.maxUses ?? null
|
|
1375
|
+
});
|
|
1376
|
+
if (!created.success || !created.token) {
|
|
1377
|
+
throw new Error(created.error ?? "Failed to create share link");
|
|
1378
|
+
}
|
|
1379
|
+
await dispatch(
|
|
1380
|
+
loadShareLinks({
|
|
1381
|
+
resourceId: arg.resourceId,
|
|
1382
|
+
resourceType: arg.resourceType
|
|
1383
|
+
})
|
|
1384
|
+
).unwrap();
|
|
1385
|
+
const link = (getState().cloudFiles.shareLinksByResourceId[arg.resourceId] ?? []).find((l) => l.shareToken === created.token);
|
|
1386
|
+
if (!link) {
|
|
1387
|
+
throw new Error("Share link was created but could not be loaded back");
|
|
1388
|
+
}
|
|
1389
|
+
return link;
|
|
1390
|
+
} finally {
|
|
1391
|
+
releaseRequest(requestId);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
const revokeShareLink = createAsyncThunk("cloudFiles/revokeShareLink", async ({ linkId }, { dispatch }) => {
|
|
1395
|
+
const requestId = newRequestId();
|
|
1396
|
+
registerRequest({
|
|
1397
|
+
requestId,
|
|
1398
|
+
kind: "revoke-share-link",
|
|
1399
|
+
resourceId: null,
|
|
1400
|
+
resourceType: null
|
|
1401
|
+
});
|
|
1402
|
+
try {
|
|
1403
|
+
const result = await revokeCanonicalShareLink(linkId);
|
|
1404
|
+
if (!result.success) {
|
|
1405
|
+
throw new Error(result.error ?? "Failed to revoke share link");
|
|
1406
|
+
}
|
|
1407
|
+
dispatch(removeShareLink({ linkId }));
|
|
1408
|
+
} finally {
|
|
1409
|
+
releaseRequest(requestId);
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
const getFileUrl = createAsyncThunk("cloudFiles/getFileUrl", async ({ fileId }) => {
|
|
1413
|
+
if (isVirtualResourceId(fileId)) {
|
|
1414
|
+
throw new Error("Byte URLs aren't available for virtual sources");
|
|
1415
|
+
}
|
|
1416
|
+
const url = await fileHandler.use({ kind: "file_id", fileId }).as({ kind: "html_src" });
|
|
1417
|
+
if (!url) {
|
|
1418
|
+
throw new Error(`Could not resolve a URL for file ${fileId}`);
|
|
1419
|
+
}
|
|
1420
|
+
return { url };
|
|
1421
|
+
});
|
|
1422
|
+
function toastBulkPartialFailure(result, verb) {
|
|
1423
|
+
if (result.failed > 0) {
|
|
1424
|
+
const total = result.succeeded + result.failed;
|
|
1425
|
+
toast.error(`Couldn't ${verb} ${result.failed} of ${total} items`, {
|
|
1426
|
+
description: result.succeeded > 0 ? `${result.succeeded} succeeded. The rest were restored.` : "All items were restored."
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const bulkDeleteFiles = createAsyncThunk("cloudFiles/bulkDeleteFiles", async (arg, { dispatch, getState }) => {
|
|
1431
|
+
if (arg.fileIds.length === 0) {
|
|
1432
|
+
return { results: [], succeeded: 0, failed: 0 };
|
|
1433
|
+
}
|
|
1434
|
+
const state = getState();
|
|
1435
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const id of arg.fileIds) {
|
|
1437
|
+
const file = state.cloudFiles.filesById[id];
|
|
1438
|
+
if (file) snapshots.set(id, file);
|
|
1439
|
+
}
|
|
1440
|
+
const requestId = newRequestId();
|
|
1441
|
+
registerRequest({
|
|
1442
|
+
requestId,
|
|
1443
|
+
kind: "bulk-delete-files",
|
|
1444
|
+
resourceId: null,
|
|
1445
|
+
resourceType: "file"
|
|
1446
|
+
});
|
|
1447
|
+
for (const id of arg.fileIds) {
|
|
1448
|
+
dispatch(removeFile({ id }));
|
|
1449
|
+
invalidateBlobCache(id);
|
|
1450
|
+
invalidateOfficeExtraction(id);
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
const { data } = await Files.bulkDeleteFiles(
|
|
1454
|
+
{ file_ids: arg.fileIds },
|
|
1455
|
+
{ requestId }
|
|
1456
|
+
);
|
|
1457
|
+
const result = data ?? {
|
|
1458
|
+
results: arg.fileIds.map((id) => ({ id, ok: true, error: null })),
|
|
1459
|
+
succeeded: arg.fileIds.length,
|
|
1460
|
+
failed: 0
|
|
1461
|
+
};
|
|
1462
|
+
for (const r of result.results) {
|
|
1463
|
+
if (r.ok) continue;
|
|
1464
|
+
const snap = snapshots.get(r.id);
|
|
1465
|
+
if (snap) dispatch(upsertFile(snap));
|
|
1466
|
+
}
|
|
1467
|
+
toastBulkPartialFailure(result, "delete");
|
|
1468
|
+
return result;
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
for (const snap of snapshots.values()) dispatch(upsertFile(snap));
|
|
1471
|
+
throw err;
|
|
1472
|
+
} finally {
|
|
1473
|
+
releaseRequest(requestId);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
const bulkMoveFiles = createAsyncThunk("cloudFiles/bulkMoveFiles", async (arg, { dispatch, getState }) => {
|
|
1477
|
+
if (arg.fileIds.length === 0) {
|
|
1478
|
+
return { results: [], succeeded: 0, failed: 0 };
|
|
1479
|
+
}
|
|
1480
|
+
const state = getState();
|
|
1481
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1482
|
+
for (const id of arg.fileIds) {
|
|
1483
|
+
const file = state.cloudFiles.filesById[id];
|
|
1484
|
+
if (file) snapshots.set(id, file);
|
|
1485
|
+
}
|
|
1486
|
+
const requestId = newRequestId();
|
|
1487
|
+
registerRequest({
|
|
1488
|
+
requestId,
|
|
1489
|
+
kind: "bulk-move-files",
|
|
1490
|
+
resourceId: null,
|
|
1491
|
+
resourceType: "file"
|
|
1492
|
+
});
|
|
1493
|
+
for (const [id, file] of snapshots) {
|
|
1494
|
+
if (file.parentFolderId === arg.newParentFolderId) continue;
|
|
1495
|
+
dispatch(
|
|
1496
|
+
detachChildFromFolder({
|
|
1497
|
+
parentFolderId: file.parentFolderId,
|
|
1498
|
+
kind: "file",
|
|
1499
|
+
id
|
|
1500
|
+
})
|
|
1501
|
+
);
|
|
1502
|
+
dispatch(
|
|
1503
|
+
attachChildToFolder({
|
|
1504
|
+
parentFolderId: arg.newParentFolderId,
|
|
1505
|
+
kind: "file",
|
|
1506
|
+
id
|
|
1507
|
+
})
|
|
1508
|
+
);
|
|
1509
|
+
dispatch(upsertFile({ ...file, parentFolderId: arg.newParentFolderId }));
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
const { data } = await Files.bulkMoveFiles(
|
|
1513
|
+
{
|
|
1514
|
+
file_ids: arg.fileIds,
|
|
1515
|
+
new_parent_folder_id: arg.newParentFolderId
|
|
1516
|
+
},
|
|
1517
|
+
{ requestId }
|
|
1518
|
+
);
|
|
1519
|
+
for (const r of data.results) {
|
|
1520
|
+
if (r.ok) continue;
|
|
1521
|
+
const snap = snapshots.get(r.id);
|
|
1522
|
+
if (!snap || snap.parentFolderId === arg.newParentFolderId) continue;
|
|
1523
|
+
dispatch(
|
|
1524
|
+
detachChildFromFolder({
|
|
1525
|
+
parentFolderId: arg.newParentFolderId,
|
|
1526
|
+
kind: "file",
|
|
1527
|
+
id: snap.id
|
|
1528
|
+
})
|
|
1529
|
+
);
|
|
1530
|
+
dispatch(
|
|
1531
|
+
attachChildToFolder({
|
|
1532
|
+
parentFolderId: snap.parentFolderId,
|
|
1533
|
+
kind: "file",
|
|
1534
|
+
id: snap.id
|
|
1535
|
+
})
|
|
1536
|
+
);
|
|
1537
|
+
dispatch(upsertFile(snap));
|
|
1538
|
+
}
|
|
1539
|
+
toastBulkPartialFailure(data, "move");
|
|
1540
|
+
return data;
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
for (const snap of snapshots.values()) {
|
|
1543
|
+
if (snap.parentFolderId === arg.newParentFolderId) continue;
|
|
1544
|
+
dispatch(
|
|
1545
|
+
detachChildFromFolder({
|
|
1546
|
+
parentFolderId: arg.newParentFolderId,
|
|
1547
|
+
kind: "file",
|
|
1548
|
+
id: snap.id
|
|
1549
|
+
})
|
|
1550
|
+
);
|
|
1551
|
+
dispatch(
|
|
1552
|
+
attachChildToFolder({
|
|
1553
|
+
parentFolderId: snap.parentFolderId,
|
|
1554
|
+
kind: "file",
|
|
1555
|
+
id: snap.id
|
|
1556
|
+
})
|
|
1557
|
+
);
|
|
1558
|
+
dispatch(upsertFile(snap));
|
|
1559
|
+
}
|
|
1560
|
+
throw err;
|
|
1561
|
+
} finally {
|
|
1562
|
+
releaseRequest(requestId);
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
const bulkMoveFolders = createAsyncThunk("cloudFiles/bulkMoveFolders", async (arg, { dispatch, getState }) => {
|
|
1566
|
+
if (arg.folderIds.length === 0) {
|
|
1567
|
+
return { results: [], succeeded: 0, failed: 0 };
|
|
1568
|
+
}
|
|
1569
|
+
const state = getState();
|
|
1570
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1571
|
+
for (const id of arg.folderIds) {
|
|
1572
|
+
const folder = state.cloudFiles.foldersById[id];
|
|
1573
|
+
if (folder) snapshots.set(id, folder);
|
|
1574
|
+
}
|
|
1575
|
+
const requestId = newRequestId();
|
|
1576
|
+
registerRequest({
|
|
1577
|
+
requestId,
|
|
1578
|
+
kind: "bulk-move-folders",
|
|
1579
|
+
resourceId: null,
|
|
1580
|
+
resourceType: "folder"
|
|
1581
|
+
});
|
|
1582
|
+
for (const [id, folder] of snapshots) {
|
|
1583
|
+
if (folder.parentId === arg.newParentId) continue;
|
|
1584
|
+
dispatch(
|
|
1585
|
+
detachChildFromFolder({
|
|
1586
|
+
parentFolderId: folder.parentId,
|
|
1587
|
+
kind: "folder",
|
|
1588
|
+
id
|
|
1589
|
+
})
|
|
1590
|
+
);
|
|
1591
|
+
dispatch(
|
|
1592
|
+
attachChildToFolder({
|
|
1593
|
+
parentFolderId: arg.newParentId,
|
|
1594
|
+
kind: "folder",
|
|
1595
|
+
id
|
|
1596
|
+
})
|
|
1597
|
+
);
|
|
1598
|
+
dispatch(upsertFolder({ ...folder, parentId: arg.newParentId }));
|
|
1599
|
+
}
|
|
1600
|
+
try {
|
|
1601
|
+
const { data } = await Folders.bulkMoveFolders(
|
|
1602
|
+
{ folder_ids: arg.folderIds, new_parent_id: arg.newParentId },
|
|
1603
|
+
{ requestId }
|
|
1604
|
+
);
|
|
1605
|
+
for (const r of data.results) {
|
|
1606
|
+
if (r.ok) continue;
|
|
1607
|
+
const snap = snapshots.get(r.id);
|
|
1608
|
+
if (!snap || snap.parentId === arg.newParentId) continue;
|
|
1609
|
+
dispatch(
|
|
1610
|
+
detachChildFromFolder({
|
|
1611
|
+
parentFolderId: arg.newParentId,
|
|
1612
|
+
kind: "folder",
|
|
1613
|
+
id: snap.id
|
|
1614
|
+
})
|
|
1615
|
+
);
|
|
1616
|
+
dispatch(
|
|
1617
|
+
attachChildToFolder({
|
|
1618
|
+
parentFolderId: snap.parentId,
|
|
1619
|
+
kind: "folder",
|
|
1620
|
+
id: snap.id
|
|
1621
|
+
})
|
|
1622
|
+
);
|
|
1623
|
+
dispatch(upsertFolder(snap));
|
|
1624
|
+
}
|
|
1625
|
+
toastBulkPartialFailure(data, "move");
|
|
1626
|
+
return data;
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
for (const snap of snapshots.values()) {
|
|
1629
|
+
if (snap.parentId === arg.newParentId) continue;
|
|
1630
|
+
dispatch(
|
|
1631
|
+
detachChildFromFolder({
|
|
1632
|
+
parentFolderId: arg.newParentId,
|
|
1633
|
+
kind: "folder",
|
|
1634
|
+
id: snap.id
|
|
1635
|
+
})
|
|
1636
|
+
);
|
|
1637
|
+
dispatch(
|
|
1638
|
+
attachChildToFolder({
|
|
1639
|
+
parentFolderId: snap.parentId,
|
|
1640
|
+
kind: "folder",
|
|
1641
|
+
id: snap.id
|
|
1642
|
+
})
|
|
1643
|
+
);
|
|
1644
|
+
dispatch(upsertFolder(snap));
|
|
1645
|
+
}
|
|
1646
|
+
throw err;
|
|
1647
|
+
} finally {
|
|
1648
|
+
releaseRequest(requestId);
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
export {
|
|
1652
|
+
announceUploadFailures,
|
|
1653
|
+
bulkDeleteFiles,
|
|
1654
|
+
bulkMoveFiles,
|
|
1655
|
+
bulkMoveFolders,
|
|
1656
|
+
clearUploadEntry,
|
|
1657
|
+
createFolder,
|
|
1658
|
+
createShareLink,
|
|
1659
|
+
deleteFile,
|
|
1660
|
+
deleteFolder,
|
|
1661
|
+
ensureCloudFileFields,
|
|
1662
|
+
ensureFolderPath,
|
|
1663
|
+
getFileUrl,
|
|
1664
|
+
grantPermission,
|
|
1665
|
+
loadFileVersions,
|
|
1666
|
+
loadFolderContents,
|
|
1667
|
+
loadPermissions,
|
|
1668
|
+
loadShareLinks,
|
|
1669
|
+
loadTrash,
|
|
1670
|
+
loadUserFileTree,
|
|
1671
|
+
moveFile,
|
|
1672
|
+
reconcileTree,
|
|
1673
|
+
renameFile,
|
|
1674
|
+
restoreFile,
|
|
1675
|
+
restoreFolder,
|
|
1676
|
+
restoreVersion,
|
|
1677
|
+
revokePermission,
|
|
1678
|
+
revokeShareLink,
|
|
1679
|
+
saveFileNewVersion,
|
|
1680
|
+
updateFileMetadata,
|
|
1681
|
+
updateFolder,
|
|
1682
|
+
uploadFiles
|
|
1683
|
+
};
|
|
1684
|
+
//# sourceMappingURL=thunks.js.map
|