sqlite-cloud-backup 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/dist/index.cjs +832 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +795 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
SqliteCloudBackup: () => SqliteCloudBackup,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/core/db-manager.ts
|
|
39
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
40
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
41
|
+
var import_path = __toESM(require("path"), 1);
|
|
42
|
+
|
|
43
|
+
// src/utils/checksum.ts
|
|
44
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
45
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
46
|
+
var ChecksumUtil = class {
|
|
47
|
+
/**
|
|
48
|
+
* Calculate SHA-256 checksum of a file
|
|
49
|
+
*/
|
|
50
|
+
static async calculateFileChecksum(filePath) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const hash = import_crypto.default.createHash("sha256");
|
|
53
|
+
const stream = import_fs.default.createReadStream(filePath);
|
|
54
|
+
stream.on("data", (data) => hash.update(data));
|
|
55
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
56
|
+
stream.on("error", reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Calculate SHA-256 checksum of a buffer
|
|
61
|
+
*/
|
|
62
|
+
static calculateBufferChecksum(buffer) {
|
|
63
|
+
return import_crypto.default.createHash("sha256").update(buffer).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Verify file integrity
|
|
67
|
+
*/
|
|
68
|
+
static async verifyChecksum(filePath, expectedChecksum) {
|
|
69
|
+
const actualChecksum = await this.calculateFileChecksum(filePath);
|
|
70
|
+
return actualChecksum === expectedChecksum;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/utils/file-operations.ts
|
|
75
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
76
|
+
var FileOperations = class {
|
|
77
|
+
/**
|
|
78
|
+
* Ensure directory exists, create if not
|
|
79
|
+
*/
|
|
80
|
+
static ensureDir(dirPath) {
|
|
81
|
+
if (!import_fs2.default.existsSync(dirPath)) {
|
|
82
|
+
import_fs2.default.mkdirSync(dirPath, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Copy file atomically
|
|
87
|
+
*/
|
|
88
|
+
static async copyFile(source, destination) {
|
|
89
|
+
const tempDest = `${destination}.tmp`;
|
|
90
|
+
await import_fs2.default.promises.copyFile(source, tempDest);
|
|
91
|
+
await import_fs2.default.promises.rename(tempDest, destination);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/core/db-manager.ts
|
|
96
|
+
var DatabaseManager = class {
|
|
97
|
+
db = null;
|
|
98
|
+
dbPath;
|
|
99
|
+
metadataPath;
|
|
100
|
+
logger;
|
|
101
|
+
constructor(dbPath, logger) {
|
|
102
|
+
this.dbPath = dbPath;
|
|
103
|
+
this.logger = logger;
|
|
104
|
+
const dbDir = import_path.default.dirname(dbPath);
|
|
105
|
+
const dbName = import_path.default.basename(dbPath, import_path.default.extname(dbPath));
|
|
106
|
+
this.metadataPath = import_path.default.join(dbDir, ".sqlite-cloud-backup", dbName, "metadata.json");
|
|
107
|
+
FileOperations.ensureDir(import_path.default.dirname(this.metadataPath));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Open database connection
|
|
111
|
+
*/
|
|
112
|
+
open() {
|
|
113
|
+
if (!import_fs3.default.existsSync(this.dbPath)) {
|
|
114
|
+
throw new Error(`Database not found: ${this.dbPath}`);
|
|
115
|
+
}
|
|
116
|
+
this.db = new import_better_sqlite3.default(this.dbPath, { readonly: false });
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Close database connection
|
|
120
|
+
*/
|
|
121
|
+
close() {
|
|
122
|
+
if (this.db) {
|
|
123
|
+
this.db.close();
|
|
124
|
+
this.db = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get database file as buffer
|
|
129
|
+
*/
|
|
130
|
+
async getBuffer() {
|
|
131
|
+
this.close();
|
|
132
|
+
return import_fs3.default.promises.readFile(this.dbPath);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Calculate current database checksum
|
|
136
|
+
*/
|
|
137
|
+
async getChecksum() {
|
|
138
|
+
this.close();
|
|
139
|
+
return ChecksumUtil.calculateFileChecksum(this.dbPath);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Replace database with buffer
|
|
143
|
+
*/
|
|
144
|
+
async replaceWithBuffer(buffer) {
|
|
145
|
+
this.close();
|
|
146
|
+
const tempPath = `${this.dbPath}.tmp`;
|
|
147
|
+
await import_fs3.default.promises.writeFile(tempPath, buffer);
|
|
148
|
+
await import_fs3.default.promises.rename(tempPath, this.dbPath);
|
|
149
|
+
this.logger.info("Database replaced from cloud");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get local metadata
|
|
153
|
+
*/
|
|
154
|
+
async getLocalMetadata() {
|
|
155
|
+
if (!import_fs3.default.existsSync(this.metadataPath)) {
|
|
156
|
+
return {
|
|
157
|
+
lastSyncTimestamp: 0,
|
|
158
|
+
lastSyncChecksum: ""
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const content = await import_fs3.default.promises.readFile(this.metadataPath, "utf-8");
|
|
162
|
+
return JSON.parse(content);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Update local metadata
|
|
166
|
+
*/
|
|
167
|
+
async updateLocalMetadata(metadata) {
|
|
168
|
+
const current = await this.getLocalMetadata();
|
|
169
|
+
const updated = { ...current, ...metadata };
|
|
170
|
+
await import_fs3.default.promises.writeFile(
|
|
171
|
+
this.metadataPath,
|
|
172
|
+
JSON.stringify(updated, null, 2)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get modification time of database file
|
|
177
|
+
*/
|
|
178
|
+
async getModifiedTime() {
|
|
179
|
+
const stats = await import_fs3.default.promises.stat(this.dbPath);
|
|
180
|
+
return stats.mtimeMs;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/core/sync-engine.ts
|
|
185
|
+
var SyncEngine = class {
|
|
186
|
+
dbManager;
|
|
187
|
+
provider;
|
|
188
|
+
logger;
|
|
189
|
+
constructor(dbManager, provider, logger) {
|
|
190
|
+
this.dbManager = dbManager;
|
|
191
|
+
this.provider = provider;
|
|
192
|
+
this.logger = logger;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Push local database to cloud
|
|
196
|
+
*/
|
|
197
|
+
async pushToCloud() {
|
|
198
|
+
const startTime = Date.now();
|
|
199
|
+
try {
|
|
200
|
+
const buffer = await this.dbManager.getBuffer();
|
|
201
|
+
const originalSize = buffer.length;
|
|
202
|
+
const checksum = ChecksumUtil.calculateBufferChecksum(buffer);
|
|
203
|
+
await this.provider.uploadFile("current.db", buffer);
|
|
204
|
+
const metadata = {
|
|
205
|
+
dbName: "current",
|
|
206
|
+
lastSyncTimestamp: Date.now(),
|
|
207
|
+
lastSyncType: "push",
|
|
208
|
+
checksum,
|
|
209
|
+
version: 1
|
|
210
|
+
};
|
|
211
|
+
await this.provider.updateMetadata(metadata);
|
|
212
|
+
await this.dbManager.updateLocalMetadata({
|
|
213
|
+
lastSyncTimestamp: metadata.lastSyncTimestamp,
|
|
214
|
+
lastSyncChecksum: checksum
|
|
215
|
+
});
|
|
216
|
+
const result = {
|
|
217
|
+
success: true,
|
|
218
|
+
type: "push",
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
localChecksum: checksum,
|
|
221
|
+
cloudChecksum: checksum,
|
|
222
|
+
bytesTransferred: buffer.length,
|
|
223
|
+
duration: Date.now() - startTime
|
|
224
|
+
};
|
|
225
|
+
this.logger.info(`Push successful: ${originalSize} bytes`);
|
|
226
|
+
return result;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.logger.error("Push failed", error);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pull database from cloud to local
|
|
234
|
+
*/
|
|
235
|
+
async pullFromCloud() {
|
|
236
|
+
const startTime = Date.now();
|
|
237
|
+
try {
|
|
238
|
+
const exists = await this.provider.fileExists("current.db");
|
|
239
|
+
if (!exists) {
|
|
240
|
+
throw new Error("No cloud version found");
|
|
241
|
+
}
|
|
242
|
+
const buffer = await this.provider.downloadFile("current.db");
|
|
243
|
+
const checksum = ChecksumUtil.calculateBufferChecksum(buffer);
|
|
244
|
+
const cloudMetadata = await this.provider.getMetadata("current.db");
|
|
245
|
+
if (cloudMetadata && cloudMetadata.checksum !== checksum) {
|
|
246
|
+
throw new Error("Checksum mismatch - data corruption detected");
|
|
247
|
+
}
|
|
248
|
+
await this.dbManager.replaceWithBuffer(buffer);
|
|
249
|
+
await this.dbManager.updateLocalMetadata({
|
|
250
|
+
lastSyncTimestamp: Date.now(),
|
|
251
|
+
lastSyncChecksum: checksum
|
|
252
|
+
});
|
|
253
|
+
const result = {
|
|
254
|
+
success: true,
|
|
255
|
+
type: "pull",
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
localChecksum: checksum,
|
|
258
|
+
cloudChecksum: checksum,
|
|
259
|
+
bytesTransferred: buffer.length,
|
|
260
|
+
duration: Date.now() - startTime
|
|
261
|
+
};
|
|
262
|
+
this.logger.info(`Pull successful: ${buffer.length} bytes`);
|
|
263
|
+
return result;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.logger.error("Pull failed", error);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Bidirectional sync - simple version for v0.1
|
|
271
|
+
*/
|
|
272
|
+
async sync() {
|
|
273
|
+
const startTime = Date.now();
|
|
274
|
+
try {
|
|
275
|
+
const cloudExists = await this.provider.fileExists("current.db");
|
|
276
|
+
if (!cloudExists) {
|
|
277
|
+
this.logger.info("No cloud version found, pushing local database");
|
|
278
|
+
return await this.pushToCloud();
|
|
279
|
+
}
|
|
280
|
+
const localChecksum = await this.dbManager.getChecksum();
|
|
281
|
+
const cloudMetadata = await this.provider.getMetadata("current.db");
|
|
282
|
+
if (!cloudMetadata) {
|
|
283
|
+
this.logger.info("No cloud metadata, pushing local database");
|
|
284
|
+
return await this.pushToCloud();
|
|
285
|
+
}
|
|
286
|
+
if (localChecksum === cloudMetadata.checksum) {
|
|
287
|
+
const result = {
|
|
288
|
+
success: true,
|
|
289
|
+
type: "bidirectional",
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
localChecksum,
|
|
292
|
+
cloudChecksum: cloudMetadata.checksum,
|
|
293
|
+
bytesTransferred: 0,
|
|
294
|
+
duration: Date.now() - startTime
|
|
295
|
+
};
|
|
296
|
+
this.logger.info("Already in sync");
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
const localModified = await this.dbManager.getModifiedTime();
|
|
300
|
+
if (localModified > cloudMetadata.modifiedAt) {
|
|
301
|
+
this.logger.info("Local is newer, pushing");
|
|
302
|
+
return await this.pushToCloud();
|
|
303
|
+
} else {
|
|
304
|
+
this.logger.info("Cloud is newer, pulling");
|
|
305
|
+
return await this.pullFromCloud();
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.logger.error("Sync failed", error);
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/providers/google-drive/google-drive-provider.ts
|
|
315
|
+
var import_googleapis = require("googleapis");
|
|
316
|
+
|
|
317
|
+
// src/providers/base-provider.ts
|
|
318
|
+
var BaseProvider = class {
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/providers/google-drive/google-drive-provider.ts
|
|
322
|
+
var import_stream = require("stream");
|
|
323
|
+
var GoogleDriveProvider = class extends BaseProvider {
|
|
324
|
+
drive;
|
|
325
|
+
oauth2Client;
|
|
326
|
+
rootFolderId = null;
|
|
327
|
+
logger;
|
|
328
|
+
dbName;
|
|
329
|
+
constructor(credentials, dbName, logger) {
|
|
330
|
+
super();
|
|
331
|
+
this.logger = logger;
|
|
332
|
+
this.dbName = dbName;
|
|
333
|
+
this.oauth2Client = new import_googleapis.google.auth.OAuth2(
|
|
334
|
+
credentials.clientId,
|
|
335
|
+
credentials.clientSecret,
|
|
336
|
+
credentials.redirectUri
|
|
337
|
+
);
|
|
338
|
+
this.oauth2Client.setCredentials({
|
|
339
|
+
refresh_token: credentials.refreshToken
|
|
340
|
+
});
|
|
341
|
+
this.drive = import_googleapis.google.drive({ version: "v3", auth: this.oauth2Client });
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Initialize folder structure
|
|
345
|
+
*/
|
|
346
|
+
async ensureRootFolder() {
|
|
347
|
+
if (this.rootFolderId) return this.rootFolderId;
|
|
348
|
+
const response = await this.drive.files.list({
|
|
349
|
+
q: "name='.sqlite-cloud-backup' and mimeType='application/vnd.google-apps.folder' and trashed=false",
|
|
350
|
+
fields: "files(id, name)",
|
|
351
|
+
spaces: "drive"
|
|
352
|
+
});
|
|
353
|
+
if (response.data.files && response.data.files.length > 0) {
|
|
354
|
+
this.rootFolderId = response.data.files[0].id;
|
|
355
|
+
} else {
|
|
356
|
+
const folder = await this.drive.files.create({
|
|
357
|
+
requestBody: {
|
|
358
|
+
name: ".sqlite-cloud-backup",
|
|
359
|
+
mimeType: "application/vnd.google-apps.folder"
|
|
360
|
+
},
|
|
361
|
+
fields: "id"
|
|
362
|
+
});
|
|
363
|
+
this.rootFolderId = folder.data.id;
|
|
364
|
+
}
|
|
365
|
+
const dbFolderId = await this.ensureDbFolder();
|
|
366
|
+
return dbFolderId;
|
|
367
|
+
}
|
|
368
|
+
async ensureDbFolder() {
|
|
369
|
+
const response = await this.drive.files.list({
|
|
370
|
+
q: `name='${this.dbName}' and mimeType='application/vnd.google-apps.folder' and '${this.rootFolderId}' in parents and trashed=false`,
|
|
371
|
+
fields: "files(id, name)"
|
|
372
|
+
});
|
|
373
|
+
if (response.data.files && response.data.files.length > 0) {
|
|
374
|
+
return response.data.files[0].id;
|
|
375
|
+
}
|
|
376
|
+
const folder = await this.drive.files.create({
|
|
377
|
+
requestBody: {
|
|
378
|
+
name: this.dbName,
|
|
379
|
+
mimeType: "application/vnd.google-apps.folder",
|
|
380
|
+
parents: [this.rootFolderId]
|
|
381
|
+
},
|
|
382
|
+
fields: "id"
|
|
383
|
+
});
|
|
384
|
+
return folder.data.id;
|
|
385
|
+
}
|
|
386
|
+
async uploadFile(fileName, buffer) {
|
|
387
|
+
const folderId = await this.ensureRootFolder();
|
|
388
|
+
const existing = await this.findFile(fileName);
|
|
389
|
+
const media = {
|
|
390
|
+
mimeType: "application/x-sqlite3",
|
|
391
|
+
body: import_stream.Readable.from(buffer)
|
|
392
|
+
};
|
|
393
|
+
if (existing) {
|
|
394
|
+
await this.drive.files.update({
|
|
395
|
+
fileId: existing.id,
|
|
396
|
+
media,
|
|
397
|
+
fields: "id"
|
|
398
|
+
});
|
|
399
|
+
this.logger.info(`Updated file in Google Drive: ${fileName}`);
|
|
400
|
+
} else {
|
|
401
|
+
await this.drive.files.create({
|
|
402
|
+
requestBody: {
|
|
403
|
+
name: fileName,
|
|
404
|
+
parents: [folderId]
|
|
405
|
+
},
|
|
406
|
+
media,
|
|
407
|
+
fields: "id"
|
|
408
|
+
});
|
|
409
|
+
this.logger.info(`Uploaded file to Google Drive: ${fileName}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async downloadFile(fileName) {
|
|
413
|
+
const file = await this.findFile(fileName);
|
|
414
|
+
if (!file) {
|
|
415
|
+
throw new Error(`File not found: ${fileName}`);
|
|
416
|
+
}
|
|
417
|
+
const response = await this.drive.files.get(
|
|
418
|
+
{ fileId: file.id, alt: "media" },
|
|
419
|
+
{ responseType: "arraybuffer" }
|
|
420
|
+
);
|
|
421
|
+
this.logger.info(`Downloaded file from Google Drive: ${fileName}`);
|
|
422
|
+
return Buffer.from(response.data);
|
|
423
|
+
}
|
|
424
|
+
async fileExists(fileName) {
|
|
425
|
+
const file = await this.findFile(fileName);
|
|
426
|
+
return file !== null;
|
|
427
|
+
}
|
|
428
|
+
async getMetadata(_fileName) {
|
|
429
|
+
const metadataFile = await this.findFile("metadata.json");
|
|
430
|
+
if (!metadataFile) return null;
|
|
431
|
+
const buffer = await this.downloadFile("metadata.json");
|
|
432
|
+
const metadata = JSON.parse(buffer.toString("utf-8"));
|
|
433
|
+
return {
|
|
434
|
+
checksum: metadata.checksum,
|
|
435
|
+
modifiedAt: metadata.lastSyncTimestamp,
|
|
436
|
+
size: 0
|
|
437
|
+
// Not tracked in metadata
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async updateMetadata(metadata) {
|
|
441
|
+
const buffer = Buffer.from(JSON.stringify(metadata, null, 2));
|
|
442
|
+
await this.uploadFile("metadata.json", buffer);
|
|
443
|
+
}
|
|
444
|
+
async deleteFile(fileName) {
|
|
445
|
+
const file = await this.findFile(fileName);
|
|
446
|
+
if (file) {
|
|
447
|
+
await this.drive.files.delete({ fileId: file.id });
|
|
448
|
+
this.logger.info(`Deleted file from Google Drive: ${fileName}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async findFile(fileName) {
|
|
452
|
+
const folderId = await this.ensureRootFolder();
|
|
453
|
+
const response = await this.drive.files.list({
|
|
454
|
+
q: `name='${fileName}' and '${folderId}' in parents and trashed=false`,
|
|
455
|
+
fields: "files(id, name, modifiedTime)"
|
|
456
|
+
});
|
|
457
|
+
return response.data.files?.[0] || null;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/providers/google-drive/oauth-flow.ts
|
|
462
|
+
var import_http = __toESM(require("http"), 1);
|
|
463
|
+
var import_url = require("url");
|
|
464
|
+
var import_open = __toESM(require("open"), 1);
|
|
465
|
+
var OAuthFlow = class {
|
|
466
|
+
logger;
|
|
467
|
+
server = null;
|
|
468
|
+
redirectUri = "http://localhost:3000/oauth/callback";
|
|
469
|
+
scopes = ["https://www.googleapis.com/auth/drive.file"];
|
|
470
|
+
constructor(logger) {
|
|
471
|
+
this.logger = logger;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Start OAuth flow - opens browser and waits for callback
|
|
475
|
+
*/
|
|
476
|
+
async authenticate(clientId, clientSecret) {
|
|
477
|
+
this.logger.info("Starting OAuth flow...");
|
|
478
|
+
const authCode = await this.startLocalServerAndWaitForCode(clientId);
|
|
479
|
+
const tokens = await this.exchangeCodeForTokens(authCode, clientId, clientSecret);
|
|
480
|
+
this.logger.info("OAuth authentication successful");
|
|
481
|
+
return tokens;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Start local HTTP server and wait for OAuth callback
|
|
485
|
+
*/
|
|
486
|
+
async startLocalServerAndWaitForCode(clientId) {
|
|
487
|
+
return new Promise((resolve, reject) => {
|
|
488
|
+
this.server = import_http.default.createServer((req, res) => {
|
|
489
|
+
const url = new import_url.URL(req.url || "", `http://localhost:3000`);
|
|
490
|
+
if (url.pathname === "/oauth/callback") {
|
|
491
|
+
const code = url.searchParams.get("code");
|
|
492
|
+
const error = url.searchParams.get("error");
|
|
493
|
+
if (error) {
|
|
494
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
495
|
+
res.end("<h1>Authentication Failed</h1><p>You can close this window.</p>");
|
|
496
|
+
this.stopServer();
|
|
497
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (code) {
|
|
501
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
502
|
+
res.end("<h1>Authentication Successful!</h1><p>You can close this window and return to the app.</p>");
|
|
503
|
+
this.stopServer();
|
|
504
|
+
resolve(code);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
508
|
+
res.end("<h1>Invalid Request</h1><p>No authorization code received.</p>");
|
|
509
|
+
this.stopServer();
|
|
510
|
+
reject(new Error("No authorization code received"));
|
|
511
|
+
} else {
|
|
512
|
+
res.writeHead(404);
|
|
513
|
+
res.end();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
this.server.listen(3e3, () => {
|
|
517
|
+
this.logger.info("Local OAuth server started on http://localhost:3000");
|
|
518
|
+
const authUrl = this.buildAuthUrl(clientId);
|
|
519
|
+
this.logger.info("Opening browser for authentication...");
|
|
520
|
+
(0, import_open.default)(authUrl).catch((err) => {
|
|
521
|
+
this.logger.error("Failed to open browser", err);
|
|
522
|
+
this.logger.info(`Please open this URL manually: ${authUrl}`);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
setTimeout(() => {
|
|
526
|
+
this.stopServer();
|
|
527
|
+
reject(new Error("OAuth flow timed out after 5 minutes"));
|
|
528
|
+
}, 5 * 60 * 1e3);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Build Google OAuth authorization URL
|
|
533
|
+
*/
|
|
534
|
+
buildAuthUrl(clientId) {
|
|
535
|
+
const params = new URLSearchParams({
|
|
536
|
+
client_id: clientId,
|
|
537
|
+
redirect_uri: this.redirectUri,
|
|
538
|
+
response_type: "code",
|
|
539
|
+
scope: this.scopes.join(" "),
|
|
540
|
+
access_type: "offline",
|
|
541
|
+
prompt: "consent"
|
|
542
|
+
// Force to get refresh token
|
|
543
|
+
});
|
|
544
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Exchange authorization code for tokens
|
|
548
|
+
*/
|
|
549
|
+
async exchangeCodeForTokens(code, clientId, clientSecret) {
|
|
550
|
+
this.logger.info("Exchanging authorization code for tokens...");
|
|
551
|
+
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
555
|
+
},
|
|
556
|
+
body: new URLSearchParams({
|
|
557
|
+
code,
|
|
558
|
+
client_id: clientId,
|
|
559
|
+
client_secret: clientSecret,
|
|
560
|
+
redirect_uri: this.redirectUri,
|
|
561
|
+
grant_type: "authorization_code"
|
|
562
|
+
}).toString()
|
|
563
|
+
});
|
|
564
|
+
if (!response.ok) {
|
|
565
|
+
const error = await response.text();
|
|
566
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
567
|
+
}
|
|
568
|
+
const data = await response.json();
|
|
569
|
+
return {
|
|
570
|
+
access_token: data.access_token,
|
|
571
|
+
refresh_token: data.refresh_token,
|
|
572
|
+
expiry_date: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Stop the local server
|
|
577
|
+
*/
|
|
578
|
+
stopServer() {
|
|
579
|
+
if (this.server) {
|
|
580
|
+
this.server.close();
|
|
581
|
+
this.server = null;
|
|
582
|
+
this.logger.info("Local OAuth server stopped");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/providers/google-drive/token-storage.ts
|
|
588
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
589
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
590
|
+
|
|
591
|
+
// src/utils/logger.ts
|
|
592
|
+
var Logger = class {
|
|
593
|
+
level;
|
|
594
|
+
constructor(level = "info") {
|
|
595
|
+
this.level = level;
|
|
596
|
+
}
|
|
597
|
+
shouldLog(level) {
|
|
598
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
599
|
+
return levels.indexOf(level) >= levels.indexOf(this.level);
|
|
600
|
+
}
|
|
601
|
+
debug(message, meta) {
|
|
602
|
+
if (this.shouldLog("debug")) {
|
|
603
|
+
console.debug(`[DEBUG] ${message}`, meta || "");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
info(message, meta) {
|
|
607
|
+
if (this.shouldLog("info")) {
|
|
608
|
+
console.info(`[INFO] ${message}`, meta || "");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
warn(message, meta) {
|
|
612
|
+
if (this.shouldLog("warn")) {
|
|
613
|
+
console.warn(`[WARN] ${message}`, meta || "");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
error(message, error) {
|
|
617
|
+
if (this.shouldLog("error")) {
|
|
618
|
+
console.error(`[ERROR] ${message}`, error || "");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/providers/google-drive/token-storage.ts
|
|
624
|
+
var TokenStorage = class {
|
|
625
|
+
logger;
|
|
626
|
+
tokenDir;
|
|
627
|
+
tokenFile;
|
|
628
|
+
constructor(dbPath, logLevel = "info") {
|
|
629
|
+
this.logger = new Logger(logLevel);
|
|
630
|
+
const dbDir = import_path2.default.dirname(dbPath);
|
|
631
|
+
this.tokenDir = import_path2.default.join(dbDir, ".sqlite-cloud-backup");
|
|
632
|
+
this.tokenFile = import_path2.default.join(this.tokenDir, "tokens.json");
|
|
633
|
+
}
|
|
634
|
+
async hasTokens() {
|
|
635
|
+
try {
|
|
636
|
+
await import_promises.default.access(this.tokenFile);
|
|
637
|
+
return true;
|
|
638
|
+
} catch {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async getTokens() {
|
|
643
|
+
try {
|
|
644
|
+
const data = await import_promises.default.readFile(this.tokenFile, "utf-8");
|
|
645
|
+
const tokens = JSON.parse(data);
|
|
646
|
+
this.logger.debug("Retrieved stored tokens");
|
|
647
|
+
return tokens;
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (error.code !== "ENOENT") {
|
|
650
|
+
this.logger.warn("Failed to read tokens", error);
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async saveTokens(tokens) {
|
|
656
|
+
try {
|
|
657
|
+
await import_promises.default.mkdir(this.tokenDir, { recursive: true });
|
|
658
|
+
await import_promises.default.writeFile(
|
|
659
|
+
this.tokenFile,
|
|
660
|
+
JSON.stringify(tokens, null, 2),
|
|
661
|
+
"utf-8"
|
|
662
|
+
);
|
|
663
|
+
this.logger.debug("Saved tokens to storage");
|
|
664
|
+
} catch (error) {
|
|
665
|
+
this.logger.error("Failed to save tokens", error);
|
|
666
|
+
throw new Error("Failed to save authentication tokens");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async clearTokens() {
|
|
670
|
+
try {
|
|
671
|
+
await import_promises.default.unlink(this.tokenFile);
|
|
672
|
+
this.logger.debug("Cleared stored tokens");
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error.code !== "ENOENT") {
|
|
675
|
+
this.logger.warn("Failed to clear tokens", error);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/index.ts
|
|
682
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
683
|
+
var SqliteCloudBackup = class {
|
|
684
|
+
dbManager;
|
|
685
|
+
provider;
|
|
686
|
+
syncEngine;
|
|
687
|
+
logger;
|
|
688
|
+
oauthFlow;
|
|
689
|
+
tokenStorage;
|
|
690
|
+
credentials;
|
|
691
|
+
dbPath;
|
|
692
|
+
constructor(config) {
|
|
693
|
+
this.logger = new Logger(config.options?.logLevel ?? "info");
|
|
694
|
+
this.dbPath = config.dbPath;
|
|
695
|
+
this.credentials = config.credentials;
|
|
696
|
+
this.oauthFlow = new OAuthFlow(this.logger);
|
|
697
|
+
this.tokenStorage = new TokenStorage(config.dbPath, config.options?.logLevel ?? "info");
|
|
698
|
+
this.dbManager = new DatabaseManager(config.dbPath, this.logger);
|
|
699
|
+
this.provider = this.createProvider(config);
|
|
700
|
+
this.syncEngine = new SyncEngine(
|
|
701
|
+
this.dbManager,
|
|
702
|
+
this.provider,
|
|
703
|
+
this.logger
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
createProvider(config) {
|
|
707
|
+
const dbName = import_path3.default.basename(config.dbPath, import_path3.default.extname(config.dbPath));
|
|
708
|
+
switch (config.provider) {
|
|
709
|
+
case "google-drive":
|
|
710
|
+
return new GoogleDriveProvider(
|
|
711
|
+
config.credentials,
|
|
712
|
+
dbName,
|
|
713
|
+
this.logger
|
|
714
|
+
);
|
|
715
|
+
default:
|
|
716
|
+
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Check if user needs authentication
|
|
721
|
+
*/
|
|
722
|
+
async needsAuthentication() {
|
|
723
|
+
if (this.credentials.refreshToken) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
return !await this.tokenStorage.hasTokens();
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Ensure user is authenticated, trigger OAuth flow if needed
|
|
730
|
+
*/
|
|
731
|
+
async ensureAuthenticated() {
|
|
732
|
+
if (this.credentials.refreshToken) {
|
|
733
|
+
this.logger.debug("Using provided refresh token");
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const storedTokens = await this.tokenStorage.getTokens();
|
|
737
|
+
if (storedTokens) {
|
|
738
|
+
this.logger.debug("Using stored refresh token");
|
|
739
|
+
this.credentials.refreshToken = storedTokens.refreshToken;
|
|
740
|
+
const dbName = import_path3.default.basename(this.dbPath, import_path3.default.extname(this.dbPath));
|
|
741
|
+
this.provider = new GoogleDriveProvider(
|
|
742
|
+
this.credentials,
|
|
743
|
+
dbName,
|
|
744
|
+
this.logger
|
|
745
|
+
);
|
|
746
|
+
this.syncEngine = new SyncEngine(
|
|
747
|
+
this.dbManager,
|
|
748
|
+
this.provider,
|
|
749
|
+
this.logger
|
|
750
|
+
);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
this.logger.info("No authentication found, starting OAuth flow...");
|
|
754
|
+
await this.authenticate();
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Trigger OAuth authentication flow
|
|
758
|
+
*/
|
|
759
|
+
async authenticate() {
|
|
760
|
+
this.logger.info("Starting OAuth authentication flow...");
|
|
761
|
+
const tokens = await this.oauthFlow.authenticate(
|
|
762
|
+
this.credentials.clientId,
|
|
763
|
+
this.credentials.clientSecret
|
|
764
|
+
);
|
|
765
|
+
await this.tokenStorage.saveTokens({
|
|
766
|
+
refreshToken: tokens.refresh_token,
|
|
767
|
+
accessToken: tokens.access_token,
|
|
768
|
+
expiryDate: tokens.expiry_date
|
|
769
|
+
});
|
|
770
|
+
this.credentials.refreshToken = tokens.refresh_token;
|
|
771
|
+
const dbName = import_path3.default.basename(this.dbPath, import_path3.default.extname(this.dbPath));
|
|
772
|
+
this.provider = new GoogleDriveProvider(
|
|
773
|
+
this.credentials,
|
|
774
|
+
dbName,
|
|
775
|
+
this.logger
|
|
776
|
+
);
|
|
777
|
+
this.syncEngine = new SyncEngine(
|
|
778
|
+
this.dbManager,
|
|
779
|
+
this.provider,
|
|
780
|
+
this.logger
|
|
781
|
+
);
|
|
782
|
+
this.logger.info("Authentication successful");
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Check if user is authenticated
|
|
786
|
+
*/
|
|
787
|
+
async isAuthenticated() {
|
|
788
|
+
return !await this.needsAuthentication();
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Logout and clear stored tokens
|
|
792
|
+
*/
|
|
793
|
+
async logout() {
|
|
794
|
+
await this.tokenStorage.clearTokens();
|
|
795
|
+
this.credentials.refreshToken = void 0;
|
|
796
|
+
this.logger.info("Logged out successfully");
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Push local database to cloud
|
|
800
|
+
*/
|
|
801
|
+
async pushToCloud() {
|
|
802
|
+
await this.ensureAuthenticated();
|
|
803
|
+
return this.syncEngine.pushToCloud();
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Pull database from cloud to local
|
|
807
|
+
*/
|
|
808
|
+
async pullFromCloud() {
|
|
809
|
+
await this.ensureAuthenticated();
|
|
810
|
+
return this.syncEngine.pullFromCloud();
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Bidirectional sync
|
|
814
|
+
*/
|
|
815
|
+
async sync() {
|
|
816
|
+
await this.ensureAuthenticated();
|
|
817
|
+
return this.syncEngine.sync();
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Cleanup and shutdown
|
|
821
|
+
*/
|
|
822
|
+
async shutdown() {
|
|
823
|
+
this.dbManager.close();
|
|
824
|
+
this.logger.info("SqliteCloudBackup shutdown complete");
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
var index_default = SqliteCloudBackup;
|
|
828
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
829
|
+
0 && (module.exports = {
|
|
830
|
+
SqliteCloudBackup
|
|
831
|
+
});
|
|
832
|
+
//# sourceMappingURL=index.cjs.map
|