// Google Drive AppData helpers. Files written to the appDataFolder space are // invisible to the user in the regular Drive UI and only readable by the // OAuth client (i.e. *this* app's client_id) that created them. // // 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. export async function findAppDataFile(token, name) { const url = `${DRIVE}/files?spaces=appDataFolder` + `&q=${encodeURIComponent(`name = '${name.replace(/'/g, "\\'")}'`)}` + `&fields=files(id,name,modifiedTime)`; const res = await driveFetch(token, url); const { files } = await res.json(); if (!files || files.length === 0) return null; // If somehow multiple, prefer the most recent. files.sort((a, b) => (b.modifiedTime || "").localeCompare(a.modifiedTime || "")); 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 JSON blob to appDataFolder. If fileId is given, // updates that file (preserving parents); otherwise creates a new file. export async function uploadAppDataJson(token, name, content, fileId = null) { const boundary = "goostr-" + Math.random().toString(36).slice(2); const metadata = fileId ? { name } : { name, parents: ["appDataFolder"] }; 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: application/json\r\n\r\n` + `${content}\r\n` + `--${boundary}--`; const url = fileId ? `${UPLOAD}/files/${fileId}?uploadType=multipart` : `${UPLOAD}/files?uploadType=multipart`; 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, ... } }