Files
2026-06-20 13:51:06 +08:00

113 lines
3.5 KiB
JavaScript

// rc.js — minimal client for rclone's RC HTTP API.
// The RC base URL is discovered from the ?url= query param that the
// webgui command appends to the GUI URL on launch. If absent (e.g. when
// developing the SPA from another server), fall back to same-origin.
const params = new URLSearchParams(location.search);
const QUERY_URL = params.get("url");
const RC_BASE = QUERY_URL
? QUERY_URL.replace(/\/$/, "")
: location.origin;
const AUTH_USER = params.get("user");
const AUTH_PASS = params.get("pass");
let authHeader = null;
if (AUTH_USER && AUTH_PASS) {
authHeader = "Basic " + btoa(`${AUTH_USER}:${AUTH_PASS}`);
scrubAuthQuery();
}
export function rcURL() {
return RC_BASE;
}
// Are we running with basic auth configured from the launch URL?
export function hasAuth() {
return authHeader !== null;
}
export function isNoAuth() {
return !authHeader;
}
function scrubAuthQuery() {
const clean = new URL(location.href);
clean.searchParams.delete("user");
clean.searchParams.delete("pass");
history.replaceState(null, "", `${clean.pathname}${clean.search}${clean.hash}`);
}
// POST JSON to an RC endpoint. Returns the parsed JSON response, or throws.
export async function post(path, body = {}, options = {}) {
const headers = { "Content-Type": "application/json" };
if (authHeader) headers["Authorization"] = authHeader;
const res = await fetch(RC_BASE + "/" + path.replace(/^\//, ""), {
method: "POST",
headers,
body: JSON.stringify(body),
});
return parseResponse(res, path, options);
}
// POST JSON and request an async job. Returns { jobid, executeId }.
export async function postAsync(path, body = {}) {
return post(path, { ...body, _async: true });
}
// Upload one or more files via multipart form-data.
// Matches operations/uploadfile: form fields `fs`, `remote`, and one
// file part per uploaded file. The server uses the part's filename.
export async function uploadFile(fs, remote, files) {
const form = new FormData();
form.set("fs", fs);
form.set("remote", remote);
for (const file of files) {
form.append("file", file, file.name);
}
const headers = {};
if (authHeader) headers["Authorization"] = authHeader;
// Do NOT set Content-Type — the browser sets multipart boundary.
const res = await fetch(RC_BASE + "/operations/uploadfile", {
method: "POST",
headers,
body: form,
});
return parseResponse(res, "/operations/uploadfile");
}
// Build a download URL for a file. Requires opt.Serve = true on the rc server.
export function downloadURL(remoteFs, remotePath, fileName) {
const base = RC_BASE.replace(/\/$/, "");
// rc server serves remote files at /<remote>:<path>
const path = [remoteFs, remotePath, fileName]
.filter(Boolean)
.flatMap((part) => String(part).split("/"))
.filter((part) => part !== "")
.map(encodeURIComponent)
.join("/");
return `${base}/${path}`;
}
async function parseResponse(res, path, { allowError = false } = {}) {
let body = null;
const ct = res.headers.get("Content-Type") || "";
if (ct.includes("application/json")) {
body = await res.json();
} else {
const text = await res.text();
body = text ? { raw: text } : {};
}
if (!res.ok) {
const msg = (body && (body.error || body.message)) || `HTTP ${res.status}`;
const err = new Error(`${path}: ${msg}`);
err.status = res.status;
err.body = body;
throw err;
}
if (body && body.error && !allowError) {
throw new Error(`${path}: ${body.error}`);
}
return body;
}