Forgot to add ts files

This commit is contained in:
Joep 2023-09-08 00:15:13 +02:00
parent 5a8ca3a50d
commit 4ea66b8bd3
16 changed files with 699 additions and 0 deletions

View File

@ -0,0 +1,18 @@
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(url: string, command: string, onmessage: WebSocket["onmessage"], onclose: WebSocket["onclose"]) {
const authStore = useAuthStore();
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
let conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);
conn.onmessage = onmessage;
conn.onclose = onclose;
}

208
frontend/src/api/files.ts Normal file
View File

@ -0,0 +1,208 @@
import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { upload as postTus, useTus } from "./tus";
export async function fetch(url: apiUrl) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
// Perhaps change the any
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
async function resourceAction(url: apiUrl, method: apiMethod, content?: any) {
debugger;
url = removePrefix(url);
let opts: apiOpts = {
method
};
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
}
export async function remove(url: apiUrl) {
return resourceAction(url, "DELETE");
}
export async function put(url: apiUrl, content = "") {
return resourceAction(url, "PUT", content);
}
export function download(format: any, ...files: string[]) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
const authStore = useAuthStore();
if (authStore.jwt) {
url += `auth=${authStore.jwt}&`;
}
window.open(url);
}
export async function post(url: apiUrl, content: apiContent = "", overwrite = false, onupload: Function = () => {}) {
// Use the pre-existing API if:
const useResourcesApi =
// a folder is being created
url.endsWith("/") ||
// We're not using http(s)
(content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)) ||
// Tus is disabled / not applicable
!(await useTus(content));
return useResourcesApi
? postResources(url, content, overwrite, onupload)
: postTus(url, content, overwrite, onupload);
}
async function postResources(url: apiUrl, content: apiContent = "", overwrite = false, onupload: any) {
url = removePrefix(url);
let bufferContent: ArrayBuffer;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
const authStore = useAuthStore();
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", authStore.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
}
function moveCopy(items: item[], copy = false, overwrite = false, rename = false) {
let promises = [];
for (let item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to ?? ""));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
}
export function move(items: item[], overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy(items: item[], overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum(url: apiUrl, algo: algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}
export function getDownloadURL(file: file, inline: any) {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
}
export function getPreviewURL(file: file, size: string) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
}
export function getSubtitlesURL(file: file) {
const params = {
inline: "true",
};
const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
}
return subtitles;
}
export async function usage(url: apiUrl) {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
}

View File

@ -0,0 +1,9 @@
import * as files from "./files";
import * as share from "./share";
import * as users from "./users";
import * as settings from "./settings";
import * as pub from "./pub";
import search from "./search";
import commands from "./commands";
export { files, share, users, settings, pub, commands, search };

71
frontend/src/api/pub.ts Normal file
View File

@ -0,0 +1,71 @@
import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url: apiUrl, password: string = "") {
url = removePrefix(url);
const res = await fetchURL(
`/api/public/share${url}`,
{
headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) },
},
false
);
let data = await res.json();
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
// Is this redundant code?
// export function download(format, hash, token, ...files) {
// let url = `${baseURL}/api/public/dl/${hash}`;
// if (files.length === 1) {
// url += encodeURIComponent(files[0]) + "?";
// } else {
// let arg = "";
// for (let file of files) {
// arg += encodeURIComponent(file) + ",";
// }
// arg = arg.substring(0, arg.length - 1);
// arg = encodeURIComponent(arg);
// url += `/?files=${arg}&`;
// }
// if (format) {
// url += `algo=${format}&`;
// }
// if (token) {
// url += `token=${token}&`;
// }
// window.open(url);
// }
export function getDownloadURL(share: share, inline = false) {
const params = {
...(inline && { inline: "true" }),
...(share.token && { token: share.token }),
};
return createURL("api/public/dl/" + share.hash + share.path, params, false);
}

View File

@ -0,0 +1,27 @@
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
export default async function search(base: apiUrl, query: string) {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item: item) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
}
return item;
});
return data;
}

View File

@ -0,0 +1,12 @@
import { fetchURL, fetchJSON } from "./utils";
export function get() {
return fetchJSON(`/api/settings`, {});
}
export async function update(settings: settings) {
await fetchURL(`/api/settings`, {
method: "PUT",
body: JSON.stringify(settings),
});
}

40
frontend/src/api/share.ts Normal file
View File

@ -0,0 +1,40 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function list() {
return fetchJSON("/api/shares");
}
export async function get(url: apiUrl) {
url = removePrefix(url);
return fetchJSON(`/api/share${url}`);
}
export async function remove(hash: string) {
await fetchURL(`/api/share/${hash}`, {
method: "DELETE",
});
}
export async function create(url: apiUrl, password = "", expires = "", unit = "hours") {
url = removePrefix(url);
url = `/api/share${url}`;
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({
password: password,
expires: expires.toString(), // backend expects string not number
unit: unit,
});
}
return fetchJSON(url, {
method: "POST",
body: body,
});
}
export function getShareURL(share: share) {
return createURL("share/" + share.hash, {}, false);
}

96
frontend/src/api/tus.ts Normal file
View File

@ -0,0 +1,96 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
export async function upload(
filePath: string,
content: apiContent = "",
overwrite = false,
onupload: Function
) {
if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function
throw new Error("Tus.io settings are not defined");
}
filePath = removePrefix(filePath);
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string
if(content === "") {
return false;
}
return new Promise<void | string>((resolve, reject) => {
let upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": authStore.jwt,
},
onError: function (error) {
reject("Upload failed: " + error);
},
onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded)
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
resolve();
},
});
upload.start();
});
}
async function createUpload(resourcePath: resourcePath) {
let headResp = await fetchURL(resourcePath, {
method: "POST",
});
if (headResp.status !== 201) {
throw new Error(
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
);
}
}
function computeRetryDelays(tusSettings: tusSettings): number[] | undefined{
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
return undefined;
}
// The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000]
const retryDelays = [];
let delay = 0;
for (let i = 0; i < tusSettings.retryCount; i++) {
retryDelays.push(Math.min(delay, RETRY_MAX_DELAY));
delay =
delay === 0 ? RETRY_BASE_DELAY : Math.min(delay * 2, RETRY_MAX_DELAY);
}
return retryDelays;
}
export async function useTus(content: apiContent) {
return isTusSupported() && content instanceof Blob;
}
function isTusSupported() {
return tus.isSupported === true;
}

41
frontend/src/api/users.ts Normal file
View File

@ -0,0 +1,41 @@
import { fetchURL, fetchJSON } from "./utils";
export async function getAll() {
return fetchJSON(`/api/users`, {});
}
export async function get(id: number) {
return fetchJSON(`/api/users/${id}`, {});
}
export async function create(user: user) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
if (res.status === 201) {
return res.headers.get("Location");
}
}
export async function update(user: user, which = ["all"]) {
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
}
export async function remove(id: number) {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
}

86
frontend/src/api/utils.ts Normal file
View File

@ -0,0 +1,86 @@
import { useAuthStore } from "@/stores/auth";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export async function fetchURL(url: apiUrl, opts: apiOpts, auth = true) {
const authStore = useAuthStore();
opts = opts || {};
opts.headers = opts.headers || {};
let { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": authStore.jwt,
...headers,
},
...rest,
});
} catch {
const error = new Error("000 No connection");
// @ts-ignore don't know yet how to solve
error.status = 0;
throw error;
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(authStore.jwt);
}
if (res.status < 200 || res.status > 299) {
const error = new Error(await res.text());
// @ts-ignore don't know yet how to solve
error.status = res.status;
if (auth && res.status == 401) {
logout();
}
throw error;
}
return res;
}
export async function fetchJSON(url: apiUrl, opts?: any) {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json();
} else {
throw new Error(res.status.toString());
}
}
export function removePrefix(url: apiUrl) {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}
export function createURL(endpoint: apiUrl, params = {}, auth = true) {
const authStore = useAuthStore();
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams: searchParams = {
...(auth && { auth: authStore.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

37
frontend/src/types/api.d.ts vendored Normal file
View File

@ -0,0 +1,37 @@
type apiUrl = string // Can also be set as a path eg: "path1" | "path2"
type resourcePath = string
type apiMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
// type apiContent = string | Blob | File
type apiContent = Blob | File | Pick<ReadableStreamDefaultReader<any>, "read"> | ""
interface apiOpts {
method?: apiMethod,
headers?: object,
body?: any
}
interface tusSettings {
retryCount: number
}
type algo = any
type inline = any
interface share {
expire: any,
hash: string,
path: string,
userID: number,
token: string
}
interface settings {
any
}
type searchParams = any

40
frontend/src/types/file.d.ts vendored Normal file
View File

@ -0,0 +1,40 @@
interface file {
name: string,
modified: string,
path: string,
subtitles: any[],
isDir: boolean,
size: number,
fullPath: string,
type: uploadType
}
interface item {
id: number,
path: string,
file: file,
url?: string,
dir?: boolean,
from?: string,
to?: string,
name?: string,
type?: uploadType
overwrite: boolean
}
type uploadType = "video" | "audio" | "image" | "pdf" | "text" | "blob"
interface req {
isDir?: boolean
}
interface uploads {
[key: string]: upload
}
interface upload {
id: number,
file: file,
type: string
}

5
frontend/src/types/layout.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
interface LayoutValue {
prompt: boolean,
confirm: boolean,
action: boolean,
}

7
frontend/src/types/user.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
interface user {
id: number,
locale: string,
perm: any
}
type userKey = keyof user

1
frontend/src/types/utils.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type settings = any

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />