mirror of
https://github.com/martinlegend/neogdsync.git
synced 2026-07-22 06:08:29 +00:00
Bump to v0.2.1; fix cross-folder rename data loss + surface delete errors
1. Cross-folder rename: handlePush now calls drive.moveFile when op='create' and a cached driveId exists (index.rename already ran). Previously, updateFile only updated content — the Drive file stayed in the old parent folder and was deleted when that folder was trashed. New DriveApi.moveFile uses addParents/removeParents to atomically relocate the file on Drive. 2. handleDelete no longer swallows Drive API errors silently. Failures are now surfaced in result.errors and the index entry is preserved so the next sync can retry, instead of orphaning the Drive file forever. 3. getChanges fields now include 'size' and confirm 'trashed' is requested (aligns compiled JS with existing TS source). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4104a479da
commit
0e2ffe2e79
8 changed files with 205 additions and 15 deletions
23
main.js
23
main.js
|
|
@ -158,6 +158,10 @@ var DriveApi = class {
|
|||
const { id } = resp.json;
|
||||
return id;
|
||||
}
|
||||
async moveFile(driveId, oldParentId, newParentId) {
|
||||
const params = new URLSearchParams({ addParents: newParentId, removeParents: oldParentId, fields: "id" });
|
||||
await this.request("PATCH", `${BASE}/files/${driveId}?${params}`);
|
||||
}
|
||||
async renameFile(driveId, newName) {
|
||||
await this.request(
|
||||
"PATCH",
|
||||
|
|
@ -191,7 +195,7 @@ var DriveApi = class {
|
|||
pageToken: token,
|
||||
pageSize: "1000",
|
||||
includeRemoved: "true",
|
||||
fields: "nextPageToken,newStartPageToken,changes(fileId,removed,file(id,name,mimeType,modifiedTime,trashed))"
|
||||
fields: "nextPageToken,newStartPageToken,changes(fileId,removed,file(id,name,mimeType,modifiedTime,size,trashed))"
|
||||
});
|
||||
const resp = await this.request("GET", `${BASE}/changes?${params}`);
|
||||
const data = resp.json;
|
||||
|
|
@ -756,6 +760,14 @@ var Syncer = class {
|
|||
const mimeType = fromPath(path);
|
||||
const cached2 = this.index.get(path);
|
||||
if (cached2 && !cached2.isFolder) {
|
||||
if (op === "create") {
|
||||
const targetParentId = await this.index.resolveParentFolder(path);
|
||||
const meta = await this.drive.getFileMeta(cached2.driveId);
|
||||
const currentParentId = meta.parents?.[0];
|
||||
if (currentParentId && currentParentId !== targetParentId) {
|
||||
await this.drive.moveFile(cached2.driveId, currentParentId, targetParentId);
|
||||
}
|
||||
}
|
||||
await this.drive.updateFile(cached2.driveId, bytes, mimeType, mtime, this.settings.keepRevisions);
|
||||
this.index.set(path, { ...cached2, driveMtime: mtime, syncedAt: Date.now() });
|
||||
} else {
|
||||
|
|
@ -777,11 +789,14 @@ var Syncer = class {
|
|||
if (cached2) {
|
||||
try {
|
||||
await this.drive.deleteFile(cached2.driveId);
|
||||
} catch (e) {
|
||||
this.index.delete(path);
|
||||
result.deleted.push(path);
|
||||
} catch (err) {
|
||||
result.errors.push({ path, error: `Drive delete failed: ${err instanceof Error ? err.message : String(err)}` });
|
||||
}
|
||||
this.index.delete(path);
|
||||
} else {
|
||||
result.deleted.push(path);
|
||||
}
|
||||
result.deleted.push(path);
|
||||
}
|
||||
async handleConflict(path, driveMtime, result) {
|
||||
const entry = this.index.get(path);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "neogdsync",
|
||||
"name": "NeoGDSync",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"minAppVersion": "1.6.0",
|
||||
"description": "Lightweight Google Drive sync with path-based index, conflict detection, and smart/push/pull modes.",
|
||||
"author": "LM",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "neogdsync",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
140
src/driveApi.js
Normal file
140
src/driveApi.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -115,6 +115,11 @@ export class DriveApi {
|
|||
return id;
|
||||
}
|
||||
|
||||
async moveFile(driveId: string, oldParentId: string, newParentId: string): Promise<void> {
|
||||
const params = new URLSearchParams({ addParents: newParentId, removeParents: oldParentId, fields: 'id' });
|
||||
await this.request('PATCH', `${BASE}/files/${driveId}?${params}`);
|
||||
}
|
||||
|
||||
async renameFile(driveId: string, newName: string): Promise<void> {
|
||||
await this.request(
|
||||
'PATCH',
|
||||
|
|
@ -156,7 +161,7 @@ export class DriveApi {
|
|||
pageToken: token,
|
||||
pageSize: '1000',
|
||||
includeRemoved: 'true',
|
||||
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file(id,name,mimeType,modifiedTime,trashed))',
|
||||
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file(id,name,mimeType,modifiedTime,size,trashed))',
|
||||
});
|
||||
const resp = await this.request('GET', `${BASE}/changes?${params}`);
|
||||
const data = resp.json as {
|
||||
|
|
|
|||
|
|
@ -182,6 +182,14 @@ export class Syncer {
|
|||
const mimeType = mime.fromPath(path);
|
||||
const cached = this.index.get(path);
|
||||
if (cached && !cached.isFolder) {
|
||||
if (op === 'create') {
|
||||
const targetParentId = await this.index.resolveParentFolder(path);
|
||||
const meta = await this.drive.getFileMeta(cached.driveId);
|
||||
const currentParentId = meta.parents?.[0];
|
||||
if (currentParentId && currentParentId !== targetParentId) {
|
||||
await this.drive.moveFile(cached.driveId, currentParentId, targetParentId);
|
||||
}
|
||||
}
|
||||
await this.drive.updateFile(cached.driveId, bytes, mimeType, mtime, this.settings.keepRevisions);
|
||||
this.index.set(path, { ...cached, driveMtime: mtime, syncedAt: Date.now() });
|
||||
}
|
||||
|
|
@ -197,11 +205,16 @@ export class Syncer {
|
|||
if (cached) {
|
||||
try {
|
||||
await this.drive.deleteFile(cached.driveId);
|
||||
this.index.delete(path);
|
||||
result.deleted.push(path);
|
||||
}
|
||||
catch (err) {
|
||||
result.errors.push({ path, error: `Drive delete failed: ${err instanceof Error ? err.message : String(err)}` });
|
||||
}
|
||||
catch ( /* already gone */_a) { /* already gone */ }
|
||||
this.index.delete(path);
|
||||
}
|
||||
result.deleted.push(path);
|
||||
else {
|
||||
result.deleted.push(path);
|
||||
}
|
||||
}
|
||||
async handleConflict(path, driveMtime, result) {
|
||||
const entry = this.index.get(path);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class Syncer {
|
|||
for (const c of changes) {
|
||||
const localPath = driveIdToPath.get(c.fileId);
|
||||
if (localPath) {
|
||||
driveChanged.set(localPath, { removed: c.removed, mtime: c.file?.modifiedTime });
|
||||
driveChanged.set(localPath, { removed: c.removed || c.file?.trashed === true, mtime: c.file?.modifiedTime });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +267,16 @@ export class Syncer {
|
|||
const mimeType = mime.fromPath(path);
|
||||
const cached = this.index.get(path);
|
||||
if (cached && !cached.isFolder) {
|
||||
// Cross-folder rename: 'create' op + existing driveId means index.rename already ran.
|
||||
// Drive.updateFile only updates content — we must also move the file to the new parent.
|
||||
if (op === 'create') {
|
||||
const targetParentId = await this.index.resolveParentFolder(path);
|
||||
const meta = await this.drive.getFileMeta(cached.driveId);
|
||||
const currentParentId = meta.parents?.[0];
|
||||
if (currentParentId && currentParentId !== targetParentId) {
|
||||
await this.drive.moveFile(cached.driveId, currentParentId, targetParentId);
|
||||
}
|
||||
}
|
||||
await this.drive.updateFile(cached.driveId, bytes, mimeType, mtime, this.settings.keepRevisions);
|
||||
this.index.set(path, { ...cached, driveMtime: mtime, syncedAt: Date.now() });
|
||||
} else {
|
||||
|
|
@ -282,10 +292,16 @@ export class Syncer {
|
|||
private async handleDelete(path: string, result: SyncResult): Promise<void> {
|
||||
const cached = this.index.get(path);
|
||||
if (cached) {
|
||||
try { await this.drive.deleteFile(cached.driveId); } catch { /* already gone */ }
|
||||
this.index.delete(path);
|
||||
try {
|
||||
await this.drive.deleteFile(cached.driveId);
|
||||
this.index.delete(path);
|
||||
result.deleted.push(path);
|
||||
} catch (err: unknown) {
|
||||
result.errors.push({ path, error: `Drive delete failed: ${err instanceof Error ? err.message : String(err)}` });
|
||||
}
|
||||
} else {
|
||||
result.deleted.push(path);
|
||||
}
|
||||
result.deleted.push(path);
|
||||
}
|
||||
|
||||
private async handleConflict(path: string, driveMtime: string, result: SyncResult): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@
|
|||
"0.1.18": "1.6.0",
|
||||
"0.1.19": "1.6.0",
|
||||
"0.1.20": "1.6.0",
|
||||
"0.2.0": "1.6.0"
|
||||
"0.2.0": "1.6.0",
|
||||
"0.2.1": "1.6.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue