Agent Sandbox Registry

awesome-providers

opensandbox/src/files.mjs

无中文版

opensandbox/src/files.mjs

// files.* methods (execd `/files` and `/directories`).

import { RpcError, CODES, badParams, unsupported } from "./errors.mjs";
import { fileEntry, modeDigits } from "./convert.mjs";
import { buildUpload } from "./multipart.mjs";
import { sandboxId } from "./sandbox.mjs";

/** A single EPP line is capped at 8 MiB and base64 adds a third. */
export const MAX_INLINE_BYTES = 4 * 1024 * 1024;

const pathOf = (params, key = "path") => {
  const value = params[key];
  if (typeof value !== "string" || value === "") throw badParams(`Missing parameter '${key}'.`);
  return value;
};

const fileOp = (ctx, id, method, path, opts = {}) =>
  ctx.osb.execd(id, method, path, { signal: ctx.signal, what: "file", timeoutMs: Math.max(ctx.cfg.httpTimeoutMs, 60_000), ...opts });

async function statOf(ctx, id, path) {
  const json = await ctx.osb.execdJson(id, "GET", "/files/info", {
    query: [["path", path]],
    signal: ctx.signal,
    what: "file",
    operation: `stat ${path}`,
  });
  const info = json?.[path] ?? Object.values(json ?? {})[0];
  if (!info) throw new RpcError(CODES.NOT_FOUND, `No such file or directory: ${path}`, { sdk_code: "E4001" });
  return info;
}

const exists = async (ctx, id, path) => {
  try {
    await statOf(ctx, id, path);
    return true;
  } catch (err) {
    if (err.code === CODES.NOT_FOUND) return false;
    throw err;
  }
};

async function removeAny(ctx, id, path, info) {
  const isDir = info.type === "directory";
  await fileOp(ctx, id, "DELETE", isDir ? "/directories" : "/files", {
    query: [["path", path]],
    idempotent: false,
    operation: `remove ${path}`,
  });
}

export const methods = {
  async "files.read"(params, ctx) {
    const id = sandboxId(params);
    const path = pathOf(params);
    const info = await statOf(ctx, id, path);
    if (info.type === "directory") {
      throw new RpcError(CODES.BAD_PARAMS, `'${path}' is a directory, not a file.`, { sdk_code: "E4000" });
    }
    const total = Number(info.size ?? 0);
    const offset = Math.max(Number(params.offset) || 0, 0);
    const wanted = params.length === undefined || params.length === null ? total - offset : Number(params.length);
    const length = Math.max(Math.min(wanted, total - offset), 0);
    if (length > MAX_INLINE_BYTES) {
      throw badParams(
        `'${path}' has ${length} bytes to read, above the ${MAX_INLINE_BYTES}-byte limit of one EPP message.`,
        { suggestion: "Pass offset/length to read it in chunks.", total_size: total },
      );
    }
    const headers = {};
    if (offset > 0 || params.length !== undefined) {
      if (length === 0) return { content_b64: "", size: 0, total_size: total, offset, eof: offset >= total };
      headers.Range = `bytes=${offset}-${offset + length - 1}`;
    }
    const { buffer } = await fileOp(ctx, id, "GET", "/files/download", { query: [["path", path]], headers, operation: `read ${path}` });
    return { content_b64: buffer.toString("base64"), size: buffer.length, total_size: total, offset, eof: offset + buffer.length >= total };
  },

  async "files.write"(params, ctx) {
    const id = sandboxId(params);
    const path = pathOf(params);
    const content = Buffer.from(String(params.content_b64 ?? ""), "base64");
    const { body, contentType } = buildUpload({ path, mode: modeDigits(params.mode), content });
    await fileOp(ctx, id, "POST", "/files/upload", { rawBody: body, contentType, idempotent: false, operation: `write ${path}` });
    return { size: content.length };
  },

  async "files.list"(params, ctx) {
    const id = sandboxId(params);
    const path = pathOf(params);
    const depth = Math.max(Number(params.depth) || 1, 1);
    const json = await ctx.osb.execdJson(id, "GET", "/directories/list", {
      query: [["path", path], ["depth", String(depth)]],
      signal: ctx.signal,
      what: "file",
      operation: `list ${path}`,
    });
    return { entries: (Array.isArray(json) ? json : []).map(fileEntry) };
  },

  async "files.stat"(params, ctx) {
    return fileEntry(await statOf(ctx, sandboxId(params), pathOf(params)));
  },

  async "files.mkdir"(params, ctx) {
    const path = pathOf(params);
    const mode = modeDigits(params.mode) ?? 755;
    await fileOp(ctx, sandboxId(params), "POST", "/directories", { body: { [path]: { mode } }, idempotent: false, operation: `mkdir ${path}` });
    return {};
  },

  async "files.remove"(params, ctx) {
    const id = sandboxId(params);
    const path = pathOf(params);
    await removeAny(ctx, id, path, await statOf(ctx, id, path));
    return {};
  },

  async "files.move"(params, ctx) {
    const id = sandboxId(params);
    const source = pathOf(params, params.source !== undefined ? "source" : "src");
    const destination = pathOf(params, params.destination !== undefined ? "destination" : "dest");
    await statOf(ctx, id, source);
    let existing;
    try {
      existing = await statOf(ctx, id, destination);
    } catch (err) {
      if (err.code !== CODES.NOT_FOUND) throw err;
    }
    if (existing) {
      if (params.overwrite !== true) throw badParams(`Destination '${destination}' already exists.`, { suggestion: "Pass overwrite=true to replace it." });
      await removeAny(ctx, id, destination, existing);
    }
    await fileOp(ctx, id, "POST", "/files/mv", { body: [{ src: source, dest: destination }], idempotent: false, operation: `move ${source}` });
    return {};
  },

  /** Extension (not in the EPP/1 table): recursive glob. */
  async "files.search"(params, ctx) {
    const id = sandboxId(params);
    const root = pathOf(params);
    const json = await ctx.osb.execdJson(id, "GET", "/files/search", {
      query: [["path", root], ["pattern", String(params.pattern ?? "**")]],
      signal: ctx.signal,
      what: "file",
      operation: `search ${root}`,
    });
    let entries = (Array.isArray(json) ? json : []).map(fileEntry);
    if (params.type === "file" || params.type === "directory") entries = entries.filter((e) => e.type === params.type);
    if (Number.isInteger(params.max_depth) && params.max_depth > 0) {
      const base = root.replace(/\/+$/, "");
      entries = entries.filter((e) => e.path.slice(base.length).split("/").filter(Boolean).length <= params.max_depth);
    }
    return { entries };
  },

  /** Extension: change mode/owner. */
  async "files.chmod"(params, ctx) {
    const path = pathOf(params);
    const spec = { mode: modeDigits(params.mode) ?? 644 };
    if (params.owner) spec.owner = String(params.owner);
    if (params.group) spec.group = String(params.group);
    await fileOp(ctx, sandboxId(params), "POST", "/files/permissions", { body: { [path]: spec }, idempotent: false, operation: `chmod ${path}` });
    return {};
  },

  async "files.watch"() {
    throw unsupported("files.watch", "OpenSandbox's execd has no file-watch endpoint.");
  },
  async "files.transfer"() {
    throw unsupported("files.transfer", "OpenSandbox has no signed upload/download URLs.");
  },
  async "files.upload_url"() {
    throw unsupported("files.transfer", "OpenSandbox has no signed upload/download URLs.");
  },
  async "files.download_url"() {
    throw unsupported("files.transfer", "OpenSandbox has no signed upload/download URLs.");
  },
};

export { exists };