@docstack/pouchdb-adapter-googledrive 0.1.5 → 0.1.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/adapter.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GoogleDriveAdapter = GoogleDriveAdapter;
4
4
  const drive_1 = require("./drive");
5
+ const pouchdb_adapter_utils_1 = require("pouchdb-adapter-utils");
6
+ const pouchdb_merge_1 = require("pouchdb-merge");
5
7
  /**
6
8
  * Schedule a function to run asynchronously.
7
9
  */
@@ -10,6 +12,50 @@ function nextTick(fn) {
10
12
  queueMicrotask(fn);
11
13
  }
12
14
  }
15
+ /** Substituted by DriveHandler.updateIndex() once the batch's change-log file
16
+ * actually has an id - lets adapter.ts build a complete IndexEntry (including
17
+ * pointers at revisions being written in *this* batch) before that file exists. */
18
+ const SELF_FILE = '__SELF__';
19
+ /** True when a newEdits write's parent rev isn't actually present in the tree
20
+ * (the caller claimed a rev that doesn't exist) - same check, same name,
21
+ * `pouchdb-adapter-native` uses (traced from pouchdb-adapter-utils's processDocs.js). */
22
+ function rootIsMissing(docInfo) {
23
+ return docInfo.metadata.rev_tree[0].ids[1].status === 'missing';
24
+ }
25
+ /**
26
+ * Builds a change's `changes` array.
27
+ *
28
+ * `style: 'all_docs'` - which is what `pouchdb-replication` asks for, and its default -
29
+ * means "list every leaf of the revision tree, not just the winner". Reporting only the
30
+ * winner hides conflict leaves from the changes feed, so they are never fetched, never
31
+ * pushed, and the two replicas quietly disagree about which revisions exist. The winner
32
+ * goes first, as CouchDB orders it.
33
+ *
34
+ * @param entry - The index entry for the document, carrying the serialized rev tree.
35
+ * @param style - The `style` the caller asked for; anything but `'all_docs'` yields the
36
+ * winning revision alone.
37
+ * @returns The `changes` array for the changes feed.
38
+ */
39
+ function buildChangesList(entry, style) {
40
+ const winner = [{ rev: entry.rev }];
41
+ if (style !== 'all_docs')
42
+ return winner;
43
+ try {
44
+ const leaves = (0, pouchdb_merge_1.collectLeaves)(JSON.parse(entry.tree));
45
+ if (!leaves || !leaves.length)
46
+ return winner;
47
+ const others = leaves
48
+ .map(leaf => leaf.rev)
49
+ .filter(rev => rev !== entry.rev)
50
+ .map(rev => ({ rev }));
51
+ return [...winner, ...others];
52
+ }
53
+ catch (e) {
54
+ // A tree that will not parse is a bigger problem than a missing conflict leaf;
55
+ // fall back to the winner rather than breaking the feed.
56
+ return winner;
57
+ }
58
+ }
13
59
  /**
14
60
  * TODO: Implement dynamic method signature, since PouchDB CRUD methods can be called as promise, callback or (?)
15
61
  */
@@ -140,9 +186,27 @@ function GoogleDriveAdapter(PouchDB) {
140
186
  if (!opts || typeof opts !== 'object') {
141
187
  opts = {};
142
188
  }
143
- // PouchDB sometimes asks for metadata only (revs, revs_info)
189
+ // PouchDB sometimes asks for metadata only (revs, revs_info)
144
190
  log('_get id:', id, 'opts:', JSON.stringify(opts));
145
- const promise = db.get(id).then(doc => {
191
+ // A specific non-winning rev was requested (e.g. to inspect a conflict
192
+ // via db.get(id, {rev, conflicts: true})'s follow-up fetch) - look it up
193
+ // among known conflict locations instead of always returning the current
194
+ // winner. The common case (no rev, or rev === the winner) is unaffected.
195
+ // api.get = api._get below means this replaces the whole public `get()`
196
+ // method (same "no _-prefixed hook, override the public method instead"
197
+ // situation @docstack/pouchdb-adapter-native's own revsDiff/bulkGet ran
198
+ // into) - AbstractPouchDB.prototype.get's own opts.conflicts handling
199
+ // (pouchdbMerge.collectConflicts(metadata)) is bypassed entirely, so it
200
+ // has to be done here instead.
201
+ const entry = db.getIndexEntry(id);
202
+ const fetchPromise = (async () => {
203
+ if (opts.rev && entry && entry.rev !== opts.rev) {
204
+ const loc = entry.conflictLocations && entry.conflictLocations[opts.rev];
205
+ return loc ? db.getRevisionBody(id, opts.rev, loc) : null;
206
+ }
207
+ return db.get(id);
208
+ })();
209
+ const promise = fetchPromise.then(doc => {
146
210
  if (!doc) {
147
211
  log('_get missing', id);
148
212
  const err = {
@@ -166,6 +230,11 @@ function GoogleDriveAdapter(PouchDB) {
166
230
  callback(null, result);
167
231
  return result;
168
232
  }
233
+ if (opts.conflicts && entry) {
234
+ const conflicts = (0, pouchdb_merge_1.collectConflicts)({ rev_tree: JSON.parse(entry.tree) });
235
+ if (conflicts.length)
236
+ doc._conflicts = conflicts;
237
+ }
169
238
  log('_get returning standard doc for:', id);
170
239
  // If only rev was requested? (Internal optimization)
171
240
  // PouchDB core handles this if we return the full doc.
@@ -272,32 +341,35 @@ function GoogleDriveAdapter(PouchDB) {
272
341
  api._bulkGet = function (opts, callback) {
273
342
  const docs = opts.docs;
274
343
  const ids = docs.map((d) => d.id);
275
- return db.getMulti(ids).then(results => {
276
- const response = {
277
- results: ids.map((id, i) => {
278
- const doc = results[i];
279
- const requestedRev = docs[i].rev;
280
- const entry = db.getIndexEntry(id);
281
- let docResult;
282
- if (!doc || (requestedRev && doc._rev !== requestedRev)) {
283
- docResult = {
284
- error: {
285
- status: 404,
286
- error: true,
287
- name: 'not_found',
288
- message: 'missing'
289
- }
290
- };
291
- }
292
- else {
293
- docResult = { ok: doc };
294
- }
295
- return {
296
- id,
297
- docs: [docResult]
344
+ return db.getMulti(ids).then(async (results) => {
345
+ const rows = await Promise.all(ids.map(async (id, i) => {
346
+ let doc = results[i];
347
+ const requestedRev = docs[i].rev;
348
+ const entry = db.getIndexEntry(id);
349
+ // Winning body didn't match the specific rev requested - it may
350
+ // still be a known conflict leaf, not necessarily missing.
351
+ if (requestedRev && (!doc || doc._rev !== requestedRev) && entry) {
352
+ const loc = entry.conflictLocations && entry.conflictLocations[requestedRev];
353
+ if (loc)
354
+ doc = await db.getRevisionBody(id, requestedRev, loc);
355
+ }
356
+ let docResult;
357
+ if (!doc || (requestedRev && doc._rev !== requestedRev)) {
358
+ docResult = {
359
+ error: {
360
+ status: 404,
361
+ error: true,
362
+ name: 'not_found',
363
+ message: 'missing'
364
+ }
298
365
  };
299
- })
300
- };
366
+ }
367
+ else {
368
+ docResult = { ok: doc };
369
+ }
370
+ return { id, docs: [docResult] };
371
+ }));
372
+ const response = { results: rows };
301
373
  if (callback)
302
374
  callback(null, response);
303
375
  return response;
@@ -308,79 +380,145 @@ function GoogleDriveAdapter(PouchDB) {
308
380
  });
309
381
  };
310
382
  api.bulkGet = api._bulkGet;
311
- // Bulk document operations
383
+ // Bulk document operations. Merges every write into a real pouchdb-merge
384
+ // rev tree (mirroring @docstack/pouchdb-adapter-native's own, already-proven
385
+ // _bulkDocs, traced from pouchdb-adapter-utils's processDocs.js/updateDoc.js)
386
+ // instead of blindly overwriting the index - the previous version had no
387
+ // conflict detection at all on the new_edits:false (replication) path, so
388
+ // two peers syncing a concurrent edit through this adapter would each just
389
+ // keep whichever write landed last, silently disagreeing with each other.
312
390
  api._bulkDocs = function (req, opts, callback) {
391
+ // `api.bulkDocs = api._bulkDocs` below means callers reach this directly and
392
+ // AbstractPouchDB.prototype.bulkDocs never runs, so its two normalizations
393
+ // have to happen here instead.
394
+ //
395
+ // First, the request shape: `db.bulkDocs([doc])` is the common call style and
396
+ // core turns the array into an envelope before any adapter sees it.
397
+ if (Array.isArray(req))
398
+ req = { docs: req };
399
+ // Second, `new_edits`, which arrives on either side. `pouchdb-replication`
400
+ // puts it on `req` (the CouchDB _bulk_docs body shape); `bulkDocs(docs,
401
+ // { new_edits: false })` and `put(doc, { new_edits: false })` - which core
402
+ // routes through `bulkDocs` - put it on `opts`. Core consults `opts` first and
403
+ // falls back to `req`; reading only one side silently mints fresh revisions
404
+ // for writes that were meant to land verbatim.
405
+ const newEdits = (opts && typeof opts === 'object' && 'new_edits' in opts)
406
+ ? opts.new_edits !== false
407
+ : req.new_edits !== false;
313
408
  const docs = req.docs;
314
- const results = [];
315
- // new_edits lives on `req` (the CouchDB _bulk_docs request body shape), not `opts`.
316
- // api.bulkDocs = api._bulkDocs below means callers reach this directly, bypassing
317
- // AbstractPouchDB.prototype.bulkDocs's req->opts normalization - so read it from req.
318
- const newEdits = req.new_edits !== false;
409
+ const results = new Array(docs.length);
410
+ const revsLimit = adapterOpts.revs_limit || 1000;
319
411
  const changes = [];
320
- // We need to validate revisions against Index
321
- // This does NOT require fetching bodies usually
322
- for (const doc of docs) {
412
+ for (let i = 0; i < docs.length; i++) {
413
+ const doc = docs[i];
323
414
  const id = doc._id;
324
- const seq = db.getNextSeq() + changes.length;
325
415
  const entry = db.getIndexEntry(id);
326
- if (doc._deleted) {
327
- if (!entry || entry.deleted) {
328
- results.push({
329
- ok: false,
330
- id,
331
- error: 'not_found',
332
- reason: 'missing'
333
- });
416
+ const docInfo = (0, pouchdb_adapter_utils_1.parseDoc)(Object.assign({}, doc), newEdits);
417
+ if (!docInfo || !docInfo.metadata) {
418
+ results[i] = Object.assign({ id }, docInfo);
419
+ continue;
420
+ }
421
+ let mergedTree;
422
+ let stemmedRevs;
423
+ let winning;
424
+ let winningDeleted;
425
+ let selfIsWinner;
426
+ let incomingRev;
427
+ let savedDocInfo = docInfo;
428
+ const conflictLocations = {};
429
+ if (!entry) {
430
+ // Brand new doc.
431
+ if (newEdits && rootIsMissing(docInfo)) {
432
+ results[i] = { ok: false, id, error: 'conflict', reason: 'Document update conflict' };
334
433
  continue;
335
434
  }
336
- // Check rev
337
- const oldRev = entry.rev || '0-0'; // Index has latest
338
- // If mismatch? PouchDB handles conflict logic before calling us sometimes?
339
- // But we should verify.
340
- // If doc._rev matches entry.rev, we are good.
341
- const revNum = parseInt(oldRev.split('-')[0], 10) + 1;
342
- const newRev = revNum + '-' + generateRevId();
343
- changes.push({
344
- seq,
345
- id,
346
- rev: newRev,
347
- deleted: true,
348
- timestamp: Date.now()
349
- });
350
- results.push({ ok: true, id, rev: newRev });
435
+ const merged = (0, pouchdb_merge_1.merge)([], docInfo.metadata.rev_tree[0], revsLimit);
436
+ mergedTree = merged.tree;
437
+ stemmedRevs = merged.stemmedRevs;
438
+ winning = (0, pouchdb_merge_1.winningRev)({ rev_tree: mergedTree });
439
+ winningDeleted = (0, pouchdb_merge_1.isDeleted)({ rev_tree: mergedTree }, winning);
440
+ incomingRev = docInfo.metadata.rev;
441
+ selfIsWinner = true; // an empty starting tree can only ever produce one leaf
351
442
  }
352
443
  else {
353
- let newRev;
354
- let savedDoc;
355
- if (newEdits) {
356
- const oldRev = entry?.rev || '0-0';
357
- const revNum = parseInt(oldRev.split('-')[0], 10) + 1;
358
- const revHash = generateRevId();
359
- newRev = revNum + '-' + revHash;
360
- savedDoc = Object.assign({}, doc, { _rev: newRev });
361
- if (doc._revisions) {
362
- savedDoc._revisions = {
363
- start: revNum,
364
- ids: [revHash, ...(doc._revisions.ids || [])]
365
- };
366
- if (savedDoc._revisions.ids.length > 500) {
367
- savedDoc._revisions.ids = savedDoc._revisions.ids.slice(0, 500);
368
- }
444
+ const existingTree = JSON.parse(entry.tree);
445
+ if ((0, pouchdb_merge_1.revExists)(existingTree, docInfo.metadata.rev) && !newEdits) {
446
+ // Replication redelivering a rev we already have - a no-op.
447
+ results[i] = { ok: true, id, rev: docInfo.metadata.rev };
448
+ continue;
449
+ }
450
+ const previousWinningRev = entry.rev;
451
+ const previouslyDeleted = !!entry.deleted;
452
+ let deleted = docInfo.metadata.deleted !== undefined ? docInfo.metadata.deleted : false;
453
+ const isRoot = /^1-/.test(docInfo.metadata.rev);
454
+ // Undeleting via a fresh newEdits put re-parents onto the tombstone
455
+ // rev instead of conflicting (CouchDB "resurrection").
456
+ if (previouslyDeleted && !deleted && newEdits && isRoot) {
457
+ const resurrected = Object.assign({}, docInfo.data, { _id: id, _rev: previousWinningRev });
458
+ const reparsed = (0, pouchdb_adapter_utils_1.parseDoc)(resurrected, newEdits);
459
+ if (!reparsed || !reparsed.metadata) {
460
+ results[i] = Object.assign({ id }, reparsed);
461
+ continue;
369
462
  }
463
+ savedDocInfo = reparsed;
464
+ deleted = savedDocInfo.metadata.deleted !== undefined ? savedDocInfo.metadata.deleted : false;
370
465
  }
371
- else {
372
- newRev = doc._rev;
373
- savedDoc = Object.assign({}, doc, { _rev: newRev });
466
+ const merged = (0, pouchdb_merge_1.merge)(existingTree, savedDocInfo.metadata.rev_tree[0], revsLimit);
467
+ const inConflict = newEdits &&
468
+ ((previouslyDeleted && deleted && merged.conflicts !== 'new_leaf') ||
469
+ (!previouslyDeleted && merged.conflicts !== 'new_leaf') ||
470
+ (previouslyDeleted && !deleted && merged.conflicts === 'new_branch'));
471
+ if (inConflict) {
472
+ results[i] = { ok: false, id, error: 'conflict', reason: 'Document update conflict' };
473
+ continue;
474
+ }
475
+ mergedTree = merged.tree;
476
+ stemmedRevs = merged.stemmedRevs;
477
+ winning = (0, pouchdb_merge_1.winningRev)({ rev_tree: mergedTree });
478
+ winningDeleted = (0, pouchdb_merge_1.isDeleted)({ rev_tree: mergedTree }, winning);
479
+ incomingRev = savedDocInfo.metadata.rev;
480
+ selfIsWinner = incomingRev === winning;
481
+ // Carry forward existing conflicts, minus anything just stemmed
482
+ // past revs_limit (pouchdb-adapter-native's own stemBodies
483
+ // handles the equivalent body cleanup for its own storage;
484
+ // compact() here does the same, see drive.ts).
485
+ if (entry.conflictLocations) {
486
+ for (const rev of Object.keys(entry.conflictLocations)) {
487
+ if (!stemmedRevs.includes(rev))
488
+ conflictLocations[rev] = entry.conflictLocations[rev];
489
+ }
490
+ }
491
+ if (selfIsWinner) {
492
+ // The old winner becomes a conflict leaf, unless it just got stemmed.
493
+ if (previousWinningRev !== winning && !stemmedRevs.includes(previousWinningRev)) {
494
+ conflictLocations[previousWinningRev] = entry.location;
495
+ }
496
+ }
497
+ else if (!stemmedRevs.includes(incomingRev)) {
498
+ conflictLocations[incomingRev] = { fileId: SELF_FILE };
374
499
  }
375
- changes.push({
376
- seq,
377
- id,
378
- rev: newRev,
379
- doc: savedDoc,
380
- timestamp: Date.now()
381
- });
382
- results.push({ ok: true, id, rev: newRev });
383
500
  }
501
+ const seq = db.getNextSeq() + changes.length;
502
+ const savedDoc = Object.assign({}, savedDocInfo.data, { _id: id, _rev: incomingRev });
503
+ const nextIndexEntry = {
504
+ tree: JSON.stringify(mergedTree),
505
+ rev: winning,
506
+ deleted: winningDeleted,
507
+ location: selfIsWinner ? { fileId: SELF_FILE } : entry.location,
508
+ };
509
+ if (Object.keys(conflictLocations).length > 0) {
510
+ nextIndexEntry.conflictLocations = conflictLocations;
511
+ }
512
+ changes.push({
513
+ seq,
514
+ id,
515
+ rev: incomingRev,
516
+ deleted: savedDocInfo.metadata.deleted,
517
+ doc: savedDoc,
518
+ timestamp: Date.now(),
519
+ nextIndexEntry,
520
+ });
521
+ results[i] = { ok: true, id, rev: incomingRev };
384
522
  }
385
523
  log('_bulkDocs flushing', changes.length, 'changes');
386
524
  // Append changes to log
@@ -422,11 +560,13 @@ function GoogleDriveAdapter(PouchDB) {
422
560
  const change = {
423
561
  id: id,
424
562
  seq: entry.seq,
425
- changes: [{ rev: entry.rev }]
563
+ changes: buildChangesList(entry, opts.style)
426
564
  };
427
565
  if (opts.include_docs) {
428
- db.get(id).then(doc => {
429
- change.doc = doc;
566
+ // Same tombstone rule as the initial pass: a `null` body
567
+ // makes a filtered replication drop the deletion.
568
+ loadChangeBodies([{ id, entry }]).then(bodies => {
569
+ change.doc = bodies[id];
430
570
  if (opts.onChange)
431
571
  opts.onChange(change);
432
572
  lastSeq = Math.max(lastSeq, change.seq);
@@ -442,37 +582,91 @@ function GoogleDriveAdapter(PouchDB) {
442
582
  };
443
583
  cancelLive = db.onChange(liveListener);
444
584
  }
585
+ /**
586
+ * Fetches the document bodies for a whole batch in one pass.
587
+ *
588
+ * `getMulti` groups ids by the file that holds them, so a batch drawn from a
589
+ * single change-log file costs one download rather than one per document -
590
+ * which is what a serial `get()` per change was costing. This path is only
591
+ * ever taken because something asked for `include_docs`, and the one thing
592
+ * that always asks is a filtered replication: `pouchdb-replication` forces
593
+ * `include_docs: true` so it can run its filter over `change.doc`.
594
+ *
595
+ * Deleted documents have no body to fetch, and a `null` there is not
596
+ * harmless: `pouchdb-replication`'s `filterChange` substitutes `{}` for a
597
+ * missing `change.doc`, and a filter given `{}` has no id to judge, so it
598
+ * drops the change. A filtered replication would then never propagate a
599
+ * deletion. Emit the tombstone the rest of PouchDB expects instead.
600
+ */
601
+ async function loadChangeBodies(batch) {
602
+ const bodies = {};
603
+ if (!batch.length)
604
+ return bodies;
605
+ const tombstone = (row) => ({
606
+ _id: row.id,
607
+ _rev: row.entry.rev,
608
+ _deleted: true
609
+ });
610
+ const live = batch.filter(row => !row.entry.deleted);
611
+ for (const row of batch) {
612
+ if (row.entry.deleted)
613
+ bodies[row.id] = tombstone(row);
614
+ }
615
+ if (!live.length)
616
+ return bodies;
617
+ try {
618
+ const docs = await db.getMulti(live.map(row => row.id));
619
+ live.forEach((row, index) => {
620
+ bodies[row.id] = docs[index] || tombstone(row);
621
+ });
622
+ }
623
+ catch (e) {
624
+ log('_changes include_docs error', e);
625
+ for (const row of live)
626
+ bodies[row.id] = tombstone(row);
627
+ }
628
+ return bodies;
629
+ }
445
630
  // Process initial changes
446
631
  async function processChangesAsync() {
447
632
  log('_changes processing since', since, 'limit', limit, 'live', !!opts.live);
448
633
  const keys = await db.getIndexKeys();
449
- let processed = 0;
450
- for (const id of keys) {
451
- if (complete || processed >= limit)
634
+ // The index is a plain object keyed by document id, so iterating it
635
+ // yields insertion order, not sequence order. Replication checkpoints on
636
+ // the highest seq in each batch and `limit` makes every batch a partial
637
+ // one, so an unordered batch can checkpoint past a change it never
638
+ // emitted - and that change is then never replicated again. Order by seq
639
+ // before the batch is cut.
640
+ const pending = keys
641
+ .filter((id) => !id.startsWith('_local/'))
642
+ .map((id) => ({ id, entry: db.getIndexEntry(id) }))
643
+ .filter((row) => Boolean(row.entry) && row.entry.seq > since)
644
+ .sort((a, b) => a.entry.seq - b.entry.seq);
645
+ // `descending` reverses the order the changes come back in. Its `since`
646
+ // semantics are CouchDB's (walk down from the given seq) and are not
647
+ // implemented here; replication never asks for it.
648
+ if (opts.descending)
649
+ pending.reverse();
650
+ // Cut the batch before fetching anything: `include_docs` costs a download
651
+ // per *file*, and there is no reason to pay it for changes beyond `limit`.
652
+ const batch = pending.slice(0, limit);
653
+ const bodies = opts.include_docs
654
+ ? await loadChangeBodies(batch)
655
+ : null;
656
+ for (const { id, entry } of batch) {
657
+ if (complete)
452
658
  break;
453
- if (id.startsWith('_local/'))
454
- continue;
455
- const entry = db.getIndexEntry(id);
456
- if (!entry || entry.seq <= since)
457
- continue;
458
659
  const change = {
459
660
  id: id,
460
661
  seq: entry.seq,
461
- changes: [{ rev: entry.rev }]
662
+ changes: buildChangesList(entry, opts.style)
462
663
  };
463
- if (opts.include_docs) {
464
- try {
465
- change.doc = await db.get(id);
466
- }
467
- catch (e) {
468
- log('_changes include_docs error', e);
469
- }
470
- }
664
+ if (bodies)
665
+ change.doc = bodies[id];
471
666
  if (opts.onChange)
472
667
  opts.onChange(change);
473
668
  if (returnDocs)
474
669
  results.push(change);
475
- processed++;
476
670
  lastSeq = Math.max(lastSeq, entry.seq);
477
671
  }
478
672
  // ✅ Call opts.complete() ONLY for non-live modes
@@ -521,14 +715,11 @@ function GoogleDriveAdapter(PouchDB) {
521
715
  if (!entry) {
522
716
  return callback({ status: 404, error: true, name: 'not_found', message: 'missing' });
523
717
  }
524
- // Return a minimal tree based on the known winning revision
525
- const revNum = parseInt(entry.rev.split('-')[0], 10);
526
- const revHash = entry.rev.split('-')[1];
527
- const revTree = [{
528
- pos: revNum,
529
- ids: [revHash, { status: 'available' }, []]
530
- }];
531
- callback(null, revTree);
718
+ // The real tree, as maintained by _bulkDocs's merge() calls - not a
719
+ // synthesized single-leaf stand-in. This is what makes PouchDB core's
720
+ // default revsDiff (which walks this) actually see conflicting revisions
721
+ // instead of only ever knowing about "whatever's currently winning".
722
+ callback(null, JSON.parse(entry.tree));
532
723
  };
533
724
  api._close = function (callback) {
534
725
  db.stopPolling();
package/lib/client.d.ts CHANGED
@@ -25,11 +25,13 @@ export declare class GoogleDriveClient {
25
25
  id: string;
26
26
  etag: string;
27
27
  modifiedTime: string;
28
+ md5Checksum?: string;
28
29
  }>;
29
30
  updateFile(fileId: string, content: string, expectedEtag?: string): Promise<{
30
31
  id: string;
31
32
  etag: string;
32
33
  modifiedTime: string;
34
+ md5Checksum?: string;
33
35
  }>;
34
36
  private extractEtag;
35
37
  deleteFile(fileId: string): Promise<void>;
package/lib/client.js CHANGED
@@ -133,7 +133,7 @@ class GoogleDriveClient {
133
133
  };
134
134
  }
135
135
  const multipartBody = this.buildMultipart(metadata, content, mimeType);
136
- const res = await this.fetch(`${this.uploadUrl}?uploadType=multipart&fields=id,modifiedTime`, {
136
+ const res = await this.fetch(`${this.uploadUrl}?uploadType=multipart&fields=id,modifiedTime,md5Checksum`, {
137
137
  method: 'POST',
138
138
  headers: {
139
139
  'Content-Type': `multipart/related; boundary=${multipartBody.boundary}`
@@ -144,13 +144,14 @@ class GoogleDriveClient {
144
144
  return {
145
145
  id: data.id,
146
146
  etag: this.extractEtag(res, data),
147
- modifiedTime: data.modifiedTime || res.headers.get('Last-Modified') || ''
147
+ modifiedTime: data.modifiedTime || res.headers.get('Last-Modified') || '',
148
+ md5Checksum: data.md5Checksum
148
149
  };
149
150
  }
150
151
  async updateFile(fileId, content, expectedEtag) {
151
152
  // Update content (media) usually, but sometimes meta?
152
153
  // In our usage (saveMeta), we update body.
153
- const res = await this.fetch(`${this.uploadUrl}/${fileId}?uploadType=media&fields=id,modifiedTime`, {
154
+ const res = await this.fetch(`${this.uploadUrl}/${fileId}?uploadType=media&fields=id,modifiedTime,md5Checksum`, {
154
155
  method: 'PATCH',
155
156
  headers: expectedEtag ? { 'If-Match': `"${expectedEtag}"`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' },
156
157
  body: content
@@ -159,7 +160,8 @@ class GoogleDriveClient {
159
160
  return {
160
161
  id: data.id,
161
162
  etag: this.extractEtag(res, data),
162
- modifiedTime: data.modifiedTime || res.headers.get('Last-Modified') || ''
163
+ modifiedTime: data.modifiedTime || res.headers.get('Last-Modified') || '',
164
+ md5Checksum: data.md5Checksum
163
165
  };
164
166
  }
165
167
  extractEtag(res, data) {