UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

370 lines • 15.6 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { ByteSpool } from "../../io/spool.js"; import { sha256Hex } from "../../utils/hash.js"; import { driveBase, driveUploadBase, googleDelete, googleGet, googleGetBytes, googleGetStream, googlePatch, googlePost, googleResumable, googleSendBytes, } from "./client.js"; // md5Checksum and headRevisionId are a file's token (driveFingerprint). const FIELDS = 'nextPageToken,' + 'files(id,name,mimeType,driveId,size,quotaBytesUsed,' + 'createdTime,modifiedTime,md5Checksum,headRevisionId,' + 'owners,capabilities/canEdit,parents)'; // A search across every corpus is answered best-effort, so Drive reports // whether it reached them all. The flag is only returned when asked for. const ALL_DRIVES_FIELDS = `incompleteSearch,${FIELDS}`; const DRIVE_FIELDS = 'nextPageToken,drives(id,name)'; // Rendered vfs filename suffixes; readdir emits only folders and these. const GoogleFileSuffix = Object.freeze({ GDOC: '.gdoc.json', GSHEET: '.gsheet.json', GSLIDE: '.gslide.json', GMAIL: '.gmail.json', }); export const MIME_TO_EXT = Object.freeze({ 'application/vnd.google-apps.document': GoogleFileSuffix.GDOC, 'application/vnd.google-apps.spreadsheet': GoogleFileSuffix.GSHEET, 'application/vnd.google-apps.presentation': GoogleFileSuffix.GSLIDE, }); export function listFiles(tm, opts = {}) { const folderId = opts.folderId ?? 'root'; const driveId = opts.driveId ?? null; const mimeType = opts.mimeType ?? null; const trashed = opts.trashed ?? false; const pageSize = opts.pageSize ?? 1000; const modifiedAfter = opts.modifiedAfter ?? null; const modifiedBefore = opts.modifiedBefore ?? null; const name = opts.name ?? null; const limit = opts.limit ?? null; const parts = [`'${folderId}' in parents`]; if (name !== null) parts.push(`name='${escapeQueryValue(name)}'`); if (mimeType !== null) parts.push(`mimeType='${mimeType}'`); if (!trashed) parts.push('trashed=false'); if (modifiedAfter !== null) parts.push(`modifiedTime >= '${modifiedAfter}'`); if (modifiedBefore !== null) parts.push(`modifiedTime < '${modifiedBefore}'`); return queryFiles(tm, parts.join(' and '), driveId, pageSize, limit); } /** * The untrashed children of several folders of one corpus, from one query * (`'a' in parents or 'b' in parents`), in the order a listing of each folder * returns them, stopping once `limit` files are in hand. Mirrors Python's * `list_children`. */ export function listChildren(tm, folderIds, driveId = null, pageSize = 1000, limit) { const parents = folderIds.map((id) => `'${escapeQueryValue(id)}' in parents`).join(' or '); return queryFiles(tm, `(${parents}) and trashed=false`, driveId, pageSize, limit); } /** * The files a query names, newest first, a page at a time. Mirrors Python's * `_query_files`. */ async function queryFiles(tm, q, driveId, pageSize, limit = null) { const files = []; let pageToken = null; for (;;) { const params = { q, fields: FIELDS, pageSize: limit === null ? pageSize : Math.min(pageSize, limit), orderBy: 'modifiedTime desc', }; if (driveId !== null) { params.corpora = 'drive'; params.driveId = driveId; params.includeItemsFromAllDrives = 'true'; params.supportsAllDrives = 'true'; } if (pageToken !== null) params.pageToken = pageToken; const url = `${driveBase(tm)}/files`; const data = (await googleGet(tm, url, params)); if (data.files !== undefined) for (const item of data.files) files.push(item); if (limit !== null && files.length >= limit) break; pageToken = data.nextPageToken ?? null; if (pageToken === null) break; } return files; } export async function listSharedDrives(tm, opts = {}) { const pageSize = opts.pageSize ?? 100; const drives = []; let pageToken = null; for (;;) { const params = { fields: DRIVE_FIELDS, pageSize, }; if (pageToken !== null) params.pageToken = pageToken; const url = `${driveBase(tm)}/drives`; const data = (await googleGet(tm, url, params)); if (data.drives !== undefined) for (const item of data.drives) drives.push(item); pageToken = data.nextPageToken ?? null; if (pageToken === null) break; } return drives; } // Searches every corpus the account can reach, so a document that lives in a // Shared Drive lists like one in My Drive. Drive answers an all-corpora // search best-effort and sets `incompleteSearch` when it gave up on one, // which is reported back rather than hidden: a caller that caches the listing // must not cache a short one as if it were the whole directory. export async function listAllFiles(tm, opts = {}) { const mimeType = opts.mimeType ?? null; const trashed = opts.trashed ?? false; const pageSize = opts.pageSize ?? 1000; const modifiedAfter = opts.modifiedAfter ?? null; const modifiedBefore = opts.modifiedBefore ?? null; const parts = []; if (mimeType !== null) parts.push(`mimeType='${mimeType}'`); if (!trashed) parts.push('trashed=false'); if (modifiedAfter !== null) parts.push(`modifiedTime >= '${modifiedAfter}'`); if (modifiedBefore !== null) parts.push(`modifiedTime < '${modifiedBefore}'`); const q = parts.length > 0 ? parts.join(' and ') : null; const files = []; let complete = true; let pageToken = null; for (;;) { const params = { fields: ALL_DRIVES_FIELDS, pageSize, orderBy: 'modifiedTime desc', // No driveId: allDrives is the union of My Drive and every shared // drive the account belongs to, so naming one would contradict it. corpora: 'allDrives', includeItemsFromAllDrives: 'true', supportsAllDrives: 'true', }; if (q !== null) params.q = q; if (pageToken !== null) params.pageToken = pageToken; const url = `${driveBase(tm)}/files`; const data = (await googleGet(tm, url, params)); if (data.files !== undefined) for (const item of data.files) files.push(item); if (data.incompleteSearch === true) complete = false; pageToken = data.nextPageToken ?? null; if (pageToken === null) break; } return { files, complete }; } /** * Ids of the files in `parentIds` Drive full-text search names, and whether * Drive searched every corpus it was asked for. Drive sorts no `fullText` * query, so none is asked for. */ export async function searchFiles(tm, text, parentIds, mimeType = null, pageSize = 1000) { const parts = [`fullText contains '${escapeQueryValue(text)}'`, 'trashed=false']; if (parentIds.length > 0) { parts.push(`(${parentIds.map((p) => `'${escapeQueryValue(p)}' in parents`).join(' or ')})`); } if (mimeType !== null) parts.push(`mimeType='${mimeType}'`); const q = parts.join(' and '); const ids = []; let complete = true; let pageToken = null; for (;;) { const params = { q, fields: 'nextPageToken,incompleteSearch,files(id)', pageSize, corpora: 'allDrives', includeItemsFromAllDrives: 'true', supportsAllDrives: 'true', }; if (pageToken !== null) params.pageToken = pageToken; const url = `${driveBase(tm)}/files`; const data = (await googleGet(tm, url, params)); for (const f of data.files ?? []) ids.push(f.id); if (data.incompleteSearch === true) complete = false; pageToken = data.nextPageToken ?? null; if (pageToken === null) break; } return { ids, complete }; } export async function downloadFile(tm, fileId, window) { return googleGetBytes(tm, mediaUrl(tm, fileId), window); } /** Stream a regular file from Drive as it downloads. */ export function downloadFileStream(tm, fileId) { return googleGetStream(tm, mediaUrl(tm, fileId)); } function mediaUrl(tm, fileId) { return `${driveBase(tm)}/files/${fileId}?alt=media&supportsAllDrives=true`; } /** A pinned revision's content (binary files only). */ export async function downloadRevision(tm, fileId, revisionId, window) { return googleGetBytes(tm, revisionUrl(tm, fileId, revisionId), window); } /** Stream a pinned revision's content as it downloads. */ export function downloadRevisionStream(tm, fileId, revisionId) { return googleGetStream(tm, revisionUrl(tm, fileId, revisionId)); } function revisionUrl(tm, fileId, revisionId) { return `${driveBase(tm)}/files/${fileId}/revisions/${revisionId}?alt=media`; } export async function deleteFile(tm, fileId) { const url = `${driveBase(tm)}/files/${fileId}?supportsAllDrives=true`; await googleDelete(tm, url); } export const FOLDER_MIME = 'application/vnd.google-apps.folder'; const ITEM_FIELDS = 'id,name,mimeType,driveId,size,quotaBytesUsed,createdTime,modifiedTime,md5Checksum,headRevisionId,parents,owners,trashed'; const DEFAULT_UPLOAD_MIME = 'application/octet-stream'; // Escape a value for a Drive API query string literal. function escapeQueryValue(value) { return value.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); } async function multipartRelated(metadata, data, mimeType) { const enc = new TextEncoder(); const meta = JSON.stringify(metadata); const parts = (await sha256Hex(enc.encode(meta))) + (await sha256Hex(data)); const boundary = (await sha256Hex(enc.encode(parts))).slice(0, 32); const head = enc.encode(`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n` + `${meta}\r\n--${boundary}\r\nContent-Type: ${mimeType}\r\n\r\n`); const tail = enc.encode(`\r\n--${boundary}--\r\n`); const body = new Uint8Array(head.length + data.length + tail.length); body.set(head, 0); body.set(data, head.length); body.set(tail, head.length + data.length); return { body, contentType: `multipart/related; boundary=${boundary}` }; } // What a change read names of each file: enough to place it on a Drive or app // mount and to tell a new file from an edited one. const CHANGE_FIELDS = 'nextPageToken,newStartPageToken,changes(changeType,fileId,removed,' + 'file(id,name,mimeType,driveId,createdTime,modifiedTime,parents,owners,trashed))'; /** * The changes page token at the present, which lists nothing. Mirrors Python * `start_page_token`. */ export async function startPageToken(tm) { const url = `${driveBase(tm)}/changes/startPageToken`; const data = (await googleGet(tm, url, { supportsAllDrives: 'true' })); return data.startPageToken; } /** * Every change since `pageToken` across the account's drives, and the token * the next read starts from. Mirrors Python `list_changes`. */ export async function listChanges(tm, pageToken) { const url = `${driveBase(tm)}/changes`; const found = []; let token = pageToken; for (;;) { const data = (await googleGet(tm, url, { pageToken: token, fields: CHANGE_FIELDS, pageSize: 1000, includeItemsFromAllDrives: 'true', supportsAllDrives: 'true', })); for (const item of data.changes ?? []) found.push(item); if (data.newStartPageToken !== undefined && data.newStartPageToken !== '') { return [found, data.newStartPageToken]; } token = String(data.nextPageToken); } } /** A file's metadata, the fields a listing names unless `fields` says others. */ export async function getFile(tm, fileId, fields = ITEM_FIELDS) { const url = `${driveBase(tm)}/files/${fileId}`; return (await googleGet(tm, url, { fields, supportsAllDrives: 'true', })); } /** The account's `storageQuota`, empty when Drive gives none. Mirrors Python `storage_quota`. */ export async function storageQuota(tm) { const about = (await googleGet(tm, `${driveBase(tm)}/about`, { fields: 'storageQuota', })); return about.storageQuota ?? {}; } export async function createFolder(tm, name, parentId) { const url = `${driveBase(tm)}/files?supportsAllDrives=true&fields=${ITEM_FIELDS}`; return (await googlePost(tm, url, { name, mimeType: FOLDER_MIME, parents: [parentId], })); } // Large spooled payloads use the resumable protocol. export async function uploadFile(tm, name, parentId, data, mimeType = DEFAULT_UPLOAD_MIME) { if (data instanceof ByteSpool) { if (data.size > 4 * 1024 * 1024) return (await googleResumable(tm, 'POST', `${driveUploadBase(tm)}/files`, { name, parents: [parentId] }, data, mimeType, { supportsAllDrives: 'true', fields: ITEM_FIELDS })); data = await data.read(0, data.size); } const { body, contentType } = await multipartRelated({ name, parents: [parentId] }, data, mimeType); const url = `${driveUploadBase(tm)}/files`; return (await googleSendBytes(tm, 'POST', url, body, contentType, { uploadType: 'multipart', supportsAllDrives: 'true', fields: ITEM_FIELDS, })); } export async function updateFileContent(tm, fileId, data, mimeType = DEFAULT_UPLOAD_MIME) { if (data instanceof ByteSpool) { if (data.size > 4 * 1024 * 1024) return (await googleResumable(tm, 'PATCH', `${driveUploadBase(tm)}/files/${fileId}`, {}, data, mimeType, { supportsAllDrives: 'true', fields: ITEM_FIELDS })); data = await data.read(0, data.size); } const url = `${driveUploadBase(tm)}/files/${fileId}`; return (await googleSendBytes(tm, 'PATCH', url, data, mimeType, { uploadType: 'media', supportsAllDrives: 'true', fields: ITEM_FIELDS, })); } // Patch file metadata (rename and/or move between parents). export async function patchFile(tm, fileId, opts = {}) { const params = { supportsAllDrives: 'true', fields: ITEM_FIELDS, }; if (opts.addParents !== undefined) params.addParents = opts.addParents; if (opts.removeParents !== undefined) params.removeParents = opts.removeParents; const url = `${driveBase(tm)}/files/${fileId}`; return (await googlePatch(tm, url, opts.body ?? {}, params)); } export async function copyFile(tm, fileId, name, parentId) { const url = `${driveBase(tm)}/files/${fileId}/copy?supportsAllDrives=true&fields=${ITEM_FIELDS}`; return (await googlePost(tm, url, { name, parents: [parentId] })); } //# sourceMappingURL=drive.js.map