b058179177
- Files: right-click a sidebar workspace (Open, Copy path) or home (Open, Open Trash), a path crumb (Open, Download as zip, Paste into, Copy path), a Trash row (Restore, Copy path, Delete forever), the Trash area (Refresh, Empty Trash) and an admin's per-user Trash folder (Open, Empty Trash). - Users: row menu (Edit + Open home in Files for an admin, Copy username/email). - Artifacts: card menu repeating its buttons, plus Copy link for a page. - Git identity: named SSH keys beside the default ones (id_<type>_<name>, max 10, DELETE ?id=); SSH logins per alias (`ssh://<alias>/` → `ssh://<user>@<host>/`, so several AWS CodeCommit accounts share a host), each optionally tied to one key (`agenthub.<from>.sshKey`) — a clone of that URL offers only that key and persists it. Terminal sshCommand follows the keys. Settings → Git: "More SSH keys" + account name/key in the AWS and other-host forms. Docs/API.md, OpenAPI updated.
385 lines
12 KiB
JavaScript
385 lines
12 KiB
JavaScript
/**
|
|
* @file OpenAPI fragment for the File Explorer — the same operations on a
|
|
* workspace (`/api/lanes/{id}/fs/*`, also under /api/workspaces) and on a
|
|
* user's home (`/api/homes/{who}/fs/*`), generated from one table so the two
|
|
* cannot drift — plus `GET /api/homes`, Add workspace → Clone
|
|
* (`POST /api/lanes/clone`) and the user's own git identity (`/api/me/git`).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const tags = [
|
|
{
|
|
name: "Files",
|
|
description:
|
|
"The File Explorer on a workspace or a user's home: list, make, rename, move, copy, delete into a restorable trash, streamed upload, zip download. Writes run as the folder's account; a taken name is settled by `conflict`.",
|
|
},
|
|
{
|
|
name: "Git identity",
|
|
description:
|
|
"A user's own SSH key and HTTPS logins and commit author, and cloning a repository into a new workspace with them.",
|
|
},
|
|
];
|
|
|
|
const errorCodes =
|
|
"400 EBADPATH/EBADNAME, 403 ESECRET/EFOREIGN/EREADONLY (another user's home)/EBADORIGIN, 404 ENOFILE/ENOHOME, 409 EEXISTS (with `error.names`)/EUPGRADE/EMEMBERGONE, 413 ETOOBIG.";
|
|
|
|
const schemas = {
|
|
FsEntry: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
path: { type: "string", description: "Relative to the workspace or home." },
|
|
type: { type: "string", enum: ["dir", "file", "link", "linkdir", "other"] },
|
|
size: { type: "integer", nullable: true },
|
|
mtime: { type: "number", description: "Milliseconds since the epoch." },
|
|
},
|
|
},
|
|
FsListing: {
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string" },
|
|
parent: { type: "string", nullable: true },
|
|
entries: { type: "array", items: { $ref: "#/components/schemas/FsEntry" } },
|
|
truncated: { type: "boolean" },
|
|
},
|
|
},
|
|
FsConflict: {
|
|
type: "string",
|
|
enum: ["fail", "skip", "keep", "replace"],
|
|
default: "fail",
|
|
description:
|
|
"A taken name: `fail` answers 409 EEXISTS with the names, `skip` leaves it, `keep` uses a free `name (1).ext`, `replace` sends the old one to the trash first.",
|
|
},
|
|
};
|
|
|
|
const json = (properties, required = []) => ({
|
|
required: true,
|
|
content: { "application/json": { schema: { type: "object", required, properties } } },
|
|
});
|
|
const pathsArr = { type: "array", items: { type: "string" } };
|
|
const conflict = { $ref: "#/components/schemas/FsConflict" };
|
|
|
|
/** The operations, without their prefix: [method, sub, summary, extra]. */
|
|
const OPS = [
|
|
[
|
|
"get",
|
|
"/list",
|
|
"List a folder (folders first)",
|
|
{
|
|
parameters: [{ name: "path", in: "query", schema: { type: "string", default: "" } }],
|
|
ok: {
|
|
description: "The folder.",
|
|
content: { "application/json": { schema: { $ref: "#/components/schemas/FsListing" } } },
|
|
},
|
|
},
|
|
],
|
|
[
|
|
"post",
|
|
"/mkdir",
|
|
"Make a folder",
|
|
{ body: json({ path: { type: "string" } }, ["path"]), status: 201 },
|
|
],
|
|
[
|
|
"post",
|
|
"/newfile",
|
|
"Make an empty file",
|
|
{ body: json({ path: { type: "string" } }, ["path"]), status: 201 },
|
|
],
|
|
[
|
|
"post",
|
|
"/rename",
|
|
"Rename in place",
|
|
{ body: json({ path: { type: "string" }, name: { type: "string" } }, ["path", "name"]) },
|
|
],
|
|
[
|
|
"post",
|
|
"/move",
|
|
"Move items into a folder",
|
|
{ body: json({ paths: pathsArr, dest: { type: "string" }, conflict }, ["paths", "dest"]) },
|
|
],
|
|
[
|
|
"post",
|
|
"/copy",
|
|
"Copy items into a folder — from here, another workspace (`fromLane`) or a home (`fromHome`) of the same account",
|
|
{
|
|
body: json(
|
|
{
|
|
paths: pathsArr,
|
|
dest: { type: "string" },
|
|
conflict,
|
|
fromLane: { type: "integer" },
|
|
fromHome: { type: "string" },
|
|
},
|
|
["paths", "dest"]
|
|
),
|
|
},
|
|
],
|
|
[
|
|
"post",
|
|
"/delete",
|
|
"Move items into the account's trash (restorable for 7 days)",
|
|
{ body: json({ paths: pathsArr }, ["paths"]) },
|
|
],
|
|
["get", "/trash", "The trash, newest first (entries older than 7 days are dropped)", {}],
|
|
[
|
|
"post",
|
|
"/trash/restore",
|
|
"Put a trash item back where it was",
|
|
{ body: json({ id: { type: "string" } }, ["id"]) },
|
|
],
|
|
[
|
|
"post",
|
|
"/trash/purge",
|
|
"Delete a trash item (or `all`) for good",
|
|
{ body: json({ id: { type: "string" } }, ["id"]) },
|
|
],
|
|
[
|
|
"post",
|
|
"/upload",
|
|
"Upload one file, streamed, making missing sub-folders (≤ 500 MB)",
|
|
{
|
|
parameters: [
|
|
{ name: "path", in: "query", required: true, schema: { type: "string" } },
|
|
{ name: "conflict", in: "query", schema: conflict },
|
|
],
|
|
body: {
|
|
required: true,
|
|
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } },
|
|
},
|
|
status: 201,
|
|
},
|
|
],
|
|
[
|
|
"get",
|
|
"/zip",
|
|
"Files and folders as one streamed zip (≤ 2 GB, 50,000 files)",
|
|
{
|
|
parameters: [
|
|
{
|
|
name: "p",
|
|
in: "query",
|
|
required: true,
|
|
schema: { type: "array", items: { type: "string" } },
|
|
style: "form",
|
|
explode: true,
|
|
},
|
|
],
|
|
ok: {
|
|
description: "application/zip",
|
|
content: { "application/zip": { schema: { type: "string", format: "binary" } } },
|
|
},
|
|
},
|
|
],
|
|
];
|
|
|
|
function operations(prefix, idParam, opPrefix, note) {
|
|
const out = {};
|
|
for (const [method, sub, summary, x] of OPS) {
|
|
const key = `${prefix}${sub}`;
|
|
out[key] = out[key] || {};
|
|
out[key][method] = {
|
|
tags: ["Files"],
|
|
summary,
|
|
description: `${note} ${errorCodes}`,
|
|
operationId: `${opPrefix}${sub.replace(/\/(\w)/g, (_, c) => c.toUpperCase())}`,
|
|
parameters: [idParam, ...(x.parameters || [])],
|
|
...(x.body ? { requestBody: x.body } : {}),
|
|
responses: { [x.status || 200]: x.ok || { description: "Done." } },
|
|
};
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const paths = {
|
|
...operations(
|
|
"/api/lanes/{id}/fs",
|
|
{ name: "id", in: "path", required: true, schema: { type: "integer" } },
|
|
"laneFs",
|
|
"On a workspace (`?session=` / `?source=` pick the session worktree / extra folder, like the git reads)."
|
|
),
|
|
...operations(
|
|
"/api/homes/{who}/fs",
|
|
{
|
|
name: "who",
|
|
in: "path",
|
|
required: true,
|
|
schema: { type: "string" },
|
|
description: "`me`, or another user's name (an admin's read-only view).",
|
|
},
|
|
"homeFs",
|
|
"On a user's home. Every dot entry at the top of the home is hidden and refused."
|
|
),
|
|
"/api/lanes/{id}/fs/download": undefined,
|
|
"/api/homes/{who}/fs/download": {
|
|
get: {
|
|
tags: ["Files"],
|
|
summary: "Download one file of a home",
|
|
operationId: "homeFsDownload",
|
|
parameters: [
|
|
{ name: "who", in: "path", required: true, schema: { type: "string" } },
|
|
{ name: "path", in: "query", required: true, schema: { type: "string" } },
|
|
],
|
|
responses: { 200: { description: "The file, as a sandboxed attachment." } },
|
|
},
|
|
},
|
|
"/api/homes": {
|
|
get: {
|
|
tags: ["Files"],
|
|
summary: "The homes the caller may open",
|
|
description: "`me` (their own), and for an admin every other user's — read-only.",
|
|
operationId: "listHomes",
|
|
responses: { 200: { description: "{homes: [{id, label, readOnly}]}" } },
|
|
},
|
|
},
|
|
"/api/lanes/clone": {
|
|
post: {
|
|
tags: ["Git identity"],
|
|
summary: "Clone a repository into a new folder and make it a workspace",
|
|
description:
|
|
"`url`: https:// (no userinfo), ssh:// or user@host:path. Runs as the folder's account with that account's own SSH key or HTTPS login. 409 ECREDENTIALS (`error.auth` names the host) asks for `credentials`; 409 ESSHAUTH: the key was refused; 409 ENOREPO: no such repository.",
|
|
operationId: "cloneLane",
|
|
requestBody: json(
|
|
{
|
|
url: { type: "string" },
|
|
parent: { type: "string" },
|
|
name: { type: "string" },
|
|
title: { type: "string" },
|
|
tool: { type: "string" },
|
|
credentials: {
|
|
type: "object",
|
|
properties: {
|
|
username: { type: "string" },
|
|
password: { type: "string" },
|
|
remember: { type: "boolean" },
|
|
},
|
|
},
|
|
},
|
|
["url", "parent"]
|
|
),
|
|
responses: { 201: { description: "{lane, created: true, path}" } },
|
|
},
|
|
},
|
|
"/api/me/git": {
|
|
get: {
|
|
tags: ["Git identity"],
|
|
summary: "The caller's own git identity",
|
|
description: "Only the public SSH key and `hasToken` are ever returned.",
|
|
operationId: "getGitIdentity",
|
|
responses: {
|
|
200: {
|
|
description:
|
|
"{account, writable, ssh: {type, publicKey, fingerprint, terminal} | null, https: [...], author: {name, email}}",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/me/git/ssh-key": {
|
|
post: {
|
|
tags: ["Git identity"],
|
|
summary:
|
|
"Make (or with `replace`, replace) the default SSH key of `type`, or with `name` one more beside it",
|
|
operationId: "generateSshKey",
|
|
requestBody: json({
|
|
replace: { type: "boolean" },
|
|
type: { type: "string", enum: ["ed25519", "rsa"] },
|
|
name: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,31}$" },
|
|
}),
|
|
responses: {
|
|
201: { description: "The identity." },
|
|
400: { description: "EBADKEYNAME" },
|
|
409: { description: "EEXISTS (without replace, or the name is taken) / ETOOMANYKEYS" },
|
|
},
|
|
},
|
|
delete: {
|
|
tags: ["Git identity"],
|
|
summary: "Delete one SSH key (?id=, or the default of ?type=ed25519|rsa), or every key",
|
|
operationId: "deleteSshKey",
|
|
parameters: [
|
|
{
|
|
name: "type",
|
|
in: "query",
|
|
required: false,
|
|
schema: { type: "string", enum: ["ed25519", "rsa"] },
|
|
},
|
|
{ name: "id", in: "query", required: false, schema: { type: "string" } },
|
|
],
|
|
responses: { 200: { description: "The identity." } },
|
|
},
|
|
},
|
|
"/api/me/git/ssh-terminal": {
|
|
put: {
|
|
tags: ["Git identity"],
|
|
summary: "Use the SSH key for git in the account's own terminal (core.sshCommand)",
|
|
operationId: "setTerminalSsh",
|
|
requestBody: json({ on: { type: "boolean" } }, ["on"]),
|
|
responses: {
|
|
200: { description: "The identity; `ssh.terminal` is on, off or foreign." },
|
|
409: { description: "EFOREIGNSSH — ~/.gitconfig already sets its own core.sshCommand." },
|
|
},
|
|
},
|
|
},
|
|
"/api/me/git/ssh-user": {
|
|
put: {
|
|
tags: ["Git identity"],
|
|
summary:
|
|
"An SSH login: url.<ssh://user@host/>.insteadOf ssh://<alias or host>/, optionally tied to one key",
|
|
operationId: "setSshUser",
|
|
requestBody: json(
|
|
{
|
|
host: { type: "string" },
|
|
user: { type: "string" },
|
|
alias: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,62}$" },
|
|
key: { type: "string" },
|
|
},
|
|
["host", "user"]
|
|
),
|
|
responses: {
|
|
200: { description: "The identity; `sshUsers: [{host, user, alias, key}]`." },
|
|
400: { description: "EBADHOST / EBADSSHUSER / EBADALIAS / ENOKEY" },
|
|
},
|
|
},
|
|
},
|
|
"/api/me/git/ssh-user/{from}": {
|
|
delete: {
|
|
tags: ["Git identity"],
|
|
summary: "Drop the SSH login of a host or alias (and its key link)",
|
|
operationId: "deleteSshUser",
|
|
parameters: [{ name: "from", in: "path", required: true, schema: { type: "string" } }],
|
|
responses: { 200: { description: "The identity." }, 404: { description: "ENOTFOUND" } },
|
|
},
|
|
},
|
|
"/api/me/git/https": {
|
|
post: {
|
|
tags: ["Git identity"],
|
|
summary: "Save an HTTPS login (never returned)",
|
|
operationId: "saveHttpsLogin",
|
|
requestBody: json(
|
|
{ host: { type: "string" }, username: { type: "string" }, password: { type: "string" } },
|
|
["host", "username", "password"]
|
|
),
|
|
responses: { 200: { description: "The identity." } },
|
|
},
|
|
},
|
|
"/api/me/git/https/{host}": {
|
|
delete: {
|
|
tags: ["Git identity"],
|
|
summary: "Delete an HTTPS login",
|
|
operationId: "deleteHttpsLogin",
|
|
parameters: [{ name: "host", in: "path", required: true, schema: { type: "string" } }],
|
|
responses: { 200: { description: "The identity." } },
|
|
},
|
|
},
|
|
"/api/me/git/author": {
|
|
put: {
|
|
tags: ["Git identity"],
|
|
summary: "Set the commit author (`git config --global user.name/email`)",
|
|
operationId: "setGitAuthor",
|
|
requestBody: json({ name: { type: "string" }, email: { type: "string" } }, ["name", "email"]),
|
|
responses: { 200: { description: "The identity." }, 400: { description: "EBADAUTHOR" } },
|
|
},
|
|
},
|
|
};
|
|
delete paths["/api/lanes/{id}/fs/download"];
|
|
|
|
module.exports = { tags, schemas, paths };
|