// 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}`); } 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; } // 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 /: const trimmed = (remotePath || "").replace(/^\/+|\/+$/g, ""); const path = trimmed ? `${remoteFs}/${trimmed}/${fileName}` : `${remoteFs}/${fileName}`; let url = `${base}/${path}`; if (authHeader) { // Embed basic auth into the URL so the browser can fetch it directly. url = url.replace(/^(https?:\/\/)/, `$1${encodeURIComponent(AUTH_USER)}:${encodeURIComponent(AUTH_PASS)}@`); } return url; } 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; }