@docstack/pouchdb-adapter-googledrive 0.0.5 → 0.0.6
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/lib/client.d.ts +3 -0
- package/lib/client.js +55 -18
- package/lib/drive.d.ts +1 -0
- package/lib/drive.js +11 -3
- package/package.json +1 -1
package/lib/client.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface DriveFile {
|
|
|
4
4
|
mimeType: string;
|
|
5
5
|
parents?: string[];
|
|
6
6
|
etag?: string;
|
|
7
|
+
modifiedTime?: string;
|
|
7
8
|
}
|
|
8
9
|
export interface DriveClientOptions {
|
|
9
10
|
accessToken: string | (() => Promise<string>);
|
|
@@ -19,10 +20,12 @@ export declare class GoogleDriveClient {
|
|
|
19
20
|
createFile(name: string, parents: string[] | undefined, mimeType: string, content: string): Promise<{
|
|
20
21
|
id: string;
|
|
21
22
|
etag: string;
|
|
23
|
+
modifiedTime: string;
|
|
22
24
|
}>;
|
|
23
25
|
updateFile(fileId: string, content: string, expectedEtag?: string): Promise<{
|
|
24
26
|
id: string;
|
|
25
27
|
etag: string;
|
|
28
|
+
modifiedTime: string;
|
|
26
29
|
}>;
|
|
27
30
|
deleteFile(fileId: string): Promise<void>;
|
|
28
31
|
private buildMultipart;
|
package/lib/client.js
CHANGED
|
@@ -14,24 +14,44 @@ class GoogleDriveClient {
|
|
|
14
14
|
return this.options.accessToken;
|
|
15
15
|
}
|
|
16
16
|
async fetch(url, init) {
|
|
17
|
+
const method = init.method || 'GET';
|
|
17
18
|
const token = await this.getToken();
|
|
18
19
|
const headers = new Headers(init.headers);
|
|
19
20
|
headers.set('Authorization', `Bearer ${token}`);
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(url, { ...init, headers });
|
|
24
|
+
}
|
|
25
|
+
catch (networkErr) {
|
|
26
|
+
const err = new Error(`Network Error: ${networkErr.message} (${method} ${url})`);
|
|
27
|
+
err.code = 'network_error';
|
|
28
|
+
err.url = url;
|
|
29
|
+
err.method = method;
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
22
32
|
if (!res.ok) {
|
|
23
|
-
// Basic error handling
|
|
24
33
|
const text = await res.text();
|
|
25
34
|
let errorMsg = `Drive API Error: ${res.status} ${res.statusText} (${method} ${url})`;
|
|
35
|
+
let reason = res.statusText;
|
|
26
36
|
try {
|
|
27
37
|
const json = JSON.parse(text);
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
const gError = json.error;
|
|
39
|
+
if (gError) {
|
|
40
|
+
errorMsg += ` - ${gError.message || 'Unknown Error'}`;
|
|
41
|
+
if (Array.isArray(gError.errors) && gError.errors.length > 0) {
|
|
42
|
+
reason = gError.errors[0].reason || reason;
|
|
43
|
+
if (gError.errors[0].message && gError.errors[0].message !== gError.message) {
|
|
44
|
+
errorMsg += ` (${gError.errors[0].message})`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
}
|
|
31
48
|
}
|
|
32
49
|
catch { }
|
|
33
50
|
const err = new Error(errorMsg);
|
|
34
51
|
err.status = res.status;
|
|
52
|
+
err.code = reason;
|
|
53
|
+
err.url = url;
|
|
54
|
+
err.method = method;
|
|
35
55
|
throw err;
|
|
36
56
|
}
|
|
37
57
|
return res;
|
|
@@ -39,11 +59,11 @@ class GoogleDriveClient {
|
|
|
39
59
|
async listFiles(q) {
|
|
40
60
|
const params = new URLSearchParams({
|
|
41
61
|
q,
|
|
42
|
-
fields: 'files(id,
|
|
43
|
-
spaces: 'drive',
|
|
44
|
-
pageSize: '1000' // Ensure we get enough
|
|
62
|
+
fields: 'files(id,name,mimeType,parents,modifiedTime)'
|
|
45
63
|
});
|
|
46
|
-
|
|
64
|
+
// FIX: URLSearchParams uses '+', but Drive API is safer with '%20'
|
|
65
|
+
const queryString = params.toString().replace(/\+/g, '%20');
|
|
66
|
+
const res = await this.fetch(`${BASE_URL}?${queryString}`, { method: 'GET' });
|
|
47
67
|
const data = await res.json();
|
|
48
68
|
return data.files || [];
|
|
49
69
|
}
|
|
@@ -51,7 +71,8 @@ class GoogleDriveClient {
|
|
|
51
71
|
// Try getting media
|
|
52
72
|
try {
|
|
53
73
|
const params = new URLSearchParams({ alt: 'media' });
|
|
54
|
-
const
|
|
74
|
+
const queryString = params.toString().replace(/\+/g, '%20');
|
|
75
|
+
const res = await this.fetch(`${BASE_URL}/${fileId}?${queryString}`, { method: 'GET' });
|
|
55
76
|
// Standard fetch handles JSON/Text transparency?
|
|
56
77
|
// We expect JSON mostly, but sometimes we might want text.
|
|
57
78
|
// PouchDB adapter flow: downloadJson, downloadNdjson
|
|
@@ -70,8 +91,9 @@ class GoogleDriveClient {
|
|
|
70
91
|
}
|
|
71
92
|
// Single metadata get (for etag check)
|
|
72
93
|
async getFileMetadata(fileId) {
|
|
73
|
-
const params = new URLSearchParams({ fields: 'id,
|
|
74
|
-
const
|
|
94
|
+
const params = new URLSearchParams({ fields: 'id,name,mimeType,parents,modifiedTime' });
|
|
95
|
+
const queryString = params.toString().replace(/\+/g, '%20');
|
|
96
|
+
const res = await this.fetch(`${BASE_URL}/${fileId}?${queryString}`, { method: 'GET' });
|
|
75
97
|
return await res.json();
|
|
76
98
|
}
|
|
77
99
|
async createFile(name, parents, mimeType, content) {
|
|
@@ -82,32 +104,47 @@ class GoogleDriveClient {
|
|
|
82
104
|
};
|
|
83
105
|
// Folders or empty content can use simple metadata-only POST
|
|
84
106
|
if (!content && mimeType === 'application/vnd.google-apps.folder') {
|
|
85
|
-
const res = await this.fetch(`${BASE_URL}?fields=id,
|
|
107
|
+
const res = await this.fetch(`${BASE_URL}?fields=id,modifiedTime`, {
|
|
86
108
|
method: 'POST',
|
|
87
109
|
headers: { 'Content-Type': 'application/json' },
|
|
88
110
|
body: JSON.stringify(metadata)
|
|
89
111
|
});
|
|
90
|
-
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
return {
|
|
114
|
+
id: data.id,
|
|
115
|
+
etag: data.etag || '',
|
|
116
|
+
modifiedTime: data.modifiedTime || ''
|
|
117
|
+
};
|
|
91
118
|
}
|
|
92
119
|
const multipartBody = this.buildMultipart(metadata, content, mimeType);
|
|
93
|
-
const res = await this.fetch(`${UPLOAD_URL}?uploadType=multipart&fields=id,
|
|
120
|
+
const res = await this.fetch(`${UPLOAD_URL}?uploadType=multipart&fields=id,modifiedTime`, {
|
|
94
121
|
method: 'POST',
|
|
95
122
|
headers: {
|
|
96
123
|
'Content-Type': `multipart/related; boundary=${multipartBody.boundary}`
|
|
97
124
|
},
|
|
98
125
|
body: multipartBody.body
|
|
99
126
|
});
|
|
100
|
-
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
return {
|
|
129
|
+
id: data.id,
|
|
130
|
+
etag: data.etag || '',
|
|
131
|
+
modifiedTime: data.modifiedTime || ''
|
|
132
|
+
};
|
|
101
133
|
}
|
|
102
134
|
async updateFile(fileId, content, expectedEtag) {
|
|
103
135
|
// Update content (media) usually, but sometimes meta?
|
|
104
136
|
// In our usage (saveMeta), we update body.
|
|
105
|
-
const res = await this.fetch(`${UPLOAD_URL}/${fileId}?uploadType=media&fields=id,
|
|
137
|
+
const res = await this.fetch(`${UPLOAD_URL}/${fileId}?uploadType=media&fields=id,modifiedTime`, {
|
|
106
138
|
method: 'PATCH',
|
|
107
139
|
headers: expectedEtag ? { 'If-Match': expectedEtag, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' },
|
|
108
140
|
body: content
|
|
109
141
|
});
|
|
110
|
-
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
return {
|
|
144
|
+
id: data.id,
|
|
145
|
+
etag: data.etag || '',
|
|
146
|
+
modifiedTime: data.modifiedTime || ''
|
|
147
|
+
};
|
|
111
148
|
}
|
|
112
149
|
async deleteFile(fileId) {
|
|
113
150
|
await this.fetch(`${BASE_URL}/${fileId}`, { method: 'DELETE' });
|
package/lib/drive.d.ts
CHANGED
package/lib/drive.js
CHANGED
|
@@ -27,6 +27,7 @@ class DriveHandler {
|
|
|
27
27
|
dbName: ''
|
|
28
28
|
};
|
|
29
29
|
this.metaEtag = null;
|
|
30
|
+
this.metaModifiedTime = null;
|
|
30
31
|
// In-Memory Index: ID -> Metadata/Pointer
|
|
31
32
|
this.index = {};
|
|
32
33
|
this.pendingChanges = [];
|
|
@@ -57,6 +58,7 @@ class DriveHandler {
|
|
|
57
58
|
if (metaFile) {
|
|
58
59
|
this.meta = await this.downloadJson(metaFile.id);
|
|
59
60
|
this.metaEtag = metaFile.etag || null;
|
|
61
|
+
this.metaModifiedTime = metaFile.modifiedTime || null;
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
62
64
|
await this.saveMeta(this.meta);
|
|
@@ -438,7 +440,11 @@ class DriveHandler {
|
|
|
438
440
|
const q = `name = '${safeName}' and '${this.folderId}' in parents and trashed = false`;
|
|
439
441
|
const files = await this.client.listFiles(q);
|
|
440
442
|
if (files.length > 0)
|
|
441
|
-
return {
|
|
443
|
+
return {
|
|
444
|
+
id: files[0].id,
|
|
445
|
+
etag: files[0].etag || '',
|
|
446
|
+
modifiedTime: files[0].modifiedTime || ''
|
|
447
|
+
};
|
|
442
448
|
return null;
|
|
443
449
|
}
|
|
444
450
|
async downloadJson(fileId) {
|
|
@@ -472,10 +478,12 @@ class DriveHandler {
|
|
|
472
478
|
if (metaFile) {
|
|
473
479
|
const res = await this.client.updateFile(metaFile.id, content, expectedEtag || undefined);
|
|
474
480
|
this.metaEtag = res.etag;
|
|
481
|
+
this.metaModifiedTime = res.modifiedTime;
|
|
475
482
|
}
|
|
476
483
|
else {
|
|
477
484
|
const res = await this.client.createFile('_meta.json', [this.folderId], 'application/json', content);
|
|
478
485
|
this.metaEtag = res.etag;
|
|
486
|
+
this.metaModifiedTime = res.modifiedTime;
|
|
479
487
|
}
|
|
480
488
|
}
|
|
481
489
|
async countTotalChanges() {
|
|
@@ -506,8 +514,8 @@ class DriveHandler {
|
|
|
506
514
|
const metaFile = await this.findFile('_meta.json');
|
|
507
515
|
if (!metaFile)
|
|
508
516
|
return;
|
|
509
|
-
//
|
|
510
|
-
if (metaFile.
|
|
517
|
+
// Use modifiedTime for polling as it's readable in projections
|
|
518
|
+
if (metaFile.modifiedTime !== this.metaModifiedTime) {
|
|
511
519
|
await this.load();
|
|
512
520
|
this.notifyListeners();
|
|
513
521
|
}
|