113 lines
3.5 KiB
JavaScript
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;
|
|
}
|