@open-webapp/drive-sync 0.6.0 → 0.7.1
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/SPEC.md +3 -1
- package/dist/files.js +15 -7
- package/dist/testing/driveFake.d.ts +16 -0
- package/dist/testing/driveFake.js +5 -1
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/SPEC.md
CHANGED
|
@@ -50,7 +50,7 @@ Files implementing the surface: `index.ts` (factory + `ProjectHandle`/`FilesHand
|
|
|
50
50
|
|
|
51
51
|
`getAccessToken()` is the one deliberate exception to `Connection` never exposing secret material (types.ts): it exists solely so an app can feed the token to Google Picker (`setOAuthToken()`), which runs outside this library's control and has no other way to read it. Reuses a cached token while it has more than 5 minutes left; otherwise acquires one (interactive by default, since callers use this to drive a UI the user is actively interacting with).
|
|
52
52
|
|
|
53
|
-
## 2. The
|
|
53
|
+
## 2. The 41 resolved design decisions
|
|
54
54
|
|
|
55
55
|
**Bugs fixed (both source apps carried these):**
|
|
56
56
|
|
|
@@ -107,6 +107,8 @@ Files implementing the surface: `index.ts` (factory + `ProjectHandle`/`FilesHand
|
|
|
107
107
|
|
|
108
108
|
41. **Envelope error mapping, opaque `sig`, no schema bump** — `envelope.ts` maps exchange failures onto typed reauth reasons: `410` → clear conn+token+envelope, then `NeedsReauthError` (`reason: 'refresh_token_revoked'`, surfaced internally as the distinguishable `EnvelopeRevokedError` subclass); `502` / other `5xx` / network throw / unparseable body → up to two retries (500ms, 1500ms) then `NeedsReauthError` (`reason: 'exchange_unavailable'`); other non-2xx (`400`/`401`/`404`/`403`/`409`) → `NeedsReauthError` (`reason: 'exchange_failed'`), no retry. The envelope's `sig` is a server signature that is **never** verified client-side — the structure is treated as fully opaque and only `payload` is read. The `envelope` key is added to the existing `auth` store with **no IndexedDB version bump** (still version 1).
|
|
109
109
|
|
|
110
|
+
42. **`list()` passes through `thumbnailLink` + `imageMediaMetadata`, unfiltered and verbatim** — `files.ts`'s `list()` extends the same `fields` mask touched in #36's `modifiedTime` change to also request `thumbnailLink` and `imageMediaMetadata(width,height,rotation)`; `FileRef` (`types.ts`) gains three optional fields — `mimeType?` (already fetched, previously just untyped), `thumbnailLink?`, and `imageMediaMetadata?: { width?; height?; rotation? }` — all `fields`-gated and may be absent on older or partial responses. `list()` stays unfiltered: it returns every file of any MIME type with no `image/` check and no opt-in flag, and `thumbnailLink` is passed through exactly as Drive returns it — no blob fetch, no URL rewrite, no `=s220` size munging, and no `files.thumbnail()` helper. Caveat: `thumbnailLink` is a short-lived URL (good for only ~hours) that can require the browser to be carrying Google auth context for the file's owning account, so a cross-origin bare `<img src>` may 403; rendering is the consuming app's responsibility, and it can fall back to `getAccessToken()` + fetch-to-blob itself. (Label is `42` though this is only the 41st entry — the section carries a duplicate `7.` label and a merged `25–27.` entry, so the labels have always run one ahead of the item count; no existing entry is renumbered.)
|
|
111
|
+
|
|
110
112
|
## 3. Storage layout
|
|
111
113
|
|
|
112
114
|
Each project gets its own IndexedDB database: **`owa-drive-{appId}-{projectId}`**, version 1, containing one object store, `auth` (`storage.ts`). The store holds up to three keys (`conn`/`token` always; `envelope` only in server-facilitated token-exchange mode):
|
package/dist/files.js
CHANGED
|
@@ -221,13 +221,19 @@ export async function write(opts) {
|
|
|
221
221
|
// and inserts a correct multipart boundary (never hand-rolled by us) and
|
|
222
222
|
// exposes the resulting `Content-Type: multipart/form-data; boundary=...`
|
|
223
223
|
// header, which we then forward explicitly alongside the serialized body.
|
|
224
|
-
// This also makes the request body a
|
|
225
|
-
// driveFetch, so environments (e.g. test fakes) that read the body
|
|
226
|
-
//
|
|
227
|
-
//
|
|
224
|
+
// This also makes the request body a concrete buffer by the time it reaches
|
|
225
|
+
// driveFetch, so environments (e.g. test fakes) that read the body without
|
|
226
|
+
// re-driving a real network stack see the fully encoded multipart payload
|
|
227
|
+
// rather than an opaque FormData object.
|
|
228
|
+
//
|
|
229
|
+
// The body is forwarded as an ArrayBuffer, never a string: a multipart body
|
|
230
|
+
// that carries a binary media part (a pasted PNG/JPEG) is not valid UTF-8,
|
|
231
|
+
// so `serialized.text()` would replace every non-UTF-8 byte with U+FFFD and
|
|
232
|
+
// fetch would then re-encode that lossy string, storing a corrupted blob on
|
|
233
|
+
// Drive. `arrayBuffer()` preserves the bytes exactly.
|
|
228
234
|
const serialized = new Request('https://example.invalid/', { method: 'POST', body: form });
|
|
229
235
|
const multipartContentType = serialized.headers.get('content-type') ?? undefined;
|
|
230
|
-
const multipartBody = await serialized.
|
|
236
|
+
const multipartBody = await serialized.arrayBuffer();
|
|
231
237
|
const res = await driveFetch({
|
|
232
238
|
appId: opts.appId,
|
|
233
239
|
projectId: opts.projectId,
|
|
@@ -302,8 +308,10 @@ export async function list(opts) {
|
|
|
302
308
|
// `version` comes back so the name-resolution path in write() can run its
|
|
303
309
|
// staleness check without a follow-up metadata fetch. `modifiedTime` is
|
|
304
310
|
// also requested so callers can get a last-modified timestamp per file
|
|
305
|
-
// without an extra round trip.
|
|
306
|
-
|
|
311
|
+
// without an extra round trip. `thumbnailLink` +
|
|
312
|
+
// `imageMediaMetadata(width,height,rotation)` are also requested so callers
|
|
313
|
+
// can render/orient low-res image thumbnails without an extra round trip.
|
|
314
|
+
const url = `${DRIVE_BASE}/files?q=${encodeURIComponent(q)}&fields=${encodeURIComponent('files(id,name,mimeType,version,modifiedTime,thumbnailLink,imageMediaMetadata(width,height,rotation))')}`;
|
|
307
315
|
const res = await driveFetch({
|
|
308
316
|
appId: opts.appId,
|
|
309
317
|
projectId: opts.projectId,
|
|
@@ -31,6 +31,22 @@ export interface DriveFakeFile {
|
|
|
31
31
|
* real Drive's `fields`-gated behavior).
|
|
32
32
|
*/
|
|
33
33
|
modifiedTime?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Drive's CDN thumbnail URL; optional so tests may seed files without it
|
|
36
|
+
* (omitted from the fake's response in that case, matching real Drive's
|
|
37
|
+
* `fields`-gated behavior).
|
|
38
|
+
*/
|
|
39
|
+
thumbnailLink?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Drive's image metadata (dimensions/rotation); optional so tests may seed
|
|
42
|
+
* files without it (omitted from the fake's response in that case, matching
|
|
43
|
+
* real Drive's `fields`-gated behavior).
|
|
44
|
+
*/
|
|
45
|
+
imageMediaMetadata?: {
|
|
46
|
+
width?: number;
|
|
47
|
+
height?: number;
|
|
48
|
+
rotation?: number;
|
|
49
|
+
};
|
|
34
50
|
}
|
|
35
51
|
export interface DriveFakePermission {
|
|
36
52
|
id: string;
|
|
@@ -105,7 +105,9 @@ async function readBodyText(init) {
|
|
|
105
105
|
return '';
|
|
106
106
|
if (typeof body === 'string')
|
|
107
107
|
return body;
|
|
108
|
-
if (body instanceof
|
|
108
|
+
if (body instanceof ArrayBuffer)
|
|
109
|
+
return new TextDecoder().decode(body);
|
|
110
|
+
if (ArrayBuffer.isView(body))
|
|
109
111
|
return new TextDecoder().decode(body);
|
|
110
112
|
if (typeof body.text === 'function')
|
|
111
113
|
return await body.text();
|
|
@@ -149,6 +151,8 @@ export function createDriveFake() {
|
|
|
149
151
|
parents: f.parents,
|
|
150
152
|
version: String(f.version ?? 1),
|
|
151
153
|
modifiedTime: f.modifiedTime,
|
|
154
|
+
thumbnailLink: f.thumbnailLink,
|
|
155
|
+
imageMediaMetadata: f.imageMediaMetadata,
|
|
152
156
|
};
|
|
153
157
|
}
|
|
154
158
|
async function handleFilesList(url) {
|
package/dist/types.d.ts
CHANGED
|
@@ -59,6 +59,16 @@ export interface FileRef {
|
|
|
59
59
|
version?: string;
|
|
60
60
|
/** Drive's last-modified timestamp (RFC3339), when the call requested it. */
|
|
61
61
|
modifiedTime?: string;
|
|
62
|
+
/** Drive MIME type, when the call requested it. */
|
|
63
|
+
mimeType?: string;
|
|
64
|
+
/** Short-lived Drive thumbnail URL, when the call requested it and Drive supplied one; may be absent. */
|
|
65
|
+
thumbnailLink?: string;
|
|
66
|
+
/** Pixel dimensions (+ `rotation`, 0/90/180/270) for image files, when Drive supplied them. */
|
|
67
|
+
imageMediaMetadata?: {
|
|
68
|
+
width?: number;
|
|
69
|
+
height?: number;
|
|
70
|
+
rotation?: number;
|
|
71
|
+
};
|
|
62
72
|
}
|
|
63
73
|
/**
|
|
64
74
|
* Sync state of one file relative to what this client last restored.
|