@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
370 lines • 15.6 kB
JavaScript
// ========= 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