@tom2012/cc-web 1.5.11 → 1.5.15

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.
Files changed (56) hide show
  1. package/README.md +21 -1
  2. package/backend/dist/backup/config.d.ts +8 -0
  3. package/backend/dist/backup/config.d.ts.map +1 -0
  4. package/backend/dist/backup/config.js +104 -0
  5. package/backend/dist/backup/config.js.map +1 -0
  6. package/backend/dist/backup/engine.d.ts +22 -0
  7. package/backend/dist/backup/engine.d.ts.map +1 -0
  8. package/backend/dist/backup/engine.js +280 -0
  9. package/backend/dist/backup/engine.js.map +1 -0
  10. package/backend/dist/backup/providers/dropbox.d.ts +25 -0
  11. package/backend/dist/backup/providers/dropbox.d.ts.map +1 -0
  12. package/backend/dist/backup/providers/dropbox.js +319 -0
  13. package/backend/dist/backup/providers/dropbox.js.map +1 -0
  14. package/backend/dist/backup/providers/google-drive.d.ts +29 -0
  15. package/backend/dist/backup/providers/google-drive.d.ts.map +1 -0
  16. package/backend/dist/backup/providers/google-drive.js +305 -0
  17. package/backend/dist/backup/providers/google-drive.js.map +1 -0
  18. package/backend/dist/backup/providers/index.d.ts +3 -0
  19. package/backend/dist/backup/providers/index.d.ts.map +1 -0
  20. package/backend/dist/backup/providers/index.js +19 -0
  21. package/backend/dist/backup/providers/index.js.map +1 -0
  22. package/backend/dist/backup/providers/onedrive.d.ts +29 -0
  23. package/backend/dist/backup/providers/onedrive.d.ts.map +1 -0
  24. package/backend/dist/backup/providers/onedrive.js +316 -0
  25. package/backend/dist/backup/providers/onedrive.js.map +1 -0
  26. package/backend/dist/backup/scheduler.d.ts +6 -0
  27. package/backend/dist/backup/scheduler.d.ts.map +1 -0
  28. package/backend/dist/backup/scheduler.js +53 -0
  29. package/backend/dist/backup/scheduler.js.map +1 -0
  30. package/backend/dist/backup/types.d.ts +68 -0
  31. package/backend/dist/backup/types.d.ts.map +1 -0
  32. package/backend/dist/backup/types.js +4 -0
  33. package/backend/dist/backup/types.js.map +1 -0
  34. package/backend/dist/index.d.ts.map +1 -1
  35. package/backend/dist/index.js +74 -10
  36. package/backend/dist/index.js.map +1 -1
  37. package/backend/dist/routes/backup.d.ts +4 -0
  38. package/backend/dist/routes/backup.d.ts.map +1 -0
  39. package/backend/dist/routes/backup.js +175 -0
  40. package/backend/dist/routes/backup.js.map +1 -0
  41. package/backend/dist/routes/projects.d.ts.map +1 -1
  42. package/backend/dist/routes/projects.js +14 -0
  43. package/backend/dist/routes/projects.js.map +1 -1
  44. package/backend/dist/routes/sounds.d.ts +3 -0
  45. package/backend/dist/routes/sounds.d.ts.map +1 -0
  46. package/backend/dist/routes/sounds.js +285 -0
  47. package/backend/dist/routes/sounds.js.map +1 -0
  48. package/backend/package-lock.json +662 -20
  49. package/backend/package.json +8 -0
  50. package/bin/ccweb.js +33 -0
  51. package/frontend/dist/assets/index-Cr99sgtQ.js +489 -0
  52. package/frontend/dist/assets/index-D7gjxQKB.css +32 -0
  53. package/frontend/dist/index.html +2 -2
  54. package/package.json +1 -1
  55. package/frontend/dist/assets/index-CQjbS4zv.css +0 -32
  56. package/frontend/dist/assets/index-C_RCd4kd.js +0 -434
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ // backend/src/backup/providers/dropbox.ts
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.DropboxProvider = void 0;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const dropbox_1 = require("dropbox");
41
+ const TOKEN_ENDPOINT = 'https://api.dropboxapi.com/oauth2/token';
42
+ // 150 MB threshold: above this use chunked upload session
43
+ const SIMPLE_UPLOAD_MAX = 150 * 1024 * 1024;
44
+ // 8 MB chunk size for upload sessions
45
+ const CHUNK_SIZE = 8 * 1024 * 1024;
46
+ class DropboxProvider {
47
+ constructor(config) {
48
+ this.dbx = null;
49
+ this.config = config;
50
+ if (config.tokens) {
51
+ this.initClient(config.tokens.access_token);
52
+ }
53
+ }
54
+ initClient(accessToken) {
55
+ this.dbx = new dropbox_1.Dropbox({ accessToken });
56
+ }
57
+ client() {
58
+ if (!this.dbx) {
59
+ throw new Error('Dropbox client is not initialized');
60
+ }
61
+ return this.dbx;
62
+ }
63
+ /**
64
+ * Normalize a path to Dropbox format:
65
+ * - Root is empty string ''
66
+ * - All other paths start with '/' and have no trailing slash
67
+ */
68
+ normPath(remotePath) {
69
+ // Strip leading/trailing slashes then re-add leading slash if non-empty
70
+ const stripped = remotePath.replace(/^\/+|\/+$/g, '');
71
+ if (!stripped)
72
+ return '';
73
+ return '/' + stripped;
74
+ }
75
+ getAuthUrl(redirectUri) {
76
+ const params = new URLSearchParams({
77
+ client_id: this.config.clientId,
78
+ response_type: 'code',
79
+ redirect_uri: redirectUri,
80
+ token_access_type: 'offline',
81
+ });
82
+ return `https://www.dropbox.com/oauth2/authorize?${params.toString()}`;
83
+ }
84
+ async handleCallback(code, redirectUri) {
85
+ const body = new URLSearchParams({
86
+ code,
87
+ grant_type: 'authorization_code',
88
+ redirect_uri: redirectUri,
89
+ });
90
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
91
+ const res = await fetch(TOKEN_ENDPOINT, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/x-www-form-urlencoded',
95
+ Authorization: `Basic ${credentials}`,
96
+ },
97
+ body: body.toString(),
98
+ });
99
+ if (!res.ok) {
100
+ const text = await res.text();
101
+ throw new Error(`Dropbox token exchange failed: ${res.status} ${text}`);
102
+ }
103
+ const data = (await res.json());
104
+ if (!data.access_token || !data.refresh_token) {
105
+ throw new Error('Missing tokens in Dropbox OAuth2 callback response');
106
+ }
107
+ const providerTokens = {
108
+ access_token: data.access_token,
109
+ refresh_token: data.refresh_token,
110
+ expiry: new Date(Date.now() + data.expires_in * 1000).toISOString(),
111
+ };
112
+ this.config.tokens = providerTokens;
113
+ this.initClient(providerTokens.access_token);
114
+ return providerTokens;
115
+ }
116
+ async refreshToken() {
117
+ if (!this.config.tokens?.refresh_token) {
118
+ throw new Error('No refresh token available for Dropbox provider');
119
+ }
120
+ const body = new URLSearchParams({
121
+ grant_type: 'refresh_token',
122
+ refresh_token: this.config.tokens.refresh_token,
123
+ });
124
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
125
+ const res = await fetch(TOKEN_ENDPOINT, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/x-www-form-urlencoded',
129
+ Authorization: `Basic ${credentials}`,
130
+ },
131
+ body: body.toString(),
132
+ });
133
+ if (!res.ok) {
134
+ const text = await res.text();
135
+ throw new Error(`Dropbox token refresh failed: ${res.status} ${text}`);
136
+ }
137
+ const data = (await res.json());
138
+ if (!data.access_token) {
139
+ throw new Error('Failed to refresh Dropbox access token');
140
+ }
141
+ const providerTokens = {
142
+ access_token: data.access_token,
143
+ refresh_token: data.refresh_token ?? this.config.tokens.refresh_token,
144
+ expiry: new Date(Date.now() + data.expires_in * 1000).toISOString(),
145
+ };
146
+ this.config.tokens = providerTokens;
147
+ this.initClient(providerTokens.access_token);
148
+ return providerTokens;
149
+ }
150
+ isAuthorized() {
151
+ return !!(this.config.tokens?.access_token && this.config.tokens?.refresh_token);
152
+ }
153
+ async ensureAuth() {
154
+ if (!this.isAuthorized()) {
155
+ throw new Error('Dropbox provider is not authorized');
156
+ }
157
+ const tokens = this.config.tokens;
158
+ const expiryMs = new Date(tokens.expiry).getTime();
159
+ const nowMs = Date.now();
160
+ // Refresh if within 60 seconds of expiry
161
+ if (expiryMs - nowMs < 60 * 1000) {
162
+ await this.refreshToken();
163
+ }
164
+ }
165
+ async listFiles(remotePath) {
166
+ await this.ensureAuth();
167
+ const dbxPath = this.normPath(remotePath);
168
+ const results = [];
169
+ let response = await this.client().filesListFolder({ path: dbxPath });
170
+ while (true) {
171
+ for (const entry of response.result.entries) {
172
+ const isDirectory = entry['.tag'] === 'folder';
173
+ const entryPath = entry.path_display ?? entry.path_lower ?? entry.name;
174
+ // Return path relative to remotePath base (strip leading slash)
175
+ const normalizedBase = remotePath.replace(/^\/+|\/+$/g, '');
176
+ const relPath = normalizedBase
177
+ ? `${normalizedBase}/${entry.name}`
178
+ : entry.name;
179
+ results.push({
180
+ name: entry.name,
181
+ path: relPath,
182
+ isDirectory,
183
+ size: isDirectory ? 0 : (entry.size ?? 0),
184
+ modifiedTime: isDirectory
185
+ ? new Date().toISOString()
186
+ : (entry.server_modified ?? new Date().toISOString()),
187
+ });
188
+ }
189
+ if (!response.result.has_more)
190
+ break;
191
+ response = await this.client().filesListFolderContinue({
192
+ cursor: response.result.cursor,
193
+ });
194
+ }
195
+ return results;
196
+ }
197
+ async uploadFile(localPath, remotePath) {
198
+ await this.ensureAuth();
199
+ const dbxPath = this.normPath(remotePath);
200
+ const stat = fs.statSync(localPath);
201
+ const fileSize = stat.size;
202
+ if (fileSize < SIMPLE_UPLOAD_MAX) {
203
+ // Simple upload
204
+ const fileContent = fs.readFileSync(localPath);
205
+ await this.client().filesUpload({
206
+ path: dbxPath,
207
+ mode: { '.tag': 'overwrite' },
208
+ contents: fileContent,
209
+ });
210
+ }
211
+ else {
212
+ // Chunked upload session
213
+ const fd = fs.openSync(localPath, 'r');
214
+ try {
215
+ // Start upload session with first chunk
216
+ let offset = 0;
217
+ const firstChunkSize = Math.min(CHUNK_SIZE, fileSize);
218
+ const firstChunk = Buffer.alloc(firstChunkSize);
219
+ fs.readSync(fd, firstChunk, 0, firstChunkSize, 0);
220
+ offset += firstChunkSize;
221
+ const startRes = await this.client().filesUploadSessionStart({
222
+ close: false,
223
+ contents: firstChunk,
224
+ });
225
+ const sessionId = startRes.result.session_id;
226
+ // Append remaining chunks
227
+ while (offset < fileSize) {
228
+ const chunkSize = Math.min(CHUNK_SIZE, fileSize - offset);
229
+ const chunk = Buffer.alloc(chunkSize);
230
+ fs.readSync(fd, chunk, 0, chunkSize, offset);
231
+ const isLast = offset + chunkSize >= fileSize;
232
+ if (isLast) {
233
+ // Finish the session
234
+ await this.client().filesUploadSessionFinish({
235
+ cursor: { session_id: sessionId, offset },
236
+ commit: {
237
+ path: dbxPath,
238
+ mode: { '.tag': 'overwrite' },
239
+ },
240
+ contents: chunk,
241
+ });
242
+ }
243
+ else {
244
+ await this.client().filesUploadSessionAppendV2({
245
+ cursor: { session_id: sessionId, offset },
246
+ close: false,
247
+ contents: chunk,
248
+ });
249
+ }
250
+ offset += chunkSize;
251
+ }
252
+ // Edge case: file exactly one chunk — finish with empty content
253
+ if (fileSize === firstChunkSize) {
254
+ await this.client().filesUploadSessionFinish({
255
+ cursor: { session_id: sessionId, offset },
256
+ commit: {
257
+ path: dbxPath,
258
+ mode: { '.tag': 'overwrite' },
259
+ },
260
+ contents: Buffer.alloc(0),
261
+ });
262
+ }
263
+ }
264
+ finally {
265
+ fs.closeSync(fd);
266
+ }
267
+ }
268
+ }
269
+ async deleteFile(remotePath) {
270
+ await this.ensureAuth();
271
+ const dbxPath = this.normPath(remotePath);
272
+ try {
273
+ await this.client().filesDeleteV2({ path: dbxPath });
274
+ }
275
+ catch (err) {
276
+ // Ignore path_lookup errors (file not found) — treat as success
277
+ const errObj = err;
278
+ const summary = errObj?.error?.error_summary ?? '';
279
+ if (!summary.startsWith('path_lookup/')) {
280
+ throw err;
281
+ }
282
+ }
283
+ }
284
+ async mkdir(remotePath) {
285
+ await this.ensureAuth();
286
+ const dbxPath = this.normPath(remotePath);
287
+ if (!dbxPath)
288
+ return; // root always exists
289
+ try {
290
+ await this.client().filesCreateFolderV2({ path: dbxPath });
291
+ }
292
+ catch (err) {
293
+ // Ignore path/conflict errors (folder already exists)
294
+ const errObj = err;
295
+ const summary = errObj?.error?.error_summary ?? '';
296
+ if (!summary.startsWith('path/conflict')) {
297
+ throw err;
298
+ }
299
+ }
300
+ }
301
+ async downloadFile(remotePath, localPath) {
302
+ await this.ensureAuth();
303
+ const dbxPath = this.normPath(remotePath);
304
+ const res = await this.client().filesDownload({ path: dbxPath });
305
+ // The SDK returns the binary content as `fileBinary` in the result
306
+ const fileBinary = res.result.fileBinary;
307
+ if (!fileBinary) {
308
+ throw new Error(`Dropbox filesDownload returned no fileBinary for: ${remotePath}`);
309
+ }
310
+ // Ensure parent directory exists
311
+ const parentDir = path.dirname(localPath);
312
+ if (!fs.existsSync(parentDir)) {
313
+ fs.mkdirSync(parentDir, { recursive: true });
314
+ }
315
+ fs.writeFileSync(localPath, fileBinary);
316
+ }
317
+ }
318
+ exports.DropboxProvider = DropboxProvider;
319
+ //# sourceMappingURL=dropbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropbox.js","sourceRoot":"","sources":["../../../src/backup/providers/dropbox.ts"],"names":[],"mappings":";AAAA,0CAA0C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE1C,uCAAyB;AACzB,2CAA6B;AAC7B,qCAAkC;AAGlC,MAAM,cAAc,GAAG,yCAAyC,CAAC;AAEjE,0DAA0D;AAC1D,MAAM,iBAAiB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5C,sCAAsC;AACtC,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAEnC,MAAa,eAAe;IAI1B,YAAY,MAAsB;QAF1B,QAAG,GAAmB,IAAI,CAAC;QAGjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAEO,UAAU,CAAC,WAAmB;QACpC,IAAI,CAAC,GAAG,GAAG,IAAI,iBAAO,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC;IAC1C,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;IAED;;;;OAIG;IACK,QAAQ,CAAC,UAAkB;QACjC,wEAAwE;QACxE,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,CAAC;QACzB,OAAO,GAAG,GAAG,QAAQ,CAAC;IACxB,CAAC;IAED,UAAU,CAAC,WAAmB;QAC5B,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC/B,aAAa,EAAE,MAAM;YACrB,YAAY,EAAE,WAAW;YACzB,iBAAiB,EAAE,SAAS;SAC7B,CAAC,CAAC;QACH,OAAO,4CAA4C,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,IAAY,EAAE,WAAmB;QACpD,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;YAC/B,IAAI;YACJ,UAAU,EAAE,oBAAoB;YAChC,YAAY,EAAE,WAAW;SAC1B,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAC7B,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CACtD,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAErB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE;YACtC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,mCAAmC;gBACnD,aAAa,EAAE,SAAS,WAAW,EAAE;aACtC;YACD,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;SACtB,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QAC1E,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAI7B,CAAC;QAEF,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,cAAc,GAAmB;YACrC,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;SACpE,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,cAAc,CAAC;QACpC,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;YAC/B,UAAU,EAAE,eAAe;YAC3B,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa;SAChD,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAC7B,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CACtD,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAErB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE;YACtC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,mCAAmC;gBACnD,aAAa,EAAE,SAAS,WAAW,EAAE;aACtC;YACD,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;SACtB,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAI7B,CAAC;QAEF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;QAC5D,CAAC;QAED,MAAM,cAAc,GAAmB;YACrC,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa;YACrE,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;SACpE,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,cAAc,CAAC;QACpC,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,YAAY;QACV,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACnF,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxD,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAO,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,yCAAyC;QACzC,IAAI,QAAQ,GAAG,KAAK,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,UAAkB;QAChC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAExB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAiB,EAAE,CAAC;QAEjC,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAEtE,OAAO,IAAI,EAAE,CAAC;YACZ,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC5C,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC;gBAC/C,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC;gBACvE,gEAAgE;gBAChE,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;gBAC5D,MAAM,OAAO,GAAG,cAAc;oBAC5B,CAAC,CAAC,GAAG,cAAc,IAAI,KAAK,CAAC,IAAI,EAAE;oBACnC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;gBAEf,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,IAAI,EAAE,OAAO;oBACb,WAAW;oBACX,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,KAA2B,CAAC,IAAI,IAAI,CAAC,CAAC;oBAChE,YAAY,EAAE,WAAW;wBACvB,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;wBAC1B,CAAC,CAAC,CAAE,KAAsC,CAAC,eAAe,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;iBAC1F,CAAC,CAAC;YACL,CAAC;YAED,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ;gBAAE,MAAM;YAErC,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,uBAAuB,CAAC;gBACrD,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM;aAC/B,CAAC,CAAC;QACL,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,UAAkB;QACpD,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAExB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;QAE3B,IAAI,QAAQ,GAAG,iBAAiB,EAAE,CAAC;YACjC,gBAAgB;YAChB,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAC/C,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC;gBAC9B,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE;gBAC7B,QAAQ,EAAE,WAAW;aACtB,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,yBAAyB;YACzB,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YACvC,IAAI,CAAC;gBACH,wCAAwC;gBACxC,IAAI,MAAM,GAAG,CAAC,CAAC;gBACf,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;gBACtD,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;gBAChD,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;gBAClD,MAAM,IAAI,cAAc,CAAC;gBAEzB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,uBAAuB,CAAC;oBAC3D,KAAK,EAAE,KAAK;oBACZ,QAAQ,EAAE,UAAU;iBACrB,CAAC,CAAC;gBACH,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC;gBAE7C,0BAA0B;gBAC1B,OAAO,MAAM,GAAG,QAAQ,EAAE,CAAC;oBACzB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,CAAC,CAAC;oBAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACtC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;oBAE7C,MAAM,MAAM,GAAG,MAAM,GAAG,SAAS,IAAI,QAAQ,CAAC;oBAE9C,IAAI,MAAM,EAAE,CAAC;wBACX,qBAAqB;wBACrB,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,wBAAwB,CAAC;4BAC3C,MAAM,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE;4BACzC,MAAM,EAAE;gCACN,IAAI,EAAE,OAAO;gCACb,IAAI,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE;6BAC9B;4BACD,QAAQ,EAAE,KAAK;yBAChB,CAAC,CAAC;oBACL,CAAC;yBAAM,CAAC;wBACN,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,0BAA0B,CAAC;4BAC7C,MAAM,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE;4BACzC,KAAK,EAAE,KAAK;4BACZ,QAAQ,EAAE,KAAK;yBAChB,CAAC,CAAC;oBACL,CAAC;oBAED,MAAM,IAAI,SAAS,CAAC;gBACtB,CAAC;gBAED,gEAAgE;gBAChE,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;oBAChC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,wBAAwB,CAAC;wBAC3C,MAAM,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE;wBACzC,MAAM,EAAE;4BACN,IAAI,EAAE,OAAO;4BACb,IAAI,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE;yBAC9B;wBACD,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;qBAC1B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,UAAkB;QACjC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAExB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,gEAAgE;YAChE,MAAM,MAAM,GAAG,GAA6C,CAAC;YAC7D,MAAM,OAAO,GAAG,MAAM,EAAE,KAAK,EAAE,aAAa,IAAI,EAAE,CAAC;YACnD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;gBACxC,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,UAAkB;QAC5B,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAExB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,qBAAqB;QAE3C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,sDAAsD;YACtD,MAAM,MAAM,GAAG,GAA6C,CAAC;YAC7D,MAAM,OAAO,GAAG,MAAM,EAAE,KAAK,EAAE,aAAa,IAAI,EAAE,CAAC;YACnD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;gBACzC,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,UAAkB,EAAE,SAAiB;QACtD,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAExB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAEjE,mEAAmE;QACnE,MAAM,UAAU,GAAI,GAAG,CAAC,MAA4C,CAAC,UAAU,CAAC;QAChF,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qDAAqD,UAAU,EAAE,CAAC,CAAC;QACrF,CAAC;QAED,iCAAiC;QACjC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,CAAC;QAED,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAC1C,CAAC;CACF;AAvUD,0CAuUC"}
@@ -0,0 +1,29 @@
1
+ import { CloudProvider, ProviderConfig, ProviderTokens, RemoteFile } from '../types';
2
+ export declare class GoogleDriveProvider implements CloudProvider {
3
+ config: ProviderConfig;
4
+ private oauth2Client;
5
+ private folderIdCache;
6
+ constructor(config: ProviderConfig);
7
+ getAuthUrl(redirectUri: string): string;
8
+ handleCallback(code: string, redirectUri: string): Promise<ProviderTokens>;
9
+ refreshToken(): Promise<ProviderTokens>;
10
+ isAuthorized(): boolean;
11
+ ensureAuth(): Promise<void>;
12
+ private drive;
13
+ /**
14
+ * Walk path segments and return the Drive folder ID, creating any missing folders.
15
+ * Empty path or '/' returns 'root'.
16
+ */
17
+ private getOrCreateFolder;
18
+ /**
19
+ * Find the Drive file ID for a given remote path.
20
+ * Returns null if not found.
21
+ */
22
+ private findFileId;
23
+ listFiles(remotePath: string): Promise<RemoteFile[]>;
24
+ uploadFile(localPath: string, remotePath: string): Promise<void>;
25
+ downloadFile(remotePath: string, localPath: string): Promise<void>;
26
+ deleteFile(remotePath: string): Promise<void>;
27
+ mkdir(remotePath: string): Promise<void>;
28
+ }
29
+ //# sourceMappingURL=google-drive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-drive.d.ts","sourceRoot":"","sources":["../../../src/backup/providers/google-drive.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAErF,qBAAa,mBAAoB,YAAW,aAAa;IACvD,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,aAAa,CAAkC;gBAE3C,MAAM,EAAE,cAAc;IAgBlC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAajC,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA0B1E,YAAY,IAAI,OAAO,CAAC,cAAc,CAAC;IAwB7C,YAAY,IAAI,OAAO;IAIjB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAajC,OAAO,CAAC,KAAK;IAIb;;;OAGG;YACW,iBAAiB;IAmD/B;;;OAGG;YACW,UAAU;IA0BlB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAoCpD,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4ChE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBlE,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB7C,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI/C"}
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ // backend/src/backup/providers/google-drive.ts
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.GoogleDriveProvider = void 0;
38
+ const fs = __importStar(require("fs"));
39
+ const googleapis_1 = require("googleapis");
40
+ class GoogleDriveProvider {
41
+ constructor(config) {
42
+ this.folderIdCache = new Map();
43
+ this.config = config;
44
+ this.oauth2Client = new googleapis_1.google.auth.OAuth2(config.clientId, config.clientSecret);
45
+ if (config.tokens) {
46
+ this.oauth2Client.setCredentials({
47
+ access_token: config.tokens.access_token,
48
+ refresh_token: config.tokens.refresh_token,
49
+ expiry_date: new Date(config.tokens.expiry).getTime(),
50
+ });
51
+ }
52
+ }
53
+ getAuthUrl(redirectUri) {
54
+ const client = new googleapis_1.google.auth.OAuth2(this.config.clientId, this.config.clientSecret, redirectUri);
55
+ return client.generateAuthUrl({
56
+ access_type: 'offline',
57
+ prompt: 'consent',
58
+ scope: ['https://www.googleapis.com/auth/drive.file'],
59
+ });
60
+ }
61
+ async handleCallback(code, redirectUri) {
62
+ const client = new googleapis_1.google.auth.OAuth2(this.config.clientId, this.config.clientSecret, redirectUri);
63
+ const { tokens } = await client.getToken(code);
64
+ if (!tokens.access_token || !tokens.refresh_token) {
65
+ throw new Error('Missing tokens in OAuth2 callback response');
66
+ }
67
+ const providerTokens = {
68
+ access_token: tokens.access_token,
69
+ refresh_token: tokens.refresh_token,
70
+ expiry: tokens.expiry_date
71
+ ? new Date(tokens.expiry_date).toISOString()
72
+ : new Date(Date.now() + 3600 * 1000).toISOString(),
73
+ };
74
+ this.oauth2Client.setCredentials({
75
+ access_token: providerTokens.access_token,
76
+ refresh_token: providerTokens.refresh_token,
77
+ expiry_date: new Date(providerTokens.expiry).getTime(),
78
+ });
79
+ this.config.tokens = providerTokens;
80
+ return providerTokens;
81
+ }
82
+ async refreshToken() {
83
+ const { credentials } = await this.oauth2Client.refreshAccessToken();
84
+ if (!credentials.access_token) {
85
+ throw new Error('Failed to refresh access token');
86
+ }
87
+ const providerTokens = {
88
+ access_token: credentials.access_token,
89
+ refresh_token: credentials.refresh_token ??
90
+ this.config.tokens?.refresh_token ??
91
+ '',
92
+ expiry: credentials.expiry_date
93
+ ? new Date(credentials.expiry_date).toISOString()
94
+ : new Date(Date.now() + 3600 * 1000).toISOString(),
95
+ };
96
+ this.oauth2Client.setCredentials({
97
+ access_token: providerTokens.access_token,
98
+ refresh_token: providerTokens.refresh_token,
99
+ expiry_date: new Date(providerTokens.expiry).getTime(),
100
+ });
101
+ this.config.tokens = providerTokens;
102
+ return providerTokens;
103
+ }
104
+ isAuthorized() {
105
+ return !!(this.config.tokens?.access_token && this.config.tokens?.refresh_token);
106
+ }
107
+ async ensureAuth() {
108
+ if (!this.isAuthorized()) {
109
+ throw new Error('Google Drive provider is not authorized');
110
+ }
111
+ const tokens = this.config.tokens;
112
+ const expiryMs = new Date(tokens.expiry).getTime();
113
+ const nowMs = Date.now();
114
+ // Refresh if within 60 seconds of expiry
115
+ if (expiryMs - nowMs < 60 * 1000) {
116
+ await this.refreshToken();
117
+ }
118
+ }
119
+ drive() {
120
+ return googleapis_1.google.drive({ version: 'v3', auth: this.oauth2Client });
121
+ }
122
+ /**
123
+ * Walk path segments and return the Drive folder ID, creating any missing folders.
124
+ * Empty path or '/' returns 'root'.
125
+ */
126
+ async getOrCreateFolder(folderPath) {
127
+ const normalized = folderPath.replace(/^\/+|\/+$/g, '');
128
+ if (!normalized)
129
+ return 'root';
130
+ if (this.folderIdCache.has(normalized)) {
131
+ return this.folderIdCache.get(normalized);
132
+ }
133
+ const segments = normalized.split('/');
134
+ let parentId = 'root';
135
+ for (let i = 0; i < segments.length; i++) {
136
+ const partialPath = segments.slice(0, i + 1).join('/');
137
+ if (this.folderIdCache.has(partialPath)) {
138
+ parentId = this.folderIdCache.get(partialPath);
139
+ continue;
140
+ }
141
+ const segment = segments[i];
142
+ const drive = this.drive();
143
+ // Search for existing folder
144
+ const res = await drive.files.list({
145
+ q: `name = '${segment.replace(/'/g, "\\'")}' and mimeType = 'application/vnd.google-apps.folder' and '${parentId}' in parents and trashed = false`,
146
+ fields: 'files(id)',
147
+ pageSize: 1,
148
+ });
149
+ let folderId;
150
+ if (res.data.files && res.data.files.length > 0) {
151
+ folderId = res.data.files[0].id;
152
+ }
153
+ else {
154
+ // Create the folder
155
+ const created = await drive.files.create({
156
+ requestBody: {
157
+ name: segment,
158
+ mimeType: 'application/vnd.google-apps.folder',
159
+ parents: [parentId],
160
+ },
161
+ fields: 'id',
162
+ });
163
+ folderId = created.data.id;
164
+ }
165
+ this.folderIdCache.set(partialPath, folderId);
166
+ parentId = folderId;
167
+ }
168
+ return parentId;
169
+ }
170
+ /**
171
+ * Find the Drive file ID for a given remote path.
172
+ * Returns null if not found.
173
+ */
174
+ async findFileId(remotePath) {
175
+ const normalized = remotePath.replace(/^\/+/, '');
176
+ const lastSlash = normalized.lastIndexOf('/');
177
+ const parentPath = lastSlash >= 0 ? normalized.slice(0, lastSlash) : '';
178
+ const fileName = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
179
+ let parentId;
180
+ try {
181
+ parentId = await this.getOrCreateFolder(parentPath);
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ const drive = this.drive();
187
+ const res = await drive.files.list({
188
+ q: `name = '${fileName.replace(/'/g, "\\'")}' and '${parentId}' in parents and trashed = false`,
189
+ fields: 'files(id)',
190
+ pageSize: 1,
191
+ });
192
+ if (res.data.files && res.data.files.length > 0) {
193
+ return res.data.files[0].id;
194
+ }
195
+ return null;
196
+ }
197
+ async listFiles(remotePath) {
198
+ await this.ensureAuth();
199
+ const folderId = await this.getOrCreateFolder(remotePath);
200
+ const drive = this.drive();
201
+ const results = [];
202
+ let pageToken;
203
+ const normalizedBase = remotePath.replace(/^\/+|\/+$/g, '');
204
+ do {
205
+ const res = await drive.files.list({
206
+ q: `'${folderId}' in parents and trashed = false`,
207
+ fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime)',
208
+ pageSize: 1000,
209
+ ...(pageToken ? { pageToken } : {}),
210
+ });
211
+ for (const file of res.data.files ?? []) {
212
+ const isDirectory = file.mimeType === 'application/vnd.google-apps.folder';
213
+ const filePath = normalizedBase ? `${normalizedBase}/${file.name}` : file.name;
214
+ results.push({
215
+ name: file.name,
216
+ path: filePath,
217
+ isDirectory,
218
+ size: isDirectory ? 0 : parseInt(file.size ?? '0', 10),
219
+ modifiedTime: file.modifiedTime ?? new Date().toISOString(),
220
+ });
221
+ }
222
+ pageToken = res.data.nextPageToken ?? undefined;
223
+ } while (pageToken);
224
+ return results;
225
+ }
226
+ async uploadFile(localPath, remotePath) {
227
+ await this.ensureAuth();
228
+ const normalized = remotePath.replace(/^\/+/, '');
229
+ const lastSlash = normalized.lastIndexOf('/');
230
+ const parentPath = lastSlash >= 0 ? normalized.slice(0, lastSlash) : '';
231
+ const fileName = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
232
+ const parentId = await this.getOrCreateFolder(parentPath);
233
+ const drive = this.drive();
234
+ const fileStream = fs.createReadStream(localPath);
235
+ const stat = fs.statSync(localPath);
236
+ // Check if file already exists
237
+ const existingId = await this.findFileId(remotePath);
238
+ if (existingId) {
239
+ // Update existing file
240
+ await drive.files.update({
241
+ fileId: existingId,
242
+ requestBody: {},
243
+ media: {
244
+ body: fileStream,
245
+ },
246
+ fields: 'id',
247
+ });
248
+ }
249
+ else {
250
+ // Create new file
251
+ await drive.files.create({
252
+ requestBody: {
253
+ name: fileName,
254
+ parents: [parentId],
255
+ },
256
+ media: {
257
+ body: fileStream,
258
+ },
259
+ fields: 'id',
260
+ });
261
+ }
262
+ // Suppress unused variable warning
263
+ void stat;
264
+ }
265
+ async downloadFile(remotePath, localPath) {
266
+ await this.ensureAuth();
267
+ const fileId = await this.findFileId(remotePath);
268
+ if (!fileId) {
269
+ throw new Error(`File not found on Google Drive: ${remotePath}`);
270
+ }
271
+ const drive = this.drive();
272
+ const res = await drive.files.get({ fileId, alt: 'media' }, { responseType: 'stream' });
273
+ await new Promise((resolve, reject) => {
274
+ const dest = fs.createWriteStream(localPath);
275
+ res.data
276
+ .on('error', reject)
277
+ .pipe(dest)
278
+ .on('error', reject)
279
+ .on('finish', resolve);
280
+ });
281
+ }
282
+ async deleteFile(remotePath) {
283
+ await this.ensureAuth();
284
+ const fileId = await this.findFileId(remotePath);
285
+ if (!fileId) {
286
+ // Already gone — treat as success
287
+ return;
288
+ }
289
+ const drive = this.drive();
290
+ await drive.files.delete({ fileId });
291
+ // Invalidate cache entries that include this path
292
+ const normalized = remotePath.replace(/^\/+|\/+$/g, '');
293
+ for (const key of this.folderIdCache.keys()) {
294
+ if (key === normalized || key.startsWith(normalized + '/')) {
295
+ this.folderIdCache.delete(key);
296
+ }
297
+ }
298
+ }
299
+ async mkdir(remotePath) {
300
+ await this.ensureAuth();
301
+ await this.getOrCreateFolder(remotePath);
302
+ }
303
+ }
304
+ exports.GoogleDriveProvider = GoogleDriveProvider;
305
+ //# sourceMappingURL=google-drive.js.map