// Google Drive helpers. Files written to the appDataFolder space are invisible // to the user in the regular Drive UI and only readable by this app's Google // Cloud project. // // Docs: https://developers.google.com/drive/api/guides/appdata const DRIVE = "https://www.googleapis.com/drive/v3"; const UPLOAD = "https://www.googleapis.com/upload/drive/v3"; async function driveFetch(token, url, init = {}) { const res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...(init.headers || {}), }, }); if (!res.ok) { const text = await res.text(); throw new Error(`Drive API ${res.status}: ${text.slice(0, 300)}`); } return res; } // Find a file by exact name inside the appDataFolder. Returns the file id, or // null if not found. If several exist (two devices signed up at the same // moment), the OLDEST wins, so every device converges on the same key. export async function findAppDataFile(token, name) { const url = `${DRIVE}/files?spaces=appDataFolder` + `&q=${encodeURIComponent(`name = '${name.replace(/'/g, "\\'")}'`)}` + `&fields=files(id,createdTime)`; const res = await driveFetch(token, url); const { files } = await res.json(); if (!files || files.length === 0) return null; files.sort((a, b) => (a.createdTime || "").localeCompare(b.createdTime || "")); return files[0].id; } export async function downloadFile(token, fileId) { const res = await driveFetch(token, `${DRIVE}/files/${fileId}?alt=media`); return res.text(); } // Multipart upload of a small text blob. If fileId is given, replaces that // file's content; otherwise creates a new file under `parents` // (["appDataFolder"] for the hidden key file, omit for My Drive). export async function uploadFile(token, { name, content, mimeType = "application/json", parents, fileId = null }) { const boundary = "goostr-" + Math.random().toString(36).slice(2); const metadata = fileId ? { name } : { name, mimeType, ...(parents ? { parents } : {}) }; const body = `--${boundary}\r\n` + `Content-Type: application/json; charset=UTF-8\r\n\r\n` + `${JSON.stringify(metadata)}\r\n` + `--${boundary}\r\n` + `Content-Type: ${mimeType}\r\n\r\n` + `${content}\r\n` + `--${boundary}--`; const url = fileId ? `${UPLOAD}/files/${fileId}?uploadType=multipart` : `${UPLOAD}/files?uploadType=multipart&fields=id,webViewLink`; const res = await driveFetch(token, url, { method: fileId ? "PATCH" : "POST", headers: { "Content-Type": `multipart/related; boundary=${boundary}` }, body, }); return res.json(); } export async function fetchUserInfo(token) { const res = await driveFetch(token, "https://www.googleapis.com/oauth2/v3/userinfo"); return res.json(); // { sub, name, picture, email, ... } }