@anuma/sdk 6.31.3 → 6.31.4
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/dist/react/index.cjs +173 -55
- package/dist/react/index.mjs +173 -55
- package/package.json +1 -1
package/dist/react/index.cjs
CHANGED
|
@@ -32167,6 +32167,22 @@ async function uploadFileToDropbox(accessToken, filename, content, folder = DEFA
|
|
|
32167
32167
|
}
|
|
32168
32168
|
return response.json();
|
|
32169
32169
|
}
|
|
32170
|
+
async function getDropboxFileMetadata(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
|
|
32171
|
+
const response = await fetch(`${DROPBOX_API_URL}/files/get_metadata`, {
|
|
32172
|
+
method: "POST",
|
|
32173
|
+
headers: {
|
|
32174
|
+
Authorization: `Bearer ${accessToken}`,
|
|
32175
|
+
"Content-Type": "application/json"
|
|
32176
|
+
},
|
|
32177
|
+
body: JSON.stringify({ path: `${folder}/${filename}` })
|
|
32178
|
+
});
|
|
32179
|
+
if (response.status === 409) return null;
|
|
32180
|
+
if (!response.ok) {
|
|
32181
|
+
const errorText = await response.text();
|
|
32182
|
+
throw new Error(`Dropbox get metadata failed: ${response.status} - ${errorText}`);
|
|
32183
|
+
}
|
|
32184
|
+
return response.json();
|
|
32185
|
+
}
|
|
32170
32186
|
async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
|
|
32171
32187
|
await ensureBackupFolder(accessToken, folder);
|
|
32172
32188
|
const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
|
|
@@ -32232,27 +32248,61 @@ async function downloadDropboxFile(accessToken, path) {
|
|
|
32232
32248
|
}
|
|
32233
32249
|
return response.blob();
|
|
32234
32250
|
}
|
|
32235
|
-
async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
|
|
32236
|
-
const files = await listDropboxFiles(accessToken, folder);
|
|
32237
|
-
return files.find((f) => f.name === filename) || null;
|
|
32238
|
-
}
|
|
32239
32251
|
|
|
32240
32252
|
// src/lib/backup/dropbox/backup.ts
|
|
32241
32253
|
var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
|
|
32242
|
-
|
|
32254
|
+
var MAX_LISTING_FAILURES = 3;
|
|
32255
|
+
function createDropboxFileIndex(backupFolder) {
|
|
32256
|
+
let pending;
|
|
32257
|
+
let failures = 0;
|
|
32258
|
+
return {
|
|
32259
|
+
get(token) {
|
|
32260
|
+
if (!pending) {
|
|
32261
|
+
if (failures >= MAX_LISTING_FAILURES) {
|
|
32262
|
+
return Promise.reject(
|
|
32263
|
+
new Error("The backup folder listing failed repeatedly; skipped for this run")
|
|
32264
|
+
);
|
|
32265
|
+
}
|
|
32266
|
+
pending = listDropboxFiles(token, backupFolder).then((files) => {
|
|
32267
|
+
const byName = /* @__PURE__ */ new Map();
|
|
32268
|
+
for (const file of files) {
|
|
32269
|
+
if (!byName.has(file.name)) byName.set(file.name, file);
|
|
32270
|
+
}
|
|
32271
|
+
return byName;
|
|
32272
|
+
}).catch((err) => {
|
|
32273
|
+
pending = void 0;
|
|
32274
|
+
failures++;
|
|
32275
|
+
throw err;
|
|
32276
|
+
});
|
|
32277
|
+
}
|
|
32278
|
+
return pending;
|
|
32279
|
+
}
|
|
32280
|
+
};
|
|
32281
|
+
}
|
|
32282
|
+
async function readLocalUpdatedAt(database, conversationId) {
|
|
32283
|
+
const { Q: Q19 } = await import("@nozbe/watermelondb");
|
|
32284
|
+
const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
|
|
32285
|
+
const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
|
|
32286
|
+
return match ? match.updatedAt : null;
|
|
32287
|
+
}
|
|
32288
|
+
async function pushConversationToDropbox(database, conversationId, userAddress, token, fileIndex, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
|
|
32243
32289
|
try {
|
|
32244
32290
|
await deps.requestEncryptionKey(userAddress);
|
|
32245
32291
|
const filename = `${conversationId}.json`;
|
|
32246
|
-
const
|
|
32292
|
+
const index = await fileIndex.get(token);
|
|
32293
|
+
const existingFile = index.get(filename);
|
|
32247
32294
|
if (existingFile) {
|
|
32248
|
-
const
|
|
32249
|
-
const
|
|
32250
|
-
|
|
32251
|
-
|
|
32252
|
-
|
|
32253
|
-
|
|
32254
|
-
|
|
32255
|
-
|
|
32295
|
+
const localUpdatedAt = await readLocalUpdatedAt(database, conversationId);
|
|
32296
|
+
const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
|
|
32297
|
+
if (localUpdated !== null && localUpdated <= new Date(existingFile.server_modified).getTime()) {
|
|
32298
|
+
return "skipped";
|
|
32299
|
+
}
|
|
32300
|
+
const current = await getDropboxFileMetadata(token, filename, backupFolder);
|
|
32301
|
+
if (!current) {
|
|
32302
|
+
index.delete(filename);
|
|
32303
|
+
} else {
|
|
32304
|
+
index.set(filename, current);
|
|
32305
|
+
if (localUpdated !== null && localUpdated <= new Date(current.server_modified).getTime()) {
|
|
32256
32306
|
return "skipped";
|
|
32257
32307
|
}
|
|
32258
32308
|
}
|
|
@@ -32261,7 +32311,8 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
|
|
|
32261
32311
|
if (!exportResult.success || !exportResult.blob) {
|
|
32262
32312
|
return "failed";
|
|
32263
32313
|
}
|
|
32264
|
-
await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
|
|
32314
|
+
const uploaded = await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
|
|
32315
|
+
index.set(filename, uploaded);
|
|
32265
32316
|
return "uploaded";
|
|
32266
32317
|
} catch (err) {
|
|
32267
32318
|
if (isAuthError(err) && !_retried) {
|
|
@@ -32272,6 +32323,7 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
|
|
|
32272
32323
|
conversationId,
|
|
32273
32324
|
userAddress,
|
|
32274
32325
|
newToken,
|
|
32326
|
+
fileIndex,
|
|
32275
32327
|
deps,
|
|
32276
32328
|
backupFolder,
|
|
32277
32329
|
true
|
|
@@ -32295,6 +32347,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
|
|
|
32295
32347
|
}
|
|
32296
32348
|
let uploaded = 0;
|
|
32297
32349
|
let skipped = 0;
|
|
32350
|
+
const fileIndex = createDropboxFileIndex(backupFolder);
|
|
32298
32351
|
for (let i = 0; i < conversations.length; i++) {
|
|
32299
32352
|
const conv = conversations[i];
|
|
32300
32353
|
onProgress?.(i + 1, total);
|
|
@@ -32303,6 +32356,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
|
|
|
32303
32356
|
conv.conversationId,
|
|
32304
32357
|
userAddress,
|
|
32305
32358
|
token,
|
|
32359
|
+
fileIndex,
|
|
32306
32360
|
deps,
|
|
32307
32361
|
backupFolder
|
|
32308
32362
|
);
|
|
@@ -32866,29 +32920,52 @@ async function listDriveFiles(accessToken, folderId) {
|
|
|
32866
32920
|
const data = await response.json();
|
|
32867
32921
|
return data.files ?? [];
|
|
32868
32922
|
}
|
|
32869
|
-
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
32876
|
-
|
|
32923
|
+
var DRIVE_MAX_PAGE_SIZE = 1e3;
|
|
32924
|
+
async function listAllDriveFiles(accessToken, folderId) {
|
|
32925
|
+
const query = `'${escapeQueryValue(folderId)}' in parents and trashed=false`;
|
|
32926
|
+
const fields = encodeURIComponent("nextPageToken,files(id,name,createdTime,modifiedTime,size)");
|
|
32927
|
+
const files = [];
|
|
32928
|
+
let pageToken;
|
|
32929
|
+
do {
|
|
32930
|
+
const tokenParam = pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "";
|
|
32931
|
+
const response = await fetch(
|
|
32932
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=${DRIVE_MAX_PAGE_SIZE}${tokenParam}`,
|
|
32933
|
+
{
|
|
32934
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
32935
|
+
}
|
|
32936
|
+
);
|
|
32937
|
+
if (!response.ok) {
|
|
32938
|
+
throw new Error(`Failed to list files: ${response.status}`);
|
|
32939
|
+
}
|
|
32940
|
+
const data = await response.json();
|
|
32941
|
+
files.push(...data.files ?? []);
|
|
32942
|
+
pageToken = data.nextPageToken;
|
|
32943
|
+
} while (pageToken);
|
|
32944
|
+
return files;
|
|
32877
32945
|
}
|
|
32878
|
-
async function
|
|
32879
|
-
const
|
|
32880
|
-
const fields = "files(id,name,createdTime,modifiedTime,size)";
|
|
32946
|
+
async function getDriveFileMetadata(accessToken, fileId) {
|
|
32947
|
+
const fields = encodeURIComponent("id,name,createdTime,modifiedTime,size,trashed");
|
|
32881
32948
|
const response = await fetch(
|
|
32882
|
-
`${DRIVE_API_URL}/files
|
|
32949
|
+
`${DRIVE_API_URL}/files/${encodeURIComponent(fileId)}?fields=${fields}`,
|
|
32883
32950
|
{
|
|
32884
32951
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
32885
32952
|
}
|
|
32886
32953
|
);
|
|
32954
|
+
if (response.status === 404) return null;
|
|
32887
32955
|
if (!response.ok) {
|
|
32888
|
-
throw new Error(`Failed to
|
|
32956
|
+
throw new Error(`Failed to read file: ${response.status}`);
|
|
32889
32957
|
}
|
|
32890
32958
|
const data = await response.json();
|
|
32891
|
-
return data.
|
|
32959
|
+
return data.trashed ? null : data;
|
|
32960
|
+
}
|
|
32961
|
+
async function downloadDriveFile(accessToken, fileId) {
|
|
32962
|
+
const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
|
|
32963
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
32964
|
+
});
|
|
32965
|
+
if (!response.ok) {
|
|
32966
|
+
throw new Error(`Failed to download file: ${response.status}`);
|
|
32967
|
+
}
|
|
32968
|
+
return response.blob();
|
|
32892
32969
|
}
|
|
32893
32970
|
|
|
32894
32971
|
// src/lib/backup/google/backup.ts
|
|
@@ -32910,28 +32987,59 @@ async function getConversationsFolder(token, requestDriveAccess, rootFolder, sub
|
|
|
32910
32987
|
throw err;
|
|
32911
32988
|
}
|
|
32912
32989
|
}
|
|
32913
|
-
|
|
32990
|
+
var MAX_LISTING_FAILURES2 = 3;
|
|
32991
|
+
function createDriveFileIndex(folderId) {
|
|
32992
|
+
let pending;
|
|
32993
|
+
let failures = 0;
|
|
32994
|
+
return {
|
|
32995
|
+
get(token) {
|
|
32996
|
+
if (!pending) {
|
|
32997
|
+
if (failures >= MAX_LISTING_FAILURES2) {
|
|
32998
|
+
return Promise.reject(
|
|
32999
|
+
new Error("The backup folder listing failed repeatedly; skipped for this run")
|
|
33000
|
+
);
|
|
33001
|
+
}
|
|
33002
|
+
pending = listAllDriveFiles(token, folderId).then((files) => {
|
|
33003
|
+
const byName = /* @__PURE__ */ new Map();
|
|
33004
|
+
for (const file of files) {
|
|
33005
|
+
if (!byName.has(file.name)) byName.set(file.name, file);
|
|
33006
|
+
}
|
|
33007
|
+
return byName;
|
|
33008
|
+
}).catch((err) => {
|
|
33009
|
+
pending = void 0;
|
|
33010
|
+
failures++;
|
|
33011
|
+
throw err;
|
|
33012
|
+
});
|
|
33013
|
+
}
|
|
33014
|
+
return pending;
|
|
33015
|
+
}
|
|
33016
|
+
};
|
|
33017
|
+
}
|
|
33018
|
+
async function readLocalUpdatedAt2(database, conversationId) {
|
|
33019
|
+
const { Q: Q19 } = await import("@nozbe/watermelondb");
|
|
33020
|
+
const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
|
|
33021
|
+
const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
|
|
33022
|
+
return match ? match.updatedAt : null;
|
|
33023
|
+
}
|
|
33024
|
+
async function pushConversationToDrive(database, conversationId, userAddress, token, folderId, fileIndex, deps, _retried = false) {
|
|
32914
33025
|
try {
|
|
32915
33026
|
await deps.requestEncryptionKey(userAddress);
|
|
32916
|
-
const folderResult = await getConversationsFolder(
|
|
32917
|
-
token,
|
|
32918
|
-
deps.requestDriveAccess,
|
|
32919
|
-
rootFolder,
|
|
32920
|
-
subfolder
|
|
32921
|
-
);
|
|
32922
|
-
if (!folderResult) return "failed";
|
|
32923
|
-
const { folderId, token: activeToken } = folderResult;
|
|
32924
33027
|
const filename = `${conversationId}.json`;
|
|
32925
|
-
const
|
|
33028
|
+
const index = await fileIndex.get(token);
|
|
33029
|
+
let existingFile = index.get(filename);
|
|
32926
33030
|
if (existingFile) {
|
|
32927
|
-
const
|
|
32928
|
-
const
|
|
32929
|
-
|
|
32930
|
-
|
|
32931
|
-
|
|
32932
|
-
|
|
32933
|
-
|
|
32934
|
-
|
|
33031
|
+
const localUpdatedAt = await readLocalUpdatedAt2(database, conversationId);
|
|
33032
|
+
const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
|
|
33033
|
+
if (localUpdated !== null && localUpdated <= new Date(existingFile.modifiedTime).getTime()) {
|
|
33034
|
+
return "skipped";
|
|
33035
|
+
}
|
|
33036
|
+
const current = await getDriveFileMetadata(token, existingFile.id);
|
|
33037
|
+
if (!current) {
|
|
33038
|
+
index.delete(filename);
|
|
33039
|
+
existingFile = void 0;
|
|
33040
|
+
} else {
|
|
33041
|
+
index.set(filename, current);
|
|
33042
|
+
if (localUpdated !== null && localUpdated <= new Date(current.modifiedTime).getTime()) {
|
|
32935
33043
|
return "skipped";
|
|
32936
33044
|
}
|
|
32937
33045
|
}
|
|
@@ -32940,10 +33048,19 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
|
|
|
32940
33048
|
if (!exportResult.success || !exportResult.blob) {
|
|
32941
33049
|
return "failed";
|
|
32942
33050
|
}
|
|
33051
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32943
33052
|
if (existingFile) {
|
|
32944
|
-
await updateDriveFile(
|
|
33053
|
+
await updateDriveFile(token, existingFile.id, exportResult.blob);
|
|
33054
|
+
index.set(filename, { ...existingFile, modifiedTime: now });
|
|
32945
33055
|
} else {
|
|
32946
|
-
await uploadFileToDrive(
|
|
33056
|
+
const created = await uploadFileToDrive(token, folderId, exportResult.blob, filename);
|
|
33057
|
+
index.set(filename, {
|
|
33058
|
+
id: created.id,
|
|
33059
|
+
name: created.name,
|
|
33060
|
+
createdTime: now,
|
|
33061
|
+
modifiedTime: now,
|
|
33062
|
+
size: String(exportResult.blob.size)
|
|
33063
|
+
});
|
|
32947
33064
|
}
|
|
32948
33065
|
return "uploaded";
|
|
32949
33066
|
} catch (err) {
|
|
@@ -32955,9 +33072,9 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
|
|
|
32955
33072
|
conversationId,
|
|
32956
33073
|
userAddress,
|
|
32957
33074
|
newToken,
|
|
33075
|
+
folderId,
|
|
33076
|
+
fileIndex,
|
|
32958
33077
|
deps,
|
|
32959
|
-
rootFolder,
|
|
32960
|
-
subfolder,
|
|
32961
33078
|
true
|
|
32962
33079
|
);
|
|
32963
33080
|
} catch {
|
|
@@ -32978,7 +33095,8 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
|
|
|
32978
33095
|
if (!folderResult) {
|
|
32979
33096
|
return { success: false, uploaded: 0, skipped: 0, total: 0 };
|
|
32980
33097
|
}
|
|
32981
|
-
const { token: activeToken } = folderResult;
|
|
33098
|
+
const { folderId, token: activeToken } = folderResult;
|
|
33099
|
+
const fileIndex = createDriveFileIndex(folderId);
|
|
32982
33100
|
const { Q: Q19 } = await import("@nozbe/watermelondb");
|
|
32983
33101
|
const conversationsCollection = database.get("conversations");
|
|
32984
33102
|
const records = await conversationsCollection.query(Q19.where("is_deleted", false)).fetch();
|
|
@@ -32997,9 +33115,9 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
|
|
|
32997
33115
|
conv.conversationId,
|
|
32998
33116
|
userAddress,
|
|
32999
33117
|
activeToken,
|
|
33000
|
-
|
|
33001
|
-
|
|
33002
|
-
|
|
33118
|
+
folderId,
|
|
33119
|
+
fileIndex,
|
|
33120
|
+
deps
|
|
33003
33121
|
);
|
|
33004
33122
|
if (result === "uploaded") uploaded++;
|
|
33005
33123
|
if (result === "skipped") skipped++;
|
package/dist/react/index.mjs
CHANGED
|
@@ -30194,6 +30194,22 @@ async function uploadFileToDropbox(accessToken, filename, content, folder = DEFA
|
|
|
30194
30194
|
}
|
|
30195
30195
|
return response.json();
|
|
30196
30196
|
}
|
|
30197
|
+
async function getDropboxFileMetadata(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
|
|
30198
|
+
const response = await fetch(`${DROPBOX_API_URL}/files/get_metadata`, {
|
|
30199
|
+
method: "POST",
|
|
30200
|
+
headers: {
|
|
30201
|
+
Authorization: `Bearer ${accessToken}`,
|
|
30202
|
+
"Content-Type": "application/json"
|
|
30203
|
+
},
|
|
30204
|
+
body: JSON.stringify({ path: `${folder}/${filename}` })
|
|
30205
|
+
});
|
|
30206
|
+
if (response.status === 409) return null;
|
|
30207
|
+
if (!response.ok) {
|
|
30208
|
+
const errorText = await response.text();
|
|
30209
|
+
throw new Error(`Dropbox get metadata failed: ${response.status} - ${errorText}`);
|
|
30210
|
+
}
|
|
30211
|
+
return response.json();
|
|
30212
|
+
}
|
|
30197
30213
|
async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
|
|
30198
30214
|
await ensureBackupFolder(accessToken, folder);
|
|
30199
30215
|
const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
|
|
@@ -30259,27 +30275,61 @@ async function downloadDropboxFile(accessToken, path) {
|
|
|
30259
30275
|
}
|
|
30260
30276
|
return response.blob();
|
|
30261
30277
|
}
|
|
30262
|
-
async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
|
|
30263
|
-
const files = await listDropboxFiles(accessToken, folder);
|
|
30264
|
-
return files.find((f) => f.name === filename) || null;
|
|
30265
|
-
}
|
|
30266
30278
|
|
|
30267
30279
|
// src/lib/backup/dropbox/backup.ts
|
|
30268
30280
|
var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
|
|
30269
|
-
|
|
30281
|
+
var MAX_LISTING_FAILURES = 3;
|
|
30282
|
+
function createDropboxFileIndex(backupFolder) {
|
|
30283
|
+
let pending;
|
|
30284
|
+
let failures = 0;
|
|
30285
|
+
return {
|
|
30286
|
+
get(token) {
|
|
30287
|
+
if (!pending) {
|
|
30288
|
+
if (failures >= MAX_LISTING_FAILURES) {
|
|
30289
|
+
return Promise.reject(
|
|
30290
|
+
new Error("The backup folder listing failed repeatedly; skipped for this run")
|
|
30291
|
+
);
|
|
30292
|
+
}
|
|
30293
|
+
pending = listDropboxFiles(token, backupFolder).then((files) => {
|
|
30294
|
+
const byName = /* @__PURE__ */ new Map();
|
|
30295
|
+
for (const file of files) {
|
|
30296
|
+
if (!byName.has(file.name)) byName.set(file.name, file);
|
|
30297
|
+
}
|
|
30298
|
+
return byName;
|
|
30299
|
+
}).catch((err) => {
|
|
30300
|
+
pending = void 0;
|
|
30301
|
+
failures++;
|
|
30302
|
+
throw err;
|
|
30303
|
+
});
|
|
30304
|
+
}
|
|
30305
|
+
return pending;
|
|
30306
|
+
}
|
|
30307
|
+
};
|
|
30308
|
+
}
|
|
30309
|
+
async function readLocalUpdatedAt(database, conversationId) {
|
|
30310
|
+
const { Q: Q19 } = await import("@nozbe/watermelondb");
|
|
30311
|
+
const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
|
|
30312
|
+
const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
|
|
30313
|
+
return match ? match.updatedAt : null;
|
|
30314
|
+
}
|
|
30315
|
+
async function pushConversationToDropbox(database, conversationId, userAddress, token, fileIndex, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
|
|
30270
30316
|
try {
|
|
30271
30317
|
await deps.requestEncryptionKey(userAddress);
|
|
30272
30318
|
const filename = `${conversationId}.json`;
|
|
30273
|
-
const
|
|
30319
|
+
const index = await fileIndex.get(token);
|
|
30320
|
+
const existingFile = index.get(filename);
|
|
30274
30321
|
if (existingFile) {
|
|
30275
|
-
const
|
|
30276
|
-
const
|
|
30277
|
-
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
|
|
30281
|
-
|
|
30282
|
-
|
|
30322
|
+
const localUpdatedAt = await readLocalUpdatedAt(database, conversationId);
|
|
30323
|
+
const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
|
|
30324
|
+
if (localUpdated !== null && localUpdated <= new Date(existingFile.server_modified).getTime()) {
|
|
30325
|
+
return "skipped";
|
|
30326
|
+
}
|
|
30327
|
+
const current = await getDropboxFileMetadata(token, filename, backupFolder);
|
|
30328
|
+
if (!current) {
|
|
30329
|
+
index.delete(filename);
|
|
30330
|
+
} else {
|
|
30331
|
+
index.set(filename, current);
|
|
30332
|
+
if (localUpdated !== null && localUpdated <= new Date(current.server_modified).getTime()) {
|
|
30283
30333
|
return "skipped";
|
|
30284
30334
|
}
|
|
30285
30335
|
}
|
|
@@ -30288,7 +30338,8 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
|
|
|
30288
30338
|
if (!exportResult.success || !exportResult.blob) {
|
|
30289
30339
|
return "failed";
|
|
30290
30340
|
}
|
|
30291
|
-
await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
|
|
30341
|
+
const uploaded = await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
|
|
30342
|
+
index.set(filename, uploaded);
|
|
30292
30343
|
return "uploaded";
|
|
30293
30344
|
} catch (err) {
|
|
30294
30345
|
if (isAuthError(err) && !_retried) {
|
|
@@ -30299,6 +30350,7 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
|
|
|
30299
30350
|
conversationId,
|
|
30300
30351
|
userAddress,
|
|
30301
30352
|
newToken,
|
|
30353
|
+
fileIndex,
|
|
30302
30354
|
deps,
|
|
30303
30355
|
backupFolder,
|
|
30304
30356
|
true
|
|
@@ -30322,6 +30374,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
|
|
|
30322
30374
|
}
|
|
30323
30375
|
let uploaded = 0;
|
|
30324
30376
|
let skipped = 0;
|
|
30377
|
+
const fileIndex = createDropboxFileIndex(backupFolder);
|
|
30325
30378
|
for (let i = 0; i < conversations.length; i++) {
|
|
30326
30379
|
const conv = conversations[i];
|
|
30327
30380
|
onProgress?.(i + 1, total);
|
|
@@ -30330,6 +30383,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
|
|
|
30330
30383
|
conv.conversationId,
|
|
30331
30384
|
userAddress,
|
|
30332
30385
|
token,
|
|
30386
|
+
fileIndex,
|
|
30333
30387
|
deps,
|
|
30334
30388
|
backupFolder
|
|
30335
30389
|
);
|
|
@@ -30898,29 +30952,52 @@ async function listDriveFiles(accessToken, folderId) {
|
|
|
30898
30952
|
const data = await response.json();
|
|
30899
30953
|
return data.files ?? [];
|
|
30900
30954
|
}
|
|
30901
|
-
|
|
30902
|
-
|
|
30903
|
-
|
|
30904
|
-
|
|
30905
|
-
|
|
30906
|
-
|
|
30907
|
-
|
|
30908
|
-
|
|
30955
|
+
var DRIVE_MAX_PAGE_SIZE = 1e3;
|
|
30956
|
+
async function listAllDriveFiles(accessToken, folderId) {
|
|
30957
|
+
const query = `'${escapeQueryValue(folderId)}' in parents and trashed=false`;
|
|
30958
|
+
const fields = encodeURIComponent("nextPageToken,files(id,name,createdTime,modifiedTime,size)");
|
|
30959
|
+
const files = [];
|
|
30960
|
+
let pageToken;
|
|
30961
|
+
do {
|
|
30962
|
+
const tokenParam = pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "";
|
|
30963
|
+
const response = await fetch(
|
|
30964
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=${DRIVE_MAX_PAGE_SIZE}${tokenParam}`,
|
|
30965
|
+
{
|
|
30966
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
30967
|
+
}
|
|
30968
|
+
);
|
|
30969
|
+
if (!response.ok) {
|
|
30970
|
+
throw new Error(`Failed to list files: ${response.status}`);
|
|
30971
|
+
}
|
|
30972
|
+
const data = await response.json();
|
|
30973
|
+
files.push(...data.files ?? []);
|
|
30974
|
+
pageToken = data.nextPageToken;
|
|
30975
|
+
} while (pageToken);
|
|
30976
|
+
return files;
|
|
30909
30977
|
}
|
|
30910
|
-
async function
|
|
30911
|
-
const
|
|
30912
|
-
const fields = "files(id,name,createdTime,modifiedTime,size)";
|
|
30978
|
+
async function getDriveFileMetadata(accessToken, fileId) {
|
|
30979
|
+
const fields = encodeURIComponent("id,name,createdTime,modifiedTime,size,trashed");
|
|
30913
30980
|
const response = await fetch(
|
|
30914
|
-
`${DRIVE_API_URL}/files
|
|
30981
|
+
`${DRIVE_API_URL}/files/${encodeURIComponent(fileId)}?fields=${fields}`,
|
|
30915
30982
|
{
|
|
30916
30983
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
30917
30984
|
}
|
|
30918
30985
|
);
|
|
30986
|
+
if (response.status === 404) return null;
|
|
30919
30987
|
if (!response.ok) {
|
|
30920
|
-
throw new Error(`Failed to
|
|
30988
|
+
throw new Error(`Failed to read file: ${response.status}`);
|
|
30921
30989
|
}
|
|
30922
30990
|
const data = await response.json();
|
|
30923
|
-
return data.
|
|
30991
|
+
return data.trashed ? null : data;
|
|
30992
|
+
}
|
|
30993
|
+
async function downloadDriveFile(accessToken, fileId) {
|
|
30994
|
+
const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
|
|
30995
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
30996
|
+
});
|
|
30997
|
+
if (!response.ok) {
|
|
30998
|
+
throw new Error(`Failed to download file: ${response.status}`);
|
|
30999
|
+
}
|
|
31000
|
+
return response.blob();
|
|
30924
31001
|
}
|
|
30925
31002
|
|
|
30926
31003
|
// src/lib/backup/google/backup.ts
|
|
@@ -30942,28 +31019,59 @@ async function getConversationsFolder(token, requestDriveAccess, rootFolder, sub
|
|
|
30942
31019
|
throw err;
|
|
30943
31020
|
}
|
|
30944
31021
|
}
|
|
30945
|
-
|
|
31022
|
+
var MAX_LISTING_FAILURES2 = 3;
|
|
31023
|
+
function createDriveFileIndex(folderId) {
|
|
31024
|
+
let pending;
|
|
31025
|
+
let failures = 0;
|
|
31026
|
+
return {
|
|
31027
|
+
get(token) {
|
|
31028
|
+
if (!pending) {
|
|
31029
|
+
if (failures >= MAX_LISTING_FAILURES2) {
|
|
31030
|
+
return Promise.reject(
|
|
31031
|
+
new Error("The backup folder listing failed repeatedly; skipped for this run")
|
|
31032
|
+
);
|
|
31033
|
+
}
|
|
31034
|
+
pending = listAllDriveFiles(token, folderId).then((files) => {
|
|
31035
|
+
const byName = /* @__PURE__ */ new Map();
|
|
31036
|
+
for (const file of files) {
|
|
31037
|
+
if (!byName.has(file.name)) byName.set(file.name, file);
|
|
31038
|
+
}
|
|
31039
|
+
return byName;
|
|
31040
|
+
}).catch((err) => {
|
|
31041
|
+
pending = void 0;
|
|
31042
|
+
failures++;
|
|
31043
|
+
throw err;
|
|
31044
|
+
});
|
|
31045
|
+
}
|
|
31046
|
+
return pending;
|
|
31047
|
+
}
|
|
31048
|
+
};
|
|
31049
|
+
}
|
|
31050
|
+
async function readLocalUpdatedAt2(database, conversationId) {
|
|
31051
|
+
const { Q: Q19 } = await import("@nozbe/watermelondb");
|
|
31052
|
+
const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
|
|
31053
|
+
const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
|
|
31054
|
+
return match ? match.updatedAt : null;
|
|
31055
|
+
}
|
|
31056
|
+
async function pushConversationToDrive(database, conversationId, userAddress, token, folderId, fileIndex, deps, _retried = false) {
|
|
30946
31057
|
try {
|
|
30947
31058
|
await deps.requestEncryptionKey(userAddress);
|
|
30948
|
-
const folderResult = await getConversationsFolder(
|
|
30949
|
-
token,
|
|
30950
|
-
deps.requestDriveAccess,
|
|
30951
|
-
rootFolder,
|
|
30952
|
-
subfolder
|
|
30953
|
-
);
|
|
30954
|
-
if (!folderResult) return "failed";
|
|
30955
|
-
const { folderId, token: activeToken } = folderResult;
|
|
30956
31059
|
const filename = `${conversationId}.json`;
|
|
30957
|
-
const
|
|
31060
|
+
const index = await fileIndex.get(token);
|
|
31061
|
+
let existingFile = index.get(filename);
|
|
30958
31062
|
if (existingFile) {
|
|
30959
|
-
const
|
|
30960
|
-
const
|
|
30961
|
-
|
|
30962
|
-
|
|
30963
|
-
|
|
30964
|
-
|
|
30965
|
-
|
|
30966
|
-
|
|
31063
|
+
const localUpdatedAt = await readLocalUpdatedAt2(database, conversationId);
|
|
31064
|
+
const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
|
|
31065
|
+
if (localUpdated !== null && localUpdated <= new Date(existingFile.modifiedTime).getTime()) {
|
|
31066
|
+
return "skipped";
|
|
31067
|
+
}
|
|
31068
|
+
const current = await getDriveFileMetadata(token, existingFile.id);
|
|
31069
|
+
if (!current) {
|
|
31070
|
+
index.delete(filename);
|
|
31071
|
+
existingFile = void 0;
|
|
31072
|
+
} else {
|
|
31073
|
+
index.set(filename, current);
|
|
31074
|
+
if (localUpdated !== null && localUpdated <= new Date(current.modifiedTime).getTime()) {
|
|
30967
31075
|
return "skipped";
|
|
30968
31076
|
}
|
|
30969
31077
|
}
|
|
@@ -30972,10 +31080,19 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
|
|
|
30972
31080
|
if (!exportResult.success || !exportResult.blob) {
|
|
30973
31081
|
return "failed";
|
|
30974
31082
|
}
|
|
31083
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
30975
31084
|
if (existingFile) {
|
|
30976
|
-
await updateDriveFile(
|
|
31085
|
+
await updateDriveFile(token, existingFile.id, exportResult.blob);
|
|
31086
|
+
index.set(filename, { ...existingFile, modifiedTime: now });
|
|
30977
31087
|
} else {
|
|
30978
|
-
await uploadFileToDrive(
|
|
31088
|
+
const created = await uploadFileToDrive(token, folderId, exportResult.blob, filename);
|
|
31089
|
+
index.set(filename, {
|
|
31090
|
+
id: created.id,
|
|
31091
|
+
name: created.name,
|
|
31092
|
+
createdTime: now,
|
|
31093
|
+
modifiedTime: now,
|
|
31094
|
+
size: String(exportResult.blob.size)
|
|
31095
|
+
});
|
|
30979
31096
|
}
|
|
30980
31097
|
return "uploaded";
|
|
30981
31098
|
} catch (err) {
|
|
@@ -30987,9 +31104,9 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
|
|
|
30987
31104
|
conversationId,
|
|
30988
31105
|
userAddress,
|
|
30989
31106
|
newToken,
|
|
31107
|
+
folderId,
|
|
31108
|
+
fileIndex,
|
|
30990
31109
|
deps,
|
|
30991
|
-
rootFolder,
|
|
30992
|
-
subfolder,
|
|
30993
31110
|
true
|
|
30994
31111
|
);
|
|
30995
31112
|
} catch {
|
|
@@ -31010,7 +31127,8 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
|
|
|
31010
31127
|
if (!folderResult) {
|
|
31011
31128
|
return { success: false, uploaded: 0, skipped: 0, total: 0 };
|
|
31012
31129
|
}
|
|
31013
|
-
const { token: activeToken } = folderResult;
|
|
31130
|
+
const { folderId, token: activeToken } = folderResult;
|
|
31131
|
+
const fileIndex = createDriveFileIndex(folderId);
|
|
31014
31132
|
const { Q: Q19 } = await import("@nozbe/watermelondb");
|
|
31015
31133
|
const conversationsCollection = database.get("conversations");
|
|
31016
31134
|
const records = await conversationsCollection.query(Q19.where("is_deleted", false)).fetch();
|
|
@@ -31029,9 +31147,9 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
|
|
|
31029
31147
|
conv.conversationId,
|
|
31030
31148
|
userAddress,
|
|
31031
31149
|
activeToken,
|
|
31032
|
-
|
|
31033
|
-
|
|
31034
|
-
|
|
31150
|
+
folderId,
|
|
31151
|
+
fileIndex,
|
|
31152
|
+
deps
|
|
31035
31153
|
);
|
|
31036
31154
|
if (result === "uploaded") uploaded++;
|
|
31037
31155
|
if (result === "skipped") skipped++;
|