@opengeni/documents 0.2.66 → 0.2.69
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/google-drive.d.ts +160 -0
- package/dist/google-drive.js +588 -0
- package/dist/google-drive.js.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
- package/src/google-drive.ts +887 -0
- package/src/index.ts +20 -5
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
import type { ScopedKnowledgeScope } from "@opengeni/contracts";
|
|
2
|
+
import type {
|
|
3
|
+
GoogleDriveSelectedSource,
|
|
4
|
+
GoogleDriveTargetScope,
|
|
5
|
+
} from "@opengeni/contracts/google-drive";
|
|
6
|
+
|
|
7
|
+
export const GOOGLE_DRIVE_PROVIDER_KEY = "google-drive" as const;
|
|
8
|
+
export const GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder" as const;
|
|
9
|
+
export const GOOGLE_DRIVE_SHORTCUT_MIME_TYPE = "application/vnd.google-apps.shortcut" as const;
|
|
10
|
+
|
|
11
|
+
const GOOGLE_DRIVE_NATIVE_MIME_PREFIX = "application/vnd.google-apps.";
|
|
12
|
+
const GOOGLE_DRIVE_MAX_ID_CHARS = 256;
|
|
13
|
+
const GOOGLE_DRIVE_MAX_NAME_CHARS = 1024;
|
|
14
|
+
const GOOGLE_DRIVE_MAX_MIME_CHARS = 256;
|
|
15
|
+
const GOOGLE_DRIVE_MAX_PAGE_TOKEN_CHARS = 4096;
|
|
16
|
+
const GOOGLE_DRIVE_MAX_PAGE_ITEMS = 100;
|
|
17
|
+
const GOOGLE_DRIVE_MAX_CHECKPOINT_FOLDERS = 2_000;
|
|
18
|
+
const GOOGLE_DRIVE_MAX_CHECKPOINT_BYTES = 2 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
const GOOGLE_DRIVE_NATIVE_EXPORTS = new Map<string, { contentType: string; extension: string }>([
|
|
21
|
+
[
|
|
22
|
+
"application/vnd.google-apps.document",
|
|
23
|
+
{
|
|
24
|
+
contentType: "application/pdf",
|
|
25
|
+
extension: ".pdf",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"application/vnd.google-apps.spreadsheet",
|
|
30
|
+
{
|
|
31
|
+
contentType: "application/pdf",
|
|
32
|
+
extension: ".pdf",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"application/vnd.google-apps.presentation",
|
|
37
|
+
{
|
|
38
|
+
contentType: "application/pdf",
|
|
39
|
+
extension: ".pdf",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
["application/vnd.google-apps.drawing", { contentType: "application/pdf", extension: ".pdf" }],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const DEPENDENCY_FREE_ORDINARY_CONTENT_TYPES = new Set([
|
|
46
|
+
"application/json",
|
|
47
|
+
"application/pdf",
|
|
48
|
+
"application/xml",
|
|
49
|
+
"application/x-yaml",
|
|
50
|
+
"application/yaml",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const GENERIC_BINARY_CONTENT_TYPES = new Set(["application/octet-stream", "binary/octet-stream"]);
|
|
54
|
+
|
|
55
|
+
const DEPENDENCY_FREE_EXTENSION_CONTENT_TYPES = new Map([
|
|
56
|
+
[".csv", "text/csv"],
|
|
57
|
+
[".htm", "text/html"],
|
|
58
|
+
[".html", "text/html"],
|
|
59
|
+
[".json", "application/json"],
|
|
60
|
+
[".markdown", "text/markdown"],
|
|
61
|
+
[".md", "text/markdown"],
|
|
62
|
+
[".pdf", "application/pdf"],
|
|
63
|
+
[".text", "text/plain"],
|
|
64
|
+
[".tsv", "text/tab-separated-values"],
|
|
65
|
+
[".txt", "text/plain"],
|
|
66
|
+
[".xml", "application/xml"],
|
|
67
|
+
[".yaml", "application/x-yaml"],
|
|
68
|
+
[".yml", "application/x-yaml"],
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
export type GoogleDriveKnowledgeSourceIdentity = {
|
|
72
|
+
providerKey: typeof GOOGLE_DRIVE_PROVIDER_KEY;
|
|
73
|
+
externalTenantId: string;
|
|
74
|
+
externalSourceId: string;
|
|
75
|
+
sourceKind: "google-drive-my-drive" | "google-drive-shared-drive" | "google-drive-folder";
|
|
76
|
+
sourceUri: string;
|
|
77
|
+
scope: ScopedKnowledgeScope;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalized item metadata expected from a bounded Drive files.list adapter.
|
|
82
|
+
* The adapter owns provider JSON validation; this planner owns traversal,
|
|
83
|
+
* checkpointing, resource limits, stable identity, and transfer classification.
|
|
84
|
+
*/
|
|
85
|
+
export type GoogleDriveInventoryProviderItem = {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
mimeType: string;
|
|
89
|
+
driveId: string | null;
|
|
90
|
+
parents: string[];
|
|
91
|
+
modifiedTime: string | null;
|
|
92
|
+
createdTime: string | null;
|
|
93
|
+
version: string | null;
|
|
94
|
+
md5Checksum: string | null;
|
|
95
|
+
size: string | null;
|
|
96
|
+
webViewLink: string | null;
|
|
97
|
+
trashed: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type GoogleDriveInventoryPage = {
|
|
101
|
+
items: GoogleDriveInventoryProviderItem[];
|
|
102
|
+
nextPageToken: string | null;
|
|
103
|
+
incompleteSearch: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type GoogleDriveInventoryLimits = {
|
|
107
|
+
/** Hard cumulative provider-item discovery limit for this checkpoint. */
|
|
108
|
+
maxItems: number;
|
|
109
|
+
/** Hard cumulative known-byte limit for ordinary file downloads. */
|
|
110
|
+
maxKnownBytes: number;
|
|
111
|
+
/** Hard cumulative Drive list request/cost limit for this checkpoint. */
|
|
112
|
+
maxApiRequests: number;
|
|
113
|
+
/** Wall-clock budget for one invocation; checkpoint counters remain cumulative. */
|
|
114
|
+
maxElapsedMs: number;
|
|
115
|
+
/** Per-file byte ceiling. Unknown/export sizes must be enforced again while streaming. */
|
|
116
|
+
maxFileBytes: number;
|
|
117
|
+
/** Cycle/graph explosion guard for traversed folders. */
|
|
118
|
+
maxFolders: number;
|
|
119
|
+
/** Requested Drive page size. */
|
|
120
|
+
pageSize: number;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type GoogleDriveTransferSkipReason =
|
|
124
|
+
| "file_too_large"
|
|
125
|
+
| "folder_limit"
|
|
126
|
+
| "folder_loop"
|
|
127
|
+
| "shortcut_unsupported"
|
|
128
|
+
| "trashed"
|
|
129
|
+
| "unsupported_file_type"
|
|
130
|
+
| "unsupported_native_type";
|
|
131
|
+
|
|
132
|
+
export type GoogleDriveTransferPlan =
|
|
133
|
+
| { action: "traverse" }
|
|
134
|
+
| {
|
|
135
|
+
action: "export";
|
|
136
|
+
contentType: string;
|
|
137
|
+
filename: string;
|
|
138
|
+
declaredBytes: null;
|
|
139
|
+
}
|
|
140
|
+
| {
|
|
141
|
+
action: "download";
|
|
142
|
+
contentType: string;
|
|
143
|
+
filename: string;
|
|
144
|
+
declaredBytes: string | null;
|
|
145
|
+
}
|
|
146
|
+
| { action: "skip"; reason: GoogleDriveTransferSkipReason };
|
|
147
|
+
|
|
148
|
+
export type GoogleDriveInventoryEntry = {
|
|
149
|
+
externalObjectId: string;
|
|
150
|
+
externalVersionId: string | null;
|
|
151
|
+
sourceId: string;
|
|
152
|
+
parentFolderId: string;
|
|
153
|
+
driveId: string | null;
|
|
154
|
+
title: string;
|
|
155
|
+
mimeType: string;
|
|
156
|
+
modifiedTime: string | null;
|
|
157
|
+
createdTime: string | null;
|
|
158
|
+
sourceUri: string;
|
|
159
|
+
transfer: GoogleDriveTransferPlan;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type GoogleDriveInventoryIssue = {
|
|
163
|
+
code: "incomplete_search" | "invalid_page" | "provider_error";
|
|
164
|
+
folderId: string;
|
|
165
|
+
providerCode: string | null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type GoogleDriveInventoryStopReason =
|
|
169
|
+
| "api_request_limit"
|
|
170
|
+
| "elapsed_time_limit"
|
|
171
|
+
| "incomplete_search"
|
|
172
|
+
| "item_limit"
|
|
173
|
+
| "known_byte_limit"
|
|
174
|
+
| "provider_error";
|
|
175
|
+
|
|
176
|
+
export type GoogleDriveInventoryTotals = {
|
|
177
|
+
itemCount: number;
|
|
178
|
+
folderCount: number;
|
|
179
|
+
plannedFileCount: number;
|
|
180
|
+
skippedItemCount: number;
|
|
181
|
+
exportFileCount: number;
|
|
182
|
+
downloadFileCount: number;
|
|
183
|
+
unknownSizeFileCount: number;
|
|
184
|
+
apiRequestCount: number;
|
|
185
|
+
knownBytes: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
type GoogleDriveInventoryFrame = {
|
|
189
|
+
folderId: string;
|
|
190
|
+
driveId: string | null;
|
|
191
|
+
pageToken: string | null;
|
|
192
|
+
loaded: boolean;
|
|
193
|
+
bufferedItems: GoogleDriveInventoryProviderItem[];
|
|
194
|
+
nextPageToken: string | null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type GoogleDriveInventoryCheckpoint = {
|
|
198
|
+
version: 2;
|
|
199
|
+
googlePermissionId: string;
|
|
200
|
+
externalTenantId: string;
|
|
201
|
+
sourceId: string;
|
|
202
|
+
sourceDriveId: string | null;
|
|
203
|
+
scope: ScopedKnowledgeScope;
|
|
204
|
+
pendingFolders: GoogleDriveInventoryFrame[];
|
|
205
|
+
seenFolderIds: string[];
|
|
206
|
+
totals: GoogleDriveInventoryTotals;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export type GoogleDriveInventoryResult = {
|
|
210
|
+
status: "complete" | "paused";
|
|
211
|
+
stopReason: GoogleDriveInventoryStopReason | null;
|
|
212
|
+
source: GoogleDriveKnowledgeSourceIdentity;
|
|
213
|
+
entries: GoogleDriveInventoryEntry[];
|
|
214
|
+
issues: GoogleDriveInventoryIssue[];
|
|
215
|
+
totals: GoogleDriveInventoryTotals;
|
|
216
|
+
run: GoogleDriveInventoryTotals & { elapsedMs: number };
|
|
217
|
+
checkpoint: GoogleDriveInventoryCheckpoint | null;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type GoogleDriveListChildren = (input: {
|
|
221
|
+
folderId: string;
|
|
222
|
+
driveId: string | null;
|
|
223
|
+
pageToken: string | null;
|
|
224
|
+
pageSize: number;
|
|
225
|
+
}) => Promise<GoogleDriveInventoryPage>;
|
|
226
|
+
|
|
227
|
+
export class GoogleDriveInventoryProviderError extends Error {
|
|
228
|
+
constructor(readonly providerCode: string) {
|
|
229
|
+
super(providerCode);
|
|
230
|
+
this.name = "GoogleDriveInventoryProviderError";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function googleDriveKnowledgeScope(
|
|
235
|
+
targetScope: GoogleDriveTargetScope,
|
|
236
|
+
workspaceId: string,
|
|
237
|
+
initiatingSubjectId: string,
|
|
238
|
+
): ScopedKnowledgeScope {
|
|
239
|
+
const boundedWorkspaceId = boundedText(workspaceId, "workspaceId", 256);
|
|
240
|
+
const boundedSubjectId = boundedText(initiatingSubjectId, "initiatingSubjectId", 1024);
|
|
241
|
+
if (targetScope === "organization") {
|
|
242
|
+
return { kind: "organization", workspaceId: null, subjectId: null };
|
|
243
|
+
}
|
|
244
|
+
if (targetScope === "workspace") {
|
|
245
|
+
return { kind: "workspace", workspaceId: boundedWorkspaceId, subjectId: null };
|
|
246
|
+
}
|
|
247
|
+
// Google Drive connections are workspace-bound today. Preserve that boundary
|
|
248
|
+
// for personal knowledge rather than silently widening it across workspaces.
|
|
249
|
+
return {
|
|
250
|
+
kind: "personal",
|
|
251
|
+
workspaceId: boundedWorkspaceId,
|
|
252
|
+
subjectId: boundedSubjectId,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function googleDriveKnowledgeSourceIdentity(input: {
|
|
257
|
+
googlePermissionId: string;
|
|
258
|
+
source: Pick<GoogleDriveSelectedSource, "id" | "driveId" | "targetScope">;
|
|
259
|
+
workspaceId: string;
|
|
260
|
+
initiatingSubjectId: string;
|
|
261
|
+
}): GoogleDriveKnowledgeSourceIdentity {
|
|
262
|
+
const sourceId = driveId(input.source.id, "source.id");
|
|
263
|
+
const externalTenantId = normalizedGooglePermissionId(input.googlePermissionId);
|
|
264
|
+
const sourceDriveId = nullableDriveId(input.source.driveId, "source.driveId");
|
|
265
|
+
return {
|
|
266
|
+
providerKey: GOOGLE_DRIVE_PROVIDER_KEY,
|
|
267
|
+
externalTenantId,
|
|
268
|
+
externalSourceId: sourceId,
|
|
269
|
+
sourceKind:
|
|
270
|
+
sourceId === "root"
|
|
271
|
+
? "google-drive-my-drive"
|
|
272
|
+
: sourceDriveId === sourceId
|
|
273
|
+
? "google-drive-shared-drive"
|
|
274
|
+
: "google-drive-folder",
|
|
275
|
+
sourceUri: googleDriveSourceUri(sourceId),
|
|
276
|
+
scope: googleDriveKnowledgeScope(
|
|
277
|
+
input.source.targetScope,
|
|
278
|
+
input.workspaceId,
|
|
279
|
+
input.initiatingSubjectId,
|
|
280
|
+
),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function planGoogleDriveTransfer(
|
|
285
|
+
item: GoogleDriveInventoryProviderItem,
|
|
286
|
+
maxFileBytes: number,
|
|
287
|
+
): GoogleDriveTransferPlan {
|
|
288
|
+
const normalized = validatedProviderItem(item);
|
|
289
|
+
const byteLimit = safePositiveInteger(maxFileBytes, "maxFileBytes");
|
|
290
|
+
if (normalized.trashed) {
|
|
291
|
+
return { action: "skip", reason: "trashed" };
|
|
292
|
+
}
|
|
293
|
+
if (normalized.mimeType === GOOGLE_DRIVE_FOLDER_MIME_TYPE) {
|
|
294
|
+
return { action: "traverse" };
|
|
295
|
+
}
|
|
296
|
+
if (normalized.mimeType === GOOGLE_DRIVE_SHORTCUT_MIME_TYPE) {
|
|
297
|
+
return { action: "skip", reason: "shortcut_unsupported" };
|
|
298
|
+
}
|
|
299
|
+
const nativeExport = GOOGLE_DRIVE_NATIVE_EXPORTS.get(normalized.mimeType);
|
|
300
|
+
if (nativeExport) {
|
|
301
|
+
return {
|
|
302
|
+
action: "export",
|
|
303
|
+
contentType: nativeExport.contentType,
|
|
304
|
+
filename: exportedFilename(normalized.name, nativeExport.extension),
|
|
305
|
+
declaredBytes: null,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (normalized.mimeType.startsWith(GOOGLE_DRIVE_NATIVE_MIME_PREFIX)) {
|
|
309
|
+
return { action: "skip", reason: "unsupported_native_type" };
|
|
310
|
+
}
|
|
311
|
+
const declaredBytes = fileSize(normalized.size);
|
|
312
|
+
if (declaredBytes !== null && declaredBytes > BigInt(byteLimit)) {
|
|
313
|
+
return { action: "skip", reason: "file_too_large" };
|
|
314
|
+
}
|
|
315
|
+
const contentType = dependencyFreeOrdinaryContentType(normalized.name, normalized.mimeType);
|
|
316
|
+
if (!contentType) {
|
|
317
|
+
return { action: "skip", reason: "unsupported_file_type" };
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
action: "download",
|
|
321
|
+
contentType,
|
|
322
|
+
filename: safeFilename(normalized.name),
|
|
323
|
+
declaredBytes: declaredBytes?.toString() ?? null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function inventoryGoogleDriveSource(input: {
|
|
328
|
+
googlePermissionId: string;
|
|
329
|
+
source: GoogleDriveSelectedSource;
|
|
330
|
+
workspaceId: string;
|
|
331
|
+
initiatingSubjectId: string;
|
|
332
|
+
limits: GoogleDriveInventoryLimits;
|
|
333
|
+
listChildren: GoogleDriveListChildren;
|
|
334
|
+
checkpoint?: GoogleDriveInventoryCheckpoint | null;
|
|
335
|
+
now?: (() => number) | undefined;
|
|
336
|
+
}): Promise<GoogleDriveInventoryResult> {
|
|
337
|
+
const limits = validatedLimits(input.limits);
|
|
338
|
+
const googlePermissionId = normalizedGooglePermissionId(input.googlePermissionId);
|
|
339
|
+
const source = googleDriveKnowledgeSourceIdentity({
|
|
340
|
+
googlePermissionId,
|
|
341
|
+
source: input.source,
|
|
342
|
+
workspaceId: input.workspaceId,
|
|
343
|
+
initiatingSubjectId: input.initiatingSubjectId,
|
|
344
|
+
});
|
|
345
|
+
const checkpointIdentity: GoogleDriveInventoryCheckpointIdentity = {
|
|
346
|
+
googlePermissionId,
|
|
347
|
+
externalTenantId: source.externalTenantId,
|
|
348
|
+
sourceId: source.externalSourceId,
|
|
349
|
+
sourceDriveId: nullableDriveId(input.source.driveId, "source.driveId"),
|
|
350
|
+
scope: source.scope,
|
|
351
|
+
};
|
|
352
|
+
const now = input.now ?? Date.now;
|
|
353
|
+
const startedAt = now();
|
|
354
|
+
const checkpoint = input.checkpoint
|
|
355
|
+
? validatedCheckpoint(input.checkpoint, checkpointIdentity, limits)
|
|
356
|
+
: initialCheckpoint(checkpointIdentity);
|
|
357
|
+
const initialTotals = cloneTotals(checkpoint.totals);
|
|
358
|
+
const entries: GoogleDriveInventoryEntry[] = [];
|
|
359
|
+
const issues: GoogleDriveInventoryIssue[] = [];
|
|
360
|
+
let stopReason: GoogleDriveInventoryStopReason | null = null;
|
|
361
|
+
|
|
362
|
+
while (checkpoint.pendingFolders.length > 0) {
|
|
363
|
+
if (now() - startedAt >= limits.maxElapsedMs) {
|
|
364
|
+
stopReason = "elapsed_time_limit";
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
const frame = checkpoint.pendingFolders[0]!;
|
|
368
|
+
if (frame.loaded && frame.bufferedItems.length === 0) {
|
|
369
|
+
if (frame.nextPageToken) {
|
|
370
|
+
frame.pageToken = frame.nextPageToken;
|
|
371
|
+
frame.nextPageToken = null;
|
|
372
|
+
frame.loaded = false;
|
|
373
|
+
} else {
|
|
374
|
+
checkpoint.pendingFolders.shift();
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (checkpoint.totals.itemCount >= limits.maxItems) {
|
|
379
|
+
stopReason = "item_limit";
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!frame.loaded) {
|
|
384
|
+
if (checkpoint.totals.apiRequestCount >= limits.maxApiRequests) {
|
|
385
|
+
stopReason = "api_request_limit";
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
const remainingItems = limits.maxItems - checkpoint.totals.itemCount;
|
|
389
|
+
const requestedPageSize = Math.min(limits.pageSize, remainingItems);
|
|
390
|
+
let page: GoogleDriveInventoryPage;
|
|
391
|
+
try {
|
|
392
|
+
page = await input.listChildren({
|
|
393
|
+
folderId: frame.folderId,
|
|
394
|
+
driveId: frame.driveId,
|
|
395
|
+
pageToken: frame.pageToken,
|
|
396
|
+
pageSize: requestedPageSize,
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
checkpoint.totals.apiRequestCount += 1;
|
|
400
|
+
issues.push({
|
|
401
|
+
code: "provider_error",
|
|
402
|
+
folderId: frame.folderId,
|
|
403
|
+
providerCode: providerErrorCode(error),
|
|
404
|
+
});
|
|
405
|
+
stopReason = "provider_error";
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
checkpoint.totals.apiRequestCount += 1;
|
|
409
|
+
if (!validPage(page, requestedPageSize)) {
|
|
410
|
+
issues.push({ code: "invalid_page", folderId: frame.folderId, providerCode: null });
|
|
411
|
+
stopReason = "provider_error";
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
if (page.incompleteSearch) {
|
|
415
|
+
issues.push({ code: "incomplete_search", folderId: frame.folderId, providerCode: null });
|
|
416
|
+
stopReason = "incomplete_search";
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
frame.loaded = true;
|
|
420
|
+
frame.bufferedItems = page.items.map(validatedProviderItem);
|
|
421
|
+
frame.nextPageToken = page.nextPageToken;
|
|
422
|
+
if (now() - startedAt >= limits.maxElapsedMs) {
|
|
423
|
+
stopReason = "elapsed_time_limit";
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (frame.bufferedItems.length === 0) {
|
|
429
|
+
if (frame.nextPageToken) {
|
|
430
|
+
frame.pageToken = frame.nextPageToken;
|
|
431
|
+
frame.nextPageToken = null;
|
|
432
|
+
frame.loaded = false;
|
|
433
|
+
} else {
|
|
434
|
+
checkpoint.pendingFolders.shift();
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const item = validatedProviderItem(frame.bufferedItems[0]!);
|
|
440
|
+
let transfer = planGoogleDriveTransfer(item, limits.maxFileBytes);
|
|
441
|
+
if (transfer.action === "download" && transfer.declaredBytes !== null) {
|
|
442
|
+
const nextKnownBytes = BigInt(checkpoint.totals.knownBytes) + BigInt(transfer.declaredBytes);
|
|
443
|
+
if (nextKnownBytes > BigInt(limits.maxKnownBytes)) {
|
|
444
|
+
stopReason = "known_byte_limit";
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
frame.bufferedItems.shift();
|
|
450
|
+
checkpoint.totals.itemCount += 1;
|
|
451
|
+
const effectiveDriveId = item.driveId ?? frame.driveId;
|
|
452
|
+
if (transfer.action === "traverse") {
|
|
453
|
+
if (checkpoint.seenFolderIds.includes(item.id)) {
|
|
454
|
+
transfer = { action: "skip", reason: "folder_loop" };
|
|
455
|
+
} else if (checkpoint.seenFolderIds.length >= limits.maxFolders) {
|
|
456
|
+
transfer = { action: "skip", reason: "folder_limit" };
|
|
457
|
+
} else {
|
|
458
|
+
checkpoint.seenFolderIds.push(item.id);
|
|
459
|
+
checkpoint.totals.folderCount += 1;
|
|
460
|
+
checkpoint.pendingFolders.push({
|
|
461
|
+
folderId: item.id,
|
|
462
|
+
driveId: effectiveDriveId,
|
|
463
|
+
pageToken: null,
|
|
464
|
+
loaded: false,
|
|
465
|
+
bufferedItems: [],
|
|
466
|
+
nextPageToken: null,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (transfer.action === "skip") {
|
|
472
|
+
checkpoint.totals.skippedItemCount += 1;
|
|
473
|
+
} else if (transfer.action === "download") {
|
|
474
|
+
checkpoint.totals.plannedFileCount += 1;
|
|
475
|
+
checkpoint.totals.downloadFileCount += 1;
|
|
476
|
+
if (transfer.declaredBytes === null) {
|
|
477
|
+
checkpoint.totals.unknownSizeFileCount += 1;
|
|
478
|
+
} else {
|
|
479
|
+
checkpoint.totals.knownBytes = (
|
|
480
|
+
BigInt(checkpoint.totals.knownBytes) + BigInt(transfer.declaredBytes)
|
|
481
|
+
).toString();
|
|
482
|
+
}
|
|
483
|
+
} else if (transfer.action === "export") {
|
|
484
|
+
checkpoint.totals.plannedFileCount += 1;
|
|
485
|
+
checkpoint.totals.exportFileCount += 1;
|
|
486
|
+
checkpoint.totals.unknownSizeFileCount += 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
entries.push({
|
|
490
|
+
externalObjectId: item.id,
|
|
491
|
+
externalVersionId: item.version ?? item.md5Checksum ?? item.modifiedTime,
|
|
492
|
+
sourceId: source.externalSourceId,
|
|
493
|
+
parentFolderId: frame.folderId,
|
|
494
|
+
driveId: effectiveDriveId,
|
|
495
|
+
title: item.name,
|
|
496
|
+
mimeType: item.mimeType,
|
|
497
|
+
modifiedTime: item.modifiedTime,
|
|
498
|
+
createdTime: item.createdTime,
|
|
499
|
+
sourceUri: item.webViewLink ?? googleDriveFileUri(item.id),
|
|
500
|
+
transfer,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const elapsedMs = Math.max(0, now() - startedAt);
|
|
505
|
+
const complete = checkpoint.pendingFolders.length === 0;
|
|
506
|
+
const outputCheckpoint = complete ? null : cloneCheckpoint(checkpoint);
|
|
507
|
+
if (outputCheckpoint) assertCheckpointBytes(outputCheckpoint);
|
|
508
|
+
return {
|
|
509
|
+
status: complete ? "complete" : "paused",
|
|
510
|
+
stopReason: complete ? null : stopReason,
|
|
511
|
+
source,
|
|
512
|
+
entries,
|
|
513
|
+
issues,
|
|
514
|
+
totals: cloneTotals(checkpoint.totals),
|
|
515
|
+
run: {
|
|
516
|
+
...subtractTotals(checkpoint.totals, initialTotals),
|
|
517
|
+
elapsedMs,
|
|
518
|
+
},
|
|
519
|
+
checkpoint: outputCheckpoint,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
type GoogleDriveInventoryCheckpointIdentity = Pick<
|
|
524
|
+
GoogleDriveInventoryCheckpoint,
|
|
525
|
+
"googlePermissionId" | "externalTenantId" | "sourceId" | "sourceDriveId" | "scope"
|
|
526
|
+
>;
|
|
527
|
+
|
|
528
|
+
function initialCheckpoint(
|
|
529
|
+
identity: GoogleDriveInventoryCheckpointIdentity,
|
|
530
|
+
): GoogleDriveInventoryCheckpoint {
|
|
531
|
+
return {
|
|
532
|
+
version: 2,
|
|
533
|
+
googlePermissionId: identity.googlePermissionId,
|
|
534
|
+
externalTenantId: identity.externalTenantId,
|
|
535
|
+
sourceId: identity.sourceId,
|
|
536
|
+
sourceDriveId: identity.sourceDriveId,
|
|
537
|
+
scope: cloneScope(identity.scope),
|
|
538
|
+
pendingFolders: [
|
|
539
|
+
{
|
|
540
|
+
folderId: identity.sourceId,
|
|
541
|
+
driveId: identity.sourceDriveId,
|
|
542
|
+
pageToken: null,
|
|
543
|
+
loaded: false,
|
|
544
|
+
bufferedItems: [],
|
|
545
|
+
nextPageToken: null,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
seenFolderIds: [identity.sourceId],
|
|
549
|
+
totals: emptyTotals(),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function validatedCheckpoint(
|
|
554
|
+
value: GoogleDriveInventoryCheckpoint,
|
|
555
|
+
identity: GoogleDriveInventoryCheckpointIdentity,
|
|
556
|
+
limits: GoogleDriveInventoryLimits,
|
|
557
|
+
): GoogleDriveInventoryCheckpoint {
|
|
558
|
+
if (!value || typeof value !== "object" || value.version !== 2) {
|
|
559
|
+
throw new Error("unsupported Google Drive inventory checkpoint");
|
|
560
|
+
}
|
|
561
|
+
if (
|
|
562
|
+
value.googlePermissionId !== identity.googlePermissionId ||
|
|
563
|
+
value.externalTenantId !== identity.externalTenantId ||
|
|
564
|
+
value.sourceId !== identity.sourceId ||
|
|
565
|
+
value.sourceDriveId !== identity.sourceDriveId ||
|
|
566
|
+
!sameScope(value.scope, identity.scope)
|
|
567
|
+
) {
|
|
568
|
+
throw new Error("Google Drive inventory checkpoint does not match the selected source");
|
|
569
|
+
}
|
|
570
|
+
const checkpoint = cloneCheckpoint(value);
|
|
571
|
+
if (checkpoint.pendingFolders.length === 0) {
|
|
572
|
+
throw new Error("Google Drive inventory checkpoint is already complete");
|
|
573
|
+
}
|
|
574
|
+
if (
|
|
575
|
+
checkpoint.pendingFolders.length > GOOGLE_DRIVE_MAX_CHECKPOINT_FOLDERS ||
|
|
576
|
+
checkpoint.seenFolderIds.length > GOOGLE_DRIVE_MAX_CHECKPOINT_FOLDERS ||
|
|
577
|
+
checkpoint.seenFolderIds.length > limits.maxFolders
|
|
578
|
+
) {
|
|
579
|
+
throw new Error("Google Drive inventory checkpoint exceeds the folder limit");
|
|
580
|
+
}
|
|
581
|
+
if (!checkpoint.seenFolderIds.includes(identity.sourceId)) {
|
|
582
|
+
throw new Error("Google Drive inventory checkpoint lost its source boundary");
|
|
583
|
+
}
|
|
584
|
+
if (new Set(checkpoint.seenFolderIds).size !== checkpoint.seenFolderIds.length) {
|
|
585
|
+
throw new Error("Google Drive inventory checkpoint contains duplicate folder identity");
|
|
586
|
+
}
|
|
587
|
+
for (const folderId of checkpoint.seenFolderIds) driveId(folderId, "checkpoint.folderId");
|
|
588
|
+
const pendingFolderIds = new Set<string>();
|
|
589
|
+
for (const frame of checkpoint.pendingFolders) {
|
|
590
|
+
driveId(frame.folderId, "checkpoint.pending.folderId");
|
|
591
|
+
if (
|
|
592
|
+
!checkpoint.seenFolderIds.includes(frame.folderId) ||
|
|
593
|
+
pendingFolderIds.has(frame.folderId)
|
|
594
|
+
) {
|
|
595
|
+
throw new Error("Google Drive inventory checkpoint contains an invalid pending folder");
|
|
596
|
+
}
|
|
597
|
+
pendingFolderIds.add(frame.folderId);
|
|
598
|
+
nullableDriveId(frame.driveId, "checkpoint.pending.driveId");
|
|
599
|
+
pageToken(frame.pageToken, "checkpoint.pending.pageToken");
|
|
600
|
+
pageToken(frame.nextPageToken, "checkpoint.pending.nextPageToken");
|
|
601
|
+
if (
|
|
602
|
+
typeof frame.loaded !== "boolean" ||
|
|
603
|
+
frame.bufferedItems.length > GOOGLE_DRIVE_MAX_PAGE_ITEMS
|
|
604
|
+
) {
|
|
605
|
+
throw new Error("Google Drive inventory checkpoint contains an invalid page frame");
|
|
606
|
+
}
|
|
607
|
+
if (!frame.loaded && (frame.bufferedItems.length > 0 || frame.nextPageToken !== null)) {
|
|
608
|
+
throw new Error("Google Drive inventory checkpoint contains an uncommitted page frame");
|
|
609
|
+
}
|
|
610
|
+
frame.bufferedItems = frame.bufferedItems.map(validatedProviderItem);
|
|
611
|
+
}
|
|
612
|
+
validatedTotals(checkpoint.totals);
|
|
613
|
+
if (
|
|
614
|
+
checkpoint.totals.folderCount !== checkpoint.seenFolderIds.length ||
|
|
615
|
+
checkpoint.totals.plannedFileCount !==
|
|
616
|
+
checkpoint.totals.exportFileCount + checkpoint.totals.downloadFileCount ||
|
|
617
|
+
checkpoint.totals.unknownSizeFileCount > checkpoint.totals.plannedFileCount ||
|
|
618
|
+
checkpoint.totals.itemCount !==
|
|
619
|
+
checkpoint.totals.skippedItemCount +
|
|
620
|
+
checkpoint.totals.plannedFileCount +
|
|
621
|
+
checkpoint.totals.folderCount -
|
|
622
|
+
1
|
|
623
|
+
) {
|
|
624
|
+
throw new Error("Google Drive inventory checkpoint totals are inconsistent");
|
|
625
|
+
}
|
|
626
|
+
if (
|
|
627
|
+
checkpoint.totals.itemCount > limits.maxItems ||
|
|
628
|
+
checkpoint.totals.apiRequestCount > limits.maxApiRequests ||
|
|
629
|
+
BigInt(checkpoint.totals.knownBytes) > BigInt(limits.maxKnownBytes)
|
|
630
|
+
) {
|
|
631
|
+
throw new Error("Google Drive inventory checkpoint exceeds the supplied limits");
|
|
632
|
+
}
|
|
633
|
+
assertCheckpointBytes(checkpoint);
|
|
634
|
+
return checkpoint;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function validatedLimits(value: GoogleDriveInventoryLimits): GoogleDriveInventoryLimits {
|
|
638
|
+
const limits = {
|
|
639
|
+
maxItems: safePositiveInteger(value.maxItems, "limits.maxItems"),
|
|
640
|
+
maxKnownBytes: safePositiveInteger(value.maxKnownBytes, "limits.maxKnownBytes"),
|
|
641
|
+
maxApiRequests: safePositiveInteger(value.maxApiRequests, "limits.maxApiRequests"),
|
|
642
|
+
maxElapsedMs: safePositiveInteger(value.maxElapsedMs, "limits.maxElapsedMs"),
|
|
643
|
+
maxFileBytes: safePositiveInteger(value.maxFileBytes, "limits.maxFileBytes"),
|
|
644
|
+
maxFolders: safePositiveInteger(value.maxFolders, "limits.maxFolders"),
|
|
645
|
+
pageSize: safePositiveInteger(value.pageSize, "limits.pageSize"),
|
|
646
|
+
};
|
|
647
|
+
if (limits.pageSize > GOOGLE_DRIVE_MAX_PAGE_ITEMS) {
|
|
648
|
+
throw new Error(`limits.pageSize must be <= ${GOOGLE_DRIVE_MAX_PAGE_ITEMS}`);
|
|
649
|
+
}
|
|
650
|
+
if (limits.maxFolders > GOOGLE_DRIVE_MAX_CHECKPOINT_FOLDERS) {
|
|
651
|
+
throw new Error(`limits.maxFolders must be <= ${GOOGLE_DRIVE_MAX_CHECKPOINT_FOLDERS}`);
|
|
652
|
+
}
|
|
653
|
+
return limits;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function validPage(value: GoogleDriveInventoryPage, expectedMaxItems: number): boolean {
|
|
657
|
+
if (
|
|
658
|
+
!value ||
|
|
659
|
+
!Array.isArray(value.items) ||
|
|
660
|
+
value.items.length > expectedMaxItems ||
|
|
661
|
+
value.items.length > GOOGLE_DRIVE_MAX_PAGE_ITEMS
|
|
662
|
+
) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
pageToken(value.nextPageToken, "page.nextPageToken");
|
|
667
|
+
value.items.forEach(validatedProviderItem);
|
|
668
|
+
return typeof value.incompleteSearch === "boolean";
|
|
669
|
+
} catch {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function validatedProviderItem(
|
|
675
|
+
item: GoogleDriveInventoryProviderItem,
|
|
676
|
+
): GoogleDriveInventoryProviderItem {
|
|
677
|
+
const id = driveId(item.id, "item.id");
|
|
678
|
+
const name = boundedText(item.name, "item.name", GOOGLE_DRIVE_MAX_NAME_CHARS);
|
|
679
|
+
const mimeType = boundedText(item.mimeType, "item.mimeType", GOOGLE_DRIVE_MAX_MIME_CHARS);
|
|
680
|
+
const parents = Array.isArray(item.parents)
|
|
681
|
+
? item.parents.map((parent) => driveId(parent, "item.parent"))
|
|
682
|
+
: [];
|
|
683
|
+
if (parents.length > 100) throw new Error("item.parents exceeds the supported bound");
|
|
684
|
+
return {
|
|
685
|
+
id,
|
|
686
|
+
name,
|
|
687
|
+
mimeType,
|
|
688
|
+
driveId: nullableDriveId(item.driveId, "item.driveId"),
|
|
689
|
+
parents,
|
|
690
|
+
modifiedTime: nullableBoundedText(item.modifiedTime, "item.modifiedTime", 128),
|
|
691
|
+
createdTime: nullableBoundedText(item.createdTime, "item.createdTime", 128),
|
|
692
|
+
version: nullableBoundedText(item.version, "item.version", 256),
|
|
693
|
+
md5Checksum: nullableBoundedText(item.md5Checksum, "item.md5Checksum", 128),
|
|
694
|
+
size: fileSize(item.size)?.toString() ?? null,
|
|
695
|
+
webViewLink: nullableHttpsUrl(item.webViewLink, "item.webViewLink"),
|
|
696
|
+
trashed: item.trashed === true,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function dependencyFreeOrdinaryContentType(name: string, mimeType: string): string | null {
|
|
701
|
+
const normalizedMime = mimeType.trim().toLowerCase();
|
|
702
|
+
if (normalizedMime.startsWith("text/")) return normalizedMime;
|
|
703
|
+
if (DEPENDENCY_FREE_ORDINARY_CONTENT_TYPES.has(normalizedMime)) return normalizedMime;
|
|
704
|
+
if (!GENERIC_BINARY_CONTENT_TYPES.has(normalizedMime)) return null;
|
|
705
|
+
const lowerName = name.toLowerCase();
|
|
706
|
+
for (const [extension, contentType] of DEPENDENCY_FREE_EXTENSION_CONTENT_TYPES) {
|
|
707
|
+
if (lowerName.endsWith(extension)) return contentType;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function exportedFilename(name: string, extension: string): string {
|
|
713
|
+
const filename = safeFilename(name);
|
|
714
|
+
return filename.toLowerCase().endsWith(extension) ? filename : `${filename}${extension}`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function safeFilename(value: string): string {
|
|
718
|
+
const cleaned = value
|
|
719
|
+
.normalize("NFKC")
|
|
720
|
+
.replace(/[\u0000-\u001f\u007f/\\]/gu, " ")
|
|
721
|
+
.replace(/\s+/gu, " ")
|
|
722
|
+
.trim()
|
|
723
|
+
.slice(0, 512);
|
|
724
|
+
return cleaned || "untitled";
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function googleDriveSourceUri(id: string): string {
|
|
728
|
+
return id === "root"
|
|
729
|
+
? "https://drive.google.com/drive/my-drive"
|
|
730
|
+
: `https://drive.google.com/drive/folders/${encodeURIComponent(id)}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function googleDriveFileUri(id: string): string {
|
|
734
|
+
return `https://drive.google.com/open?id=${encodeURIComponent(id)}`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function fileSize(value: string | null): bigint | null {
|
|
738
|
+
if (value === null) return null;
|
|
739
|
+
if (!/^\d{1,40}$/u.test(value)) throw new Error("item.size is invalid");
|
|
740
|
+
return BigInt(value);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function providerErrorCode(error: unknown): string | null {
|
|
744
|
+
if (!(error instanceof GoogleDriveInventoryProviderError)) return null;
|
|
745
|
+
const normalized = error.providerCode.trim().toLowerCase();
|
|
746
|
+
return /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u.test(normalized) ? normalized : null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function driveId(value: string, label: string): string {
|
|
750
|
+
const candidate = boundedText(value, label, GOOGLE_DRIVE_MAX_ID_CHARS);
|
|
751
|
+
if (candidate === "root" || /^[A-Za-z0-9_-]+$/u.test(candidate)) return candidate;
|
|
752
|
+
throw new Error(`${label} is invalid`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function normalizedGooglePermissionId(value: string): string {
|
|
756
|
+
return boundedText(value, "googlePermissionId", GOOGLE_DRIVE_MAX_ID_CHARS);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function nullableDriveId(value: string | null, label: string): string | null {
|
|
760
|
+
return value === null ? null : driveId(value, label);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function pageToken(value: string | null, label: string): string | null {
|
|
764
|
+
if (value === null) return null;
|
|
765
|
+
return boundedText(value, label, GOOGLE_DRIVE_MAX_PAGE_TOKEN_CHARS);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function boundedText(value: string, label: string, maxChars: number): string {
|
|
769
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
770
|
+
const trimmed = value.trim();
|
|
771
|
+
if (!trimmed || trimmed.length > maxChars || /[\u0000-\u001f\u007f]/u.test(trimmed)) {
|
|
772
|
+
throw new Error(`${label} is invalid`);
|
|
773
|
+
}
|
|
774
|
+
return trimmed;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function nullableBoundedText(value: string | null, label: string, maxChars: number): string | null {
|
|
778
|
+
return value === null ? null : boundedText(value, label, maxChars);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function nullableHttpsUrl(value: string | null, label: string): string | null {
|
|
782
|
+
if (value === null) return null;
|
|
783
|
+
const bounded = boundedText(value, label, 4096);
|
|
784
|
+
const parsed = new URL(bounded);
|
|
785
|
+
if (parsed.protocol !== "https:") throw new Error(`${label} must use https`);
|
|
786
|
+
return parsed.toString();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function safePositiveInteger(value: number, label: string): number {
|
|
790
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
791
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
792
|
+
}
|
|
793
|
+
return value;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function validatedTotals(totals: GoogleDriveInventoryTotals): void {
|
|
797
|
+
for (const [key, value] of Object.entries(totals)) {
|
|
798
|
+
if (key === "knownBytes") continue;
|
|
799
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
800
|
+
throw new Error(`checkpoint.totals.${key} is invalid`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!/^\d+$/u.test(totals.knownBytes)) {
|
|
804
|
+
throw new Error("checkpoint.totals.knownBytes is invalid");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function emptyTotals(): GoogleDriveInventoryTotals {
|
|
809
|
+
return {
|
|
810
|
+
itemCount: 0,
|
|
811
|
+
folderCount: 1,
|
|
812
|
+
plannedFileCount: 0,
|
|
813
|
+
skippedItemCount: 0,
|
|
814
|
+
exportFileCount: 0,
|
|
815
|
+
downloadFileCount: 0,
|
|
816
|
+
unknownSizeFileCount: 0,
|
|
817
|
+
apiRequestCount: 0,
|
|
818
|
+
knownBytes: "0",
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function cloneTotals(value: GoogleDriveInventoryTotals): GoogleDriveInventoryTotals {
|
|
823
|
+
return { ...value };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function subtractTotals(
|
|
827
|
+
value: GoogleDriveInventoryTotals,
|
|
828
|
+
initial: GoogleDriveInventoryTotals,
|
|
829
|
+
): GoogleDriveInventoryTotals {
|
|
830
|
+
return {
|
|
831
|
+
itemCount: value.itemCount - initial.itemCount,
|
|
832
|
+
folderCount: value.folderCount - initial.folderCount,
|
|
833
|
+
plannedFileCount: value.plannedFileCount - initial.plannedFileCount,
|
|
834
|
+
skippedItemCount: value.skippedItemCount - initial.skippedItemCount,
|
|
835
|
+
exportFileCount: value.exportFileCount - initial.exportFileCount,
|
|
836
|
+
downloadFileCount: value.downloadFileCount - initial.downloadFileCount,
|
|
837
|
+
unknownSizeFileCount: value.unknownSizeFileCount - initial.unknownSizeFileCount,
|
|
838
|
+
apiRequestCount: value.apiRequestCount - initial.apiRequestCount,
|
|
839
|
+
knownBytes: (BigInt(value.knownBytes) - BigInt(initial.knownBytes)).toString(),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function cloneProviderItem(
|
|
844
|
+
value: GoogleDriveInventoryProviderItem,
|
|
845
|
+
): GoogleDriveInventoryProviderItem {
|
|
846
|
+
return { ...value, parents: [...value.parents] };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function cloneScope(scope: ScopedKnowledgeScope): ScopedKnowledgeScope {
|
|
850
|
+
return { ...scope };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function cloneCheckpoint(value: GoogleDriveInventoryCheckpoint): GoogleDriveInventoryCheckpoint {
|
|
854
|
+
return {
|
|
855
|
+
version: 2,
|
|
856
|
+
googlePermissionId: value.googlePermissionId,
|
|
857
|
+
externalTenantId: value.externalTenantId,
|
|
858
|
+
sourceId: value.sourceId,
|
|
859
|
+
sourceDriveId: value.sourceDriveId,
|
|
860
|
+
scope: cloneScope(value.scope),
|
|
861
|
+
pendingFolders: value.pendingFolders.map((frame) => ({
|
|
862
|
+
...frame,
|
|
863
|
+
bufferedItems: frame.bufferedItems.map(cloneProviderItem),
|
|
864
|
+
})),
|
|
865
|
+
seenFolderIds: [...value.seenFolderIds],
|
|
866
|
+
totals: cloneTotals(value.totals),
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function sameScope(
|
|
871
|
+
left: ScopedKnowledgeScope | null | undefined,
|
|
872
|
+
right: ScopedKnowledgeScope,
|
|
873
|
+
): boolean {
|
|
874
|
+
return (
|
|
875
|
+
!!left &&
|
|
876
|
+
typeof left === "object" &&
|
|
877
|
+
left.kind === right.kind &&
|
|
878
|
+
left.workspaceId === right.workspaceId &&
|
|
879
|
+
left.subjectId === right.subjectId
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function assertCheckpointBytes(checkpoint: GoogleDriveInventoryCheckpoint): void {
|
|
884
|
+
if (Buffer.byteLength(JSON.stringify(checkpoint), "utf8") > GOOGLE_DRIVE_MAX_CHECKPOINT_BYTES) {
|
|
885
|
+
throw new Error("Google Drive inventory checkpoint exceeds the serialized byte limit");
|
|
886
|
+
}
|
|
887
|
+
}
|