From 0e2ffe2e7948f745fdfec99250fd74326ea717f8 Mon Sep 17 00:00:00 2001 From: ml2310 Date: Fri, 1 May 2026 18:09:51 +0800 Subject: [PATCH] Bump to v0.2.1; fix cross-folder rename data loss + surface delete errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- main.js | 23 ++++++-- manifest.json | 2 +- package.json | 2 +- src/driveApi.js | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ src/driveApi.ts | 7 ++- src/syncer.js | 19 +++++-- src/syncer.ts | 24 +++++++-- versions.json | 3 +- 8 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 src/driveApi.js diff --git a/main.js b/main.js index 28cb07d..f8f866c 100644 --- a/main.js +++ b/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); diff --git a/manifest.json b/manifest.json index 53d1d83..fa95fc7 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package.json b/package.json index 0e9ee2f..e686d1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neogdsync", - "version": "0.2.0", + "version": "0.2.1", "description": "", "main": "index.js", "scripts": { diff --git a/src/driveApi.js b/src/driveApi.js new file mode 100644 index 0000000..8ec53c4 --- /dev/null +++ b/src/driveApi.js @@ -0,0 +1,140 @@ +/** Google Drive API v3 wrapper */ +import { requestUrl } from 'obsidian'; +import { getAccessToken } from './auth'; +const BASE = 'https://www.googleapis.com/drive/v3'; +const UPLOAD = 'https://www.googleapis.com/upload/drive/v3'; +const FOLDER_MIME = 'application/vnd.google-apps.folder'; +async function driveRequest(method, url, body, headers, refreshToken) { + const token = refreshToken ? await getAccessToken(refreshToken) : ''; + const resp = await requestUrl({ + url, + method, + headers: { Authorization: `Bearer ${token}`, ...headers }, + body, + throw: false, + }); + if (resp.status >= 400) { + const txt = resp.text.slice(0, 200); + throw new Error(`Drive ${method} ${url} → ${resp.status}: ${txt}`); + } + return resp; +} +export class DriveApi { + constructor(refreshToken) { + this.refreshToken = refreshToken; + } + request(method, url, body, headers) { + return driveRequest(method, url, body, headers, this.refreshToken); + } + // ── Folder operations ────────────────────────────────────────── + async listChildren(folderId) { + var _a; + const results = []; + let pageToken; + do { + const params = new URLSearchParams({ + q: `'${folderId}' in parents and trashed=false`, + fields: 'nextPageToken,files(id,name,mimeType,modifiedTime,size)', + pageSize: '1000', + }); + if (pageToken) + params.set('pageToken', pageToken); + const resp = await this.request('GET', `${BASE}/files?${params}`); + const data = resp.json; + results.push(...((_a = data.files) !== null && _a !== void 0 ? _a : [])); + pageToken = data.nextPageToken; + } while (pageToken); + return results; + } + async createFolder(name, parentId) { + const resp = await this.request('POST', `${BASE}/files?fields=id`, JSON.stringify({ name, mimeType: FOLDER_MIME, parents: [parentId] }), { 'Content-Type': 'application/json' }); + const { id } = resp.json; + return id; + } + // ── File operations ──────────────────────────────────────────── + async uploadFile(name, parentId, content, mimeType, modifiedTime, keepRevision = false) { + const boundary = 'neogdsync_boundary'; + const meta = JSON.stringify({ name, parents: [parentId], modifiedTime }); + const body = buildMultipart(boundary, meta, content, mimeType); + const params = new URLSearchParams({ uploadType: 'multipart', fields: 'id' }); + if (keepRevision) + params.set('keepRevisionForever', 'true'); + const resp = await this.request('POST', `${UPLOAD}/files?${params}`, body.buffer, { 'Content-Type': `multipart/related; boundary=${boundary}` }); + const { id } = resp.json; + return id; + } + async updateFile(driveId, content, mimeType, modifiedTime, keepRevision = false) { + const boundary = 'neogdsync_boundary'; + const meta = JSON.stringify({ modifiedTime }); + const body = buildMultipart(boundary, meta, content, mimeType); + const params = new URLSearchParams({ uploadType: 'multipart', fields: 'id' }); + if (keepRevision) + params.set('keepRevisionForever', 'true'); + const resp = await this.request('PATCH', `${UPLOAD}/files/${driveId}?${params}`, body.buffer, { 'Content-Type': `multipart/related; boundary=${boundary}` }); + 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', `${BASE}/files/${driveId}?fields=id`, JSON.stringify({ name: newName }), { 'Content-Type': 'application/json' }); + } + async deleteFile(driveId) { + await this.request('DELETE', `${BASE}/files/${driveId}`); + } + async downloadFile(driveId) { + const resp = await this.request('GET', `${BASE}/files/${driveId}?alt=media`); + return resp.arrayBuffer; + } + async getFileMeta(driveId) { + const resp = await this.request('GET', `${BASE}/files/${driveId}?fields=id,name,mimeType,modifiedTime,parents,size`); + return resp.json; + } + async getChanges(pageToken) { + var _a, _b; + const changes = []; + let token = pageToken; + while (token) { + const params = new URLSearchParams({ + pageToken: token, + pageSize: '1000', + includeRemoved: 'true', + 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; + changes.push(...((_a = data.changes) !== null && _a !== void 0 ? _a : [])); + token = (_b = data.nextPageToken) !== null && _b !== void 0 ? _b : ''; + if (data.newStartPageToken) { + return { changes, newToken: data.newStartPageToken }; + } + } + return { changes, newToken: pageToken }; + } + async getStartPageToken() { + const resp = await this.request('GET', `${BASE}/changes/startPageToken`); + const { startPageToken } = resp.json; + return startPageToken; + } + async listRevisions(driveId) { + var _a; + const resp = await this.request('GET', `${BASE}/files/${driveId}/revisions?fields=revisions(id,modifiedTime,size)`); + const data = resp.json; + return (_a = data.revisions) !== null && _a !== void 0 ? _a : []; + } +} +// ── helpers ──────────────────────────────────────────────────── +function buildMultipart(boundary, meta, content, mime) { + const enc = new TextEncoder(); + const header = enc.encode(`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${meta}\r\n` + + `--${boundary}\r\nContent-Type: ${mime}\r\n\r\n`); + const footer = enc.encode(`\r\n--${boundary}--`); + const body = new Uint8Array(header.byteLength + content.byteLength + footer.byteLength); + body.set(header, 0); + body.set(new Uint8Array(content), header.byteLength); + body.set(footer, header.byteLength + content.byteLength); + return body; +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHJpdmVBcGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJkcml2ZUFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxrQ0FBa0M7QUFFbEMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFVBQVUsQ0FBQztBQUN0QyxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBR3hDLE1BQU0sSUFBSSxHQUFHLHFDQUFxQyxDQUFDO0FBQ25ELE1BQU0sTUFBTSxHQUFHLDRDQUE0QyxDQUFDO0FBQzVELE1BQU0sV0FBVyxHQUFHLG9DQUFvQyxDQUFDO0FBRXpELEtBQUssVUFBVSxZQUFZLENBQ3pCLE1BQWMsRUFDZCxHQUFXLEVBQ1gsSUFBMkIsRUFDM0IsT0FBZ0MsRUFDaEMsWUFBcUI7SUFFckIsTUFBTSxLQUFLLEdBQUcsWUFBWSxDQUFDLENBQUMsQ0FBQyxNQUFNLGNBQWMsQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3JFLE1BQU0sSUFBSSxHQUFHLE1BQU0sVUFBVSxDQUFDO1FBQzVCLEdBQUc7UUFDSCxNQUFNO1FBQ04sT0FBTyxFQUFFLEVBQUUsYUFBYSxFQUFFLFVBQVUsS0FBSyxFQUFFLEVBQUUsR0FBRyxPQUFPLEVBQUU7UUFDekQsSUFBSTtRQUNKLEtBQUssRUFBRSxLQUFLO0tBQ2IsQ0FBQyxDQUFDO0lBQ0gsSUFBSSxJQUFJLENBQUMsTUFBTSxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQztRQUNwQyxNQUFNLElBQUksS0FBSyxDQUFDLFNBQVMsTUFBTSxJQUFJLEdBQUcsTUFBTSxJQUFJLENBQUMsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDckUsQ0FBQztJQUNELE9BQU8sSUFBSSxDQUFDO0FBQ2QsQ0FBQztBQUVELE1BQU0sT0FBTyxRQUFR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S0FBSyxRQUFRLDREQUE0RCxJQUFJLE1BQU07UUFDbkYsS0FBSyxRQUFRLHFCQUFxQixJQUFJLFVBQVUsQ0FDakQsQ0FBQztJQUNGLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsU0FBUyxRQUFRLElBQUksQ0FBQyxDQUFDO0lBQ2pELE1BQU0sSUFBSSxHQUFHLElBQUksVUFBVSxDQUFDLE1BQU0sQ0FBQyxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsR0FBRyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDeEYsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDcEIsSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJLFVBQVUsQ0FBQyxPQUFPLENBQUMsRUFBRSxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDckQsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLFVBQVUsR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDekQsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqIEdvb2dsZSBEcml2ZSBBUEkgdjMgd3JhcHBlciAqL1xuXG5pbXBvcnQgeyByZXF1ZXN0VXJsIH0gZnJvbSAnb2JzaWRpYW4nO1xuaW1wb3J0IHsgZ2V0QWNjZXNzVG9rZW4gfSBmcm9tICcuL2F1dGgnO1xuaW1wb3J0IHsgRHJpdmVGaWxlSW5mbywgRHJpdmVDaGFuZ2UsIERyaXZlUmV2aXNpb24gfSBmcm9tICcuL3R5cGVzJztcblxuY29uc3QgQkFTRSA9ICdodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9kcml2ZS92Myc7XG5jb25zdCBVUExPQUQgPSAnaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXBsb2FkL2RyaXZlL3YzJztcbmNvbnN0IEZPTERFUl9NSU1FID0gJ2FwcGxpY2F0aW9uL3ZuZC5nb29nbGUtYXBwcy5mb2xkZXInO1xuXG5hc3luYyBmdW5jdGlvbiBkcml2ZVJlcXVlc3QoXG4gIG1ldGhvZDogc3RyaW5nLFxuICB1cmw6IHN0cmluZyxcbiAgYm9keT86IHN0cmluZyB8IEFycmF5QnVmZmVyLFxuICBoZWFkZXJzPzogUmVjb3JkPHN0cmluZywgc3RyaW5nPixcbiAgcmVmcmVzaFRva2VuPzogc3RyaW5nLFxuKTogUHJvbWlzZTx7IHN0YXR1czogbnVtYmVyOyBqc29uOiB1bmtub3duOyB0ZXh0OiBzdHJpbmc7IGFycmF5QnVmZmVyOiBBcnJheUJ1ZmZlciB9PiB7XG4gIGNvbnN0IHRva2VuID0gcmVmcmVzaFRva2VuID8gYXdhaXQgZ2V0QWNjZXNzVG9rZW4ocmVmcmVzaFRva2VuKSA6ICcnO1xuICBjb25zdCByZXNwID0gYXdhaXQgcmVxdWVzdFVybCh7XG4gICAgdXJsLFxuICAgIG1ldGhvZCxcbiAgICBoZWFkZXJzOiB7IEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHt0b2tlbn1gLCAuLi5oZWFkZXJzIH0sXG4gICAgYm9keSxcbiAgICB0aHJvdzogZmFsc2UsXG4gIH0pO1xuICBpZiAocmVzcC5zdGF0dXMgPj0gNDAwKSB7XG4gICAgY29uc3QgdHh0ID0gcmVzcC50ZXh0LnNsaWNlKDAsIDIwMCk7XG4gICAgdGhyb3cgbmV3IEVycm9yKGBEcml2ZSAke21ldGhvZH0gJHt1cmx9IOKGkiAke3Jlc3Auc3RhdHVzfTogJHt0eHR9YCk7XG4gIH1cbiAgcmV0dXJuIHJlc3A7XG59XG5cbmV4cG9ydCBjbGFzcyBEcml2ZUFwaSB7XG4gIGNvbnN0cnVjdG9yKHByaXZhdGUgcmVmcmVzaFRva2VuOiBzdHJpbmcpIHt9XG5cbiAgcHJpdmF0ZSByZXF1ZXN0KG1ldGhvZDogc3RyaW5nLCB1cmw6IHN0cmluZywgYm9keT86IHN0cmluZyB8IEFycmF5QnVmZmVyLCBoZWFkZXJzPzogUmVjb3JkPHN0cmluZywgc3RyaW5nPikge1xuICAgIHJldHVybiBkcml2ZVJlcXVlc3QobWV0aG9kLCB1cmwsIGJvZHksIGhlYWRlcnMsIHRoaXMucmVmcmVzaFRva2VuKTtcbiAgfVxuXG4gIC8vIOKUgOKUgCBGb2xkZXIgb3BlcmF0aW9ucyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuICBhc3luYyBsaXN0Q2hpbGRyZW4oZm9sZGVySWQ6IHN0cmluZyk6IFByb21pc2U8RHJpdmVGaWxlSW5mb1tdPiB7XG4gICAgY29uc3QgcmVzdWx0czogRHJpdmVGaWxlSW5mb1tdID0gW107XG4gICAgbGV0IHBhZ2VUb2tlbjogc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIGRvIHtcbiAgICAgIGNvbnN0IHBhcmFtcyA9IG5ldyBVUkxTZWFyY2hQYXJhbXMoe1xuICAgICAgICBxOiBgJyR7Zm9sZGVySWR9JyBpbiBwYXJlbnRzIGFuZCB0cmFzaGVkPWZhbHNlYCxcbiAgICAgICAgZmllbGRzOiAnbmV4dFBhZ2VUb2tlbixmaWxlcyhpZCxuYW1lLG1pbWVUeXBlLG1vZGlmaWVkVGltZSxzaXplKScsXG4gICAgICAgIHBhZ2VTaXplOiAnMTAwMCcsXG4gICAgICB9KTtcbiAgICAgIGlmIChwYWdlVG9rZW4pIHBhcmFtcy5zZXQoJ3BhZ2VUb2tlbicsIHBhZ2VUb2tlbik7XG4gICAgICBjb25zdCByZXNwID0gYXdhaXQgdGhpcy5yZXF1ZXN0KCdHRVQnLCBgJHtCQVNFfS9maWxlcz8ke3BhcmFtc31gKTtcbiAgICAgIGNvbnN0IGRhdGEgPSByZXNwLmpzb24gYXMgeyBmaWxlcz86IERyaXZlRmlsZUluZm9bXTsgbmV4dFBhZ2VUb2tlbj86IHN0cmluZyB9O1xuICAgICAgcmVzdWx0cy5wdXNoKC4uLihkYXRhLmZpbGVzID8/IFtdKSk7XG4gICAgICBwYWdlVG9rZW4gPSBkYXRhLm5leHRQYWdlVG9rZW47XG4gICAgfSB3aGlsZSAocGFnZVRva2VuKTtcbiAgICByZXR1cm4gcmVzdWx0cztcbiAgfVxuXG4gIGFzeW5jIGNyZWF0ZUZvbGRlcihuYW1lOiBzdHJpbmcsIHBhcmVudElkOiBzdHJpbmcpOiBQcm9taXNlPHN0cmluZz4ge1xuICAgIGNvbnN0IHJlc3AgPSBhd2FpdCB0aGlzLnJlcXVlc3QoXG4gICAgICAnUE9TVCcsXG4gICAgICBgJHtCQVNFfS9maWxlcz9maWVsZHM9aWRgLFxuICAgICAgSlNPTi5zdHJpbmdpZnkoeyBuYW1lLCBtaW1lVHlwZTogRk9MREVSX01JTUUsIHBhcmVudHM6IFtwYXJlbnRJZF0gfSksXG4gICAgICB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICApO1xuICAgIGNvbnN0IHsgaWQgfSA9IHJlc3AuanNvbiBhcyB7IGlkOiBzdHJpbmcgfTtcbiAgICByZXR1cm4gaWQ7XG4gIH1cblxuICAvLyDilIDilIAgRmlsZSBvcGVyYXRpb25zIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuXG4gIGFzeW5jIHVwbG9hZEZpbGUoXG4gICAgbmFtZTogc3RyaW5nLFxuICAgIHBhcmVudElkOiBzdHJpbmcsXG4gICAgY29udGVudDogQXJyYXlCdWZmZXIsXG4gICAgbWltZVR5cGU6IHN0cmluZyxcbiAgICBtb2RpZmllZFRpbWU6IHN0cmluZyxcbiAgICBrZWVwUmV2aXNpb24gPSBmYWxzZSxcbiAgKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICBjb25zdCBib3VuZGFyeSA9ICduZW9nZHN5bmNfYm91bmRhcnknO1xuICAgIGNvbnN0IG1ldGEgPSBKU09OLnN0cmluZ2lmeSh7IG5hbWUsIHBhcmVudHM6IFtwYXJlbnRJZF0sIG1vZGlmaWVkVGltZSB9KTtcbiAgICBjb25zdCBib2R5ID0gYnVpbGRNdWx0aXBhcnQoYm91bmRhcnksIG1ldGEsIGNvbnRlbnQsIG1pbWVUeXBlKTtcbiAgICBjb25zdCBwYXJhbXMgPSBuZXcgVVJMU2VhcmNoUGFyYW1zKHsgdXBsb2FkVHlwZTogJ211bHRpcGFydCcsIGZpZWxkczogJ2lkJyB9KTtcbiAgICBpZiAoa2VlcFJldmlzaW9uKSBwYXJhbXMuc2V0KCdrZWVwUmV2aXNpb25Gb3JldmVyJywgJ3RydWUnKTtcbiAgICBjb25zdCByZXNwID0gYXdhaXQgdGhpcy5yZXF1ZXN0KFxuICAgICAgJ1BPU1QnLFxuICAgICAgYCR7VVBMT0FEfS9maWxlcz8ke3BhcmFtc31gLFxuICAgICAgYm9keS5idWZmZXIsXG4gICAgICB7ICdDb250ZW50LVR5cGUnOiBgbXVsdGlwYXJ0L3JlbGF0ZWQ7IGJvdW5kYXJ5PSR7Ym91bmRhcnl9YCB9LFxuICAgICk7XG4gICAgY29uc3QgeyBpZCB9ID0gcmVzcC5qc29uIGFzIHsgaWQ6IHN0cmluZyB9O1xuICAgIHJldHVybiBpZDtcbiAgfVxuXG4gIGFzeW5jIHVwZGF0ZUZpbGUoXG4gICAgZHJpdmVJZDogc3RyaW5nLFxuICAgIGNvbnRlbnQ6IEFycmF5QnVmZmVyLFxuICAgIG1pbWVUeXBlOiBzdHJpbmcsXG4gICAgbW9kaWZpZWRUaW1lOiBzdHJpbmcsXG4gICAga2VlcFJldmlzaW9uID0gZmFsc2UsXG4gICk6IFByb21pc2U8c3RyaW5nPiB7XG4gICAgY29uc3QgYm91bmRhcnkgPSAnbmVvZ2RzeW5jX2JvdW5kYXJ5JztcbiAgICBjb25zdCBtZXRhID0gSlNPTi5zdHJpbmdpZnkoeyBtb2RpZmllZFRpbWUgfSk7XG4gICAgY29uc3QgYm9keSA9IGJ1aWxkTXVsdGlwYXJ0KGJvdW5kYXJ5LCBtZXRhLCBjb250ZW50LCBtaW1lVHlwZSk7XG4gICAgY29uc3QgcGFyYW1zID0gbmV3IFVSTFNlYXJjaFBhcmFtcyh7IHVwbG9hZFR5cGU6ICdtdWx0aXBhcnQnLCBmaWVsZHM6ICdpZCcgfSk7XG4gICAgaWYgKGtlZXBSZXZpc2lvbikgcGFyYW1zLnNldCgna2VlcFJldmlzaW9uRm9yZXZlcicsICd0cnVlJyk7XG4gICAgY29uc3QgcmVzcCA9IGF3YWl0IHRoaXMucmVxdWVzdChcbiAgICAgICdQQVRDSCcsXG4gICAgICBgJHtVUExPQUR9L2ZpbGVzLyR7ZHJpdmVJZH0/JHtwYXJhbXN9YCxcbiAgICAgIGJvZHkuYnVmZmVyLFxuICAgICAgeyAnQ29udGVudC1UeXBlJzogYG11bHRpcGFydC9yZWxhdGVkOyBib3VuZGFyeT0ke2JvdW5kYXJ5fWAgfSxcbiAgICApO1xuICAgIGNvbnN0IHsgaWQgfSA9IHJlc3AuanNvbiBhcyB7IGlkOiBzdHJpbmcgfTtcbiAgICByZXR1cm4gaWQ7XG4gIH1cblxuICBhc3luYyByZW5hbWVGaWxlKGRyaXZlSWQ6IHN0cmluZywgbmV3TmFtZTogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgYXdhaXQgdGhpcy5yZXF1ZXN0KFxuICAgICAgJ1BBVENIJyxcbiAgICAgIGAke0JBU0V9L2ZpbGVzLyR7ZHJpdmVJZH0/ZmllbGRzPWlkYCxcbiAgICAgIEpTT04uc3RyaW5naWZ5KHsgbmFtZTogbmV3TmFtZSB9KSxcbiAgICAgIHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICk7XG4gIH1cblxuICBhc3luYyBkZWxldGVGaWxlKGRyaXZlSWQ6IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICAgIGF3YWl0IHRoaXMucmVxdWVzdCgnREVMRVRFJywgYCR7QkFTRX0vZmlsZXMvJHtkcml2ZUlkfWApO1xuICB9XG5cbiAgYXN5bmMgZG93bmxvYWRGaWxlKGRyaXZlSWQ6IHN0cmluZyk6IFByb21pc2U8QXJyYXlCdWZmZXI+IHtcbiAgICBjb25zdCByZXNwID0gYXdhaXQgdGhpcy5yZXF1ZXN0KCdHRVQnLCBgJHtCQVNFfS9maWxlcy8ke2RyaXZlSWR9P2FsdD1tZWRpYWApO1xuICAgIHJldHVybiByZXNwLmFycmF5QnVmZmVyO1xuICB9XG5cbiAgYXN5bmMgZ2V0RmlsZU1ldGEoZHJpdmVJZDogc3RyaW5nKTogUHJvbWlzZTxEcml2ZUZpbGVJbmZvPiB7XG4gICAgY29uc3QgcmVzcCA9IGF3YWl0IHRoaXMucmVxdWVzdCgnR0VUJywgYCR7QkFTRX0vZmlsZXMvJHtkcml2ZUlkfT9maWVsZHM9aWQsbmFtZSxtaW1lVHlwZSxtb2RpZmllZFRpbWUscGFyZW50cyxzaXplYCk7XG4gICAgcmV0dXJuIHJlc3AuanNvbiBhcyBEcml2ZUZpbGVJbmZvO1xuICB9XG5cbiAgYXN5bmMgZ2V0Q2hhbmdlcyhwYWdlVG9rZW46IHN0cmluZyk6IFByb21pc2U8eyBjaGFuZ2VzOiBEcml2ZUNoYW5nZVtdOyBuZXdUb2tlbjogc3RyaW5nIH0+IHtcbiAgICBjb25zdCBjaGFuZ2VzOiBEcml2ZUNoYW5nZVtdID0gW107XG4gICAgbGV0IHRva2VuID0gcGFnZVRva2VuO1xuICAgIHdoaWxlICh0b2tlbikge1xuICAgICAgY29uc3QgcGFyYW1zID0gbmV3IFVSTFNlYXJjaFBhcmFtcyh7XG4gICAgICAgIHBhZ2VUb2tlbjogdG9rZW4sXG4gICAgICAgIHBhZ2VTaXplOiAnMTAwMCcsXG4gICAgICAgIGluY2x1ZGVSZW1vdmVkOiAndHJ1ZScsXG4gICAgICAgIGZpZWxkczogJ25leHRQYWdlVG9rZW4sbmV3U3RhcnRQYWdlVG9rZW4sY2hhbmdlcyhmaWxlSWQscmVtb3ZlZCxmaWxlKGlkLG5hbWUsbWltZVR5cGUsbW9kaWZpZWRUaW1lKSknLFxuICAgICAgfSk7XG4gICAgICBjb25zdCByZXNwID0gYXdhaXQgdGhpcy5yZXF1ZXN0KCdHRVQnLCBgJHtCQVNFfS9jaGFuZ2VzPyR7cGFyYW1zfWApO1xuICAgICAgY29uc3QgZGF0YSA9IHJlc3AuanNvbiBhcyB7XG4gICAgICAgIGNoYW5nZXM/OiBEcml2ZUNoYW5nZVtdO1xuICAgICAgICBuZXh0UGFnZVRva2VuPzogc3RyaW5nO1xuICAgICAgICBuZXdTdGFydFBhZ2VUb2tlbj86IHN0cmluZztcbiAgICAgIH07XG4gICAgICBjaGFuZ2VzLnB1c2goLi4uKGRhdGEuY2hhbmdlcyA/PyBbXSkpO1xuICAgICAgdG9rZW4gPSBkYXRhLm5leHRQYWdlVG9rZW4gPz8gJyc7XG4gICAgICBpZiAoZGF0YS5uZXdTdGFydFBhZ2VUb2tlbikge1xuICAgICAgICByZXR1cm4geyBjaGFuZ2VzLCBuZXdUb2tlbjogZGF0YS5uZXdTdGFydFBhZ2VUb2tlbiB9O1xuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4geyBjaGFuZ2VzLCBuZXdUb2tlbjogcGFnZVRva2VuIH07XG4gIH1cblxuICBhc3luYyBnZXRTdGFydFBhZ2VUb2tlbigpOiBQcm9taXNlPHN0cmluZz4ge1xuICAgIGNvbnN0IHJlc3AgPSBhd2FpdCB0aGlzLnJlcXVlc3QoJ0dFVCcsIGAke0JBU0V9L2NoYW5nZXMvc3RhcnRQYWdlVG9rZW5gKTtcbiAgICBjb25zdCB7IHN0YXJ0UGFnZVRva2VuIH0gPSByZXNwLmpzb24gYXMgeyBzdGFydFBhZ2VUb2tlbjogc3RyaW5nIH07XG4gICAgcmV0dXJuIHN0YXJ0UGFnZVRva2VuO1xuICB9XG5cbiAgYXN5bmMgbGlzdFJldmlzaW9ucyhkcml2ZUlkOiBzdHJpbmcpOiBQcm9taXNlPERyaXZlUmV2aXNpb25bXT4ge1xuICAgIGNvbnN0IHJlc3AgPSBhd2FpdCB0aGlzLnJlcXVlc3QoJ0dFVCcsIGAke0JBU0V9L2ZpbGVzLyR7ZHJpdmVJZH0vcmV2aXNpb25zP2ZpZWxkcz1yZXZpc2lvbnMoaWQsbW9kaWZpZWRUaW1lLHNpemUpYCk7XG4gICAgY29uc3QgZGF0YSA9IHJlc3AuanNvbiBhcyB7IHJldmlzaW9ucz86IERyaXZlUmV2aXNpb25bXSB9O1xuICAgIHJldHVybiBkYXRhLnJldmlzaW9ucyA/PyBbXTtcbiAgfVxufVxuXG4vLyDilIDilIAgaGVscGVycyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuZnVuY3Rpb24gYnVpbGRNdWx0aXBhcnQoYm91bmRhcnk6IHN0cmluZywgbWV0YTogc3RyaW5nLCBjb250ZW50OiBBcnJheUJ1ZmZlciwgbWltZTogc3RyaW5nKTogVWludDhBcnJheSB7XG4gIGNvbnN0IGVuYyA9IG5ldyBUZXh0RW5jb2RlcigpO1xuICBjb25zdCBoZWFkZXIgPSBlbmMuZW5jb2RlKFxuICAgIGAtLSR7Ym91bmRhcnl9XFxyXFxuQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PVVURi04XFxyXFxuXFxyXFxuJHttZXRhfVxcclxcbmAgK1xuICAgIGAtLSR7Ym91bmRhcnl9XFxyXFxuQ29udGVudC1UeXBlOiAke21pbWV9XFxyXFxuXFxyXFxuYCxcbiAgKTtcbiAgY29uc3QgZm9vdGVyID0gZW5jLmVuY29kZShgXFxyXFxuLS0ke2JvdW5kYXJ5fS0tYCk7XG4gIGNvbnN0IGJvZHkgPSBuZXcgVWludDhBcnJheShoZWFkZXIuYnl0ZUxlbmd0aCArIGNvbnRlbnQuYnl0ZUxlbmd0aCArIGZvb3Rlci5ieXRlTGVuZ3RoKTtcbiAgYm9keS5zZXQoaGVhZGVyLCAwKTtcbiAgYm9keS5zZXQobmV3IFVpbnQ4QXJyYXkoY29udGVudCksIGhlYWRlci5ieXRlTGVuZ3RoKTtcbiAgYm9keS5zZXQoZm9vdGVyLCBoZWFkZXIuYnl0ZUxlbmd0aCArIGNvbnRlbnQuYnl0ZUxlbmd0aCk7XG4gIHJldHVybiBib2R5O1xufVxuIl19 \ No newline at end of file diff --git a/src/driveApi.ts b/src/driveApi.ts index 9d5e4b7..2b4683b 100644 --- a/src/driveApi.ts +++ b/src/driveApi.ts @@ -115,6 +115,11 @@ export class DriveApi { return id; } + async moveFile(driveId: string, oldParentId: string, newParentId: string): Promise { + 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 { 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 { diff --git a/src/syncer.js b/src/syncer.js index 14aaf66..96bbade 100644 --- a/src/syncer.js +++ b/src/syncer.js @@ -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); diff --git a/src/syncer.ts b/src/syncer.ts index 3f37179..65c7e5a 100644 --- a/src/syncer.ts +++ b/src/syncer.ts @@ -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 { 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 { diff --git a/versions.json b/versions.json index 6154d93..165d387 100644 --- a/versions.json +++ b/versions.json @@ -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" }