mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
const ApiBase = "/api";
|
|
|
|
async function ParseJsonOrThrow(Response) {
|
|
const Body = await Response.json().catch(() => null);
|
|
|
|
if (!Response.ok) {
|
|
const Detail = Body && Body.detail ? Body.detail : Response.statusText;
|
|
throw new Error(Detail);
|
|
}
|
|
|
|
return Body;
|
|
}
|
|
|
|
function FetchJson(Path, Options = {}) {
|
|
return fetch(`${ApiBase}${Path}`, { credentials: "same-origin", ...Options }).then(
|
|
ParseJsonOrThrow
|
|
);
|
|
}
|
|
|
|
function FetchText(Path, Options = {}) {
|
|
return fetch(`${ApiBase}${Path}`, { credentials: "same-origin", ...Options }).then(
|
|
(Response) => Response.text()
|
|
);
|
|
}
|
|
|
|
function JsonBody(Body) {
|
|
return { headers: { "Content-Type": "application/json" }, body: JSON.stringify(Body) };
|
|
}
|
|
|
|
export function FetchAuthStatus() {
|
|
return FetchJson("/auth/status");
|
|
}
|
|
|
|
export function Bootstrap(Username, Password) {
|
|
return FetchJson("/auth/bootstrap", { method: "POST", ...JsonBody({ Username, Password }) });
|
|
}
|
|
|
|
export function Login(Username, Password) {
|
|
return FetchJson("/auth/login", { method: "POST", ...JsonBody({ Username, Password }) });
|
|
}
|
|
|
|
export function Logout() {
|
|
return FetchJson("/auth/logout", { method: "POST" });
|
|
}
|
|
|
|
export function FetchMe() {
|
|
return FetchJson("/auth/me");
|
|
}
|
|
|
|
export function FetchUsers() {
|
|
return FetchJson("/users");
|
|
}
|
|
|
|
export function CreateUserAccount(Username, Password, IsAdmin, Colour) {
|
|
return FetchJson("/users", { method: "POST", ...JsonBody({ Username, Password, IsAdmin, Colour }) });
|
|
}
|
|
|
|
export function FetchProfileColourOptions() {
|
|
return FetchJson("/profile/colours");
|
|
}
|
|
|
|
export function UpdateProfileColour(Colour) {
|
|
return FetchJson("/profile/colour", { method: "PUT", ...JsonBody({ Colour }) });
|
|
}
|
|
|
|
export async function UploadAvatar(File) {
|
|
const Form = new FormData();
|
|
Form.append("Avatar", File);
|
|
const Response = await fetch(`${ApiBase}/profile/avatar`, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
body: Form,
|
|
});
|
|
return ParseJsonOrThrow(Response);
|
|
}
|
|
|
|
export function GetAvatarUrl(Username) {
|
|
const Params = new URLSearchParams({ Username });
|
|
return `${ApiBase}/profile/avatar?${Params}`;
|
|
}
|
|
|
|
export function UpdateUsername(NewUsername) {
|
|
return FetchJson("/profile/username", { method: "PUT", ...JsonBody({ NewUsername }) });
|
|
}
|
|
|
|
export function ChangePassword(CurrentPassword, NewPassword) {
|
|
return FetchJson("/profile/password", {
|
|
method: "PUT",
|
|
...JsonBody({ CurrentPassword, NewPassword }),
|
|
});
|
|
}
|
|
|
|
export function FetchGroups() {
|
|
return FetchJson("/groups");
|
|
}
|
|
|
|
export function CreateGroupApi(Name) {
|
|
return FetchJson("/groups", { method: "POST", ...JsonBody({ Name }) });
|
|
}
|
|
|
|
export function DeleteGroupApi(Name) {
|
|
const Params = new URLSearchParams({ Name });
|
|
return FetchJson(`/groups?${Params}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function SetGroupMembersApi(Name, Members) {
|
|
const Params = new URLSearchParams({ Name });
|
|
return FetchJson(`/groups/members?${Params}`, { method: "PUT", ...JsonBody({ Members }) });
|
|
}
|
|
|
|
export function FetchLocks() {
|
|
return FetchJson("/locks");
|
|
}
|
|
|
|
export function FetchLock(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/lock?${Params}`);
|
|
}
|
|
|
|
export function FetchInheritedLock(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/lock/inherited?${Params}`);
|
|
}
|
|
|
|
export function SetLock(RootName, RelativePath, Users, Groups) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/lock?${Params}`, { method: "PUT", ...JsonBody({ Users, Groups }) });
|
|
}
|
|
|
|
export function RemoveLock(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/lock?${Params}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function FetchRoots() {
|
|
return FetchJson("/roots");
|
|
}
|
|
|
|
export function FetchTree(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/tree?${Params}`);
|
|
}
|
|
|
|
export function FetchGraph(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/graph?${Params}`);
|
|
}
|
|
|
|
export function FetchFile(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchText(`/file?${Params}`);
|
|
}
|
|
|
|
export function FetchResolvedFile(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/file/resolved?${Params}`);
|
|
}
|
|
|
|
export function GetFileUrl(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return `${ApiBase}/file?${Params}`;
|
|
}
|
|
|
|
export function GetRawFileUrl(RootName, RelativePath) {
|
|
const EncodedSegments = RelativePath.split("/").map(encodeURIComponent);
|
|
return `${ApiBase}/raw/${encodeURIComponent(RootName)}/${EncodedSegments.join("/")}`;
|
|
}
|
|
|
|
export function GetDownloadUrl(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return `${ApiBase}/download?${Params}`;
|
|
}
|
|
|
|
// Uploads go through XMLHttpRequest: unlike fetch, it reports how much has
|
|
// been sent. OnProgress(BytesSent) is optional.
|
|
function PostFormWithProgress(Url, Form, OnProgress) {
|
|
return new Promise((Resolve, Reject) => {
|
|
const Request = new XMLHttpRequest();
|
|
Request.open("POST", Url);
|
|
Request.withCredentials = true;
|
|
if (OnProgress) {
|
|
Request.upload.onprogress = (Event) => OnProgress(Event.loaded);
|
|
}
|
|
Request.onload = () => {
|
|
let Body = null;
|
|
try {
|
|
Body = JSON.parse(Request.responseText);
|
|
} catch {
|
|
Body = null;
|
|
}
|
|
if (Request.status >= 200 && Request.status < 300) {
|
|
Resolve(Body);
|
|
} else {
|
|
Reject(new Error(Body && Body.detail ? Body.detail : Request.statusText || `Upload failed (${Request.status})`));
|
|
}
|
|
};
|
|
Request.onerror = () => Reject(new Error("Upload failed: couldn't reach the server."));
|
|
Request.send(Form);
|
|
});
|
|
}
|
|
|
|
export function UploadFiles(RootName, RelativePath, Files, OnProgress) {
|
|
const Form = new FormData();
|
|
for (const File of Files) {
|
|
Form.append("Files", File);
|
|
}
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return PostFormWithProgress(`${ApiBase}/upload?${Params}`, Form, OnProgress);
|
|
}
|
|
|
|
const UploadChunkSizeBytes = 20 * 1024 * 1024;
|
|
export const ChunkedUploadThresholdBytes = 40 * 1024 * 1024;
|
|
|
|
export function IsLargeUpload(File) {
|
|
return File.size > ChunkedUploadThresholdBytes;
|
|
}
|
|
|
|
function GenerateUploadId() {
|
|
if (window.crypto && window.crypto.randomUUID) {
|
|
return window.crypto.randomUUID();
|
|
}
|
|
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
}
|
|
|
|
function UploadFileChunk(RootName, RelativePath, UploadId, ChunkIndex, TotalChunks, FileName, ChunkBlob, OnProgress) {
|
|
const Form = new FormData();
|
|
Form.append("Chunk", ChunkBlob, "chunk");
|
|
const Params = new URLSearchParams({
|
|
Root: RootName,
|
|
Path: RelativePath,
|
|
UploadId,
|
|
ChunkIndex: String(ChunkIndex),
|
|
TotalChunks: String(TotalChunks),
|
|
FileName,
|
|
});
|
|
return PostFormWithProgress(`${ApiBase}/upload/chunk?${Params}`, Form, OnProgress);
|
|
}
|
|
|
|
function CompleteChunkedUpload(RootName, RelativePath, UploadId, TotalChunks, FileName) {
|
|
const Params = new URLSearchParams({
|
|
Root: RootName,
|
|
Path: RelativePath,
|
|
UploadId,
|
|
TotalChunks: String(TotalChunks),
|
|
FileName,
|
|
});
|
|
return FetchJson(`/upload/complete?${Params}`, { method: "POST" });
|
|
}
|
|
|
|
// OnProgress(BytesSent) counts across all the chunks.
|
|
export async function UploadFileChunked(RootName, RelativePath, File, OnProgress) {
|
|
const UploadId = GenerateUploadId();
|
|
const TotalChunks = Math.max(1, Math.ceil(File.size / UploadChunkSizeBytes));
|
|
|
|
for (let ChunkIndex = 0; ChunkIndex < TotalChunks; ChunkIndex++) {
|
|
const Start = ChunkIndex * UploadChunkSizeBytes;
|
|
const ChunkBlob = File.slice(Start, Start + UploadChunkSizeBytes);
|
|
await UploadFileChunk(RootName, RelativePath, UploadId, ChunkIndex, TotalChunks, File.name, ChunkBlob,
|
|
OnProgress && ((Sent) => OnProgress(Start + Math.min(Sent, ChunkBlob.size))));
|
|
}
|
|
|
|
return CompleteChunkedUpload(RootName, RelativePath, UploadId, TotalChunks, File.name);
|
|
}
|
|
|
|
// Where each variable (portal, definition, loop) is in Content, and what it
|
|
// resolves to: the live editor shows the results in place (LivePreview.js).
|
|
export function FetchSpans(RootName, RelativePath, Content) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/file/spans?${Params}`, { method: "POST", ...JsonBody({ Content }) });
|
|
}
|
|
|
|
export function SaveFile(RootName, RelativePath, Content) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/file?${Params}`, { method: "PUT", body: Content });
|
|
}
|
|
|
|
export function FetchBook(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/book?${Params}`);
|
|
}
|
|
|
|
export function FetchBlame(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/blame?${Params}`);
|
|
}
|
|
|
|
export function CreateEntry(RootName, RelativePath, IsDirectory) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/tree?${Params}`, { method: "POST", ...JsonBody({ IsDirectory }) });
|
|
}
|
|
|
|
export function RenameEntry(RootName, RelativePath, NewName) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/tree?${Params}`, { method: "PATCH", ...JsonBody({ NewName }) });
|
|
}
|
|
|
|
export function MoveEntry(RootName, RelativePath, DestinationPath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/tree?${Params}`, { method: "PATCH", ...JsonBody({ DestinationPath }) });
|
|
}
|
|
|
|
export function DeleteEntry(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/tree?${Params}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function EnsureDirectoryNote(RootName, RelativePath) {
|
|
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
|
return FetchJson(`/note?${Params}`, { method: "POST" });
|
|
}
|
|
|
|
export function FetchSettings() {
|
|
return FetchJson("/settings");
|
|
}
|
|
|
|
export function SaveSettings(NewSettings) {
|
|
return FetchJson("/settings", { method: "PUT", ...JsonBody(NewSettings) });
|
|
}
|
|
|
|
export function FetchThemeNames() {
|
|
return FetchJson("/themes");
|
|
}
|
|
|
|
export function FetchTheme(Name) {
|
|
const Params = new URLSearchParams({ Name });
|
|
return FetchJson(`/theme?${Params}`);
|
|
}
|
|
|
|
export function SaveTheme(Name, Palette) {
|
|
const Params = new URLSearchParams({ Name });
|
|
return FetchJson(`/theme?${Params}`, { method: "PUT", ...JsonBody({ Palette }) });
|
|
}
|
|
|
|
export function DeleteTheme(Name) {
|
|
const Params = new URLSearchParams({ Name });
|
|
return FetchJson(`/theme?${Params}`, { method: "DELETE" });
|
|
}
|