From 4ea66b8bd37eee8f1ee91430d4fccefa43255586 Mon Sep 17 00:00:00 2001 From: Joep Date: Fri, 8 Sep 2023 00:15:13 +0200 Subject: [PATCH] Forgot to add ts files --- frontend/src/api/commands.ts | 18 +++ frontend/src/api/files.ts | 208 +++++++++++++++++++++++++++++++++ frontend/src/api/index.ts | 9 ++ frontend/src/api/pub.ts | 71 +++++++++++ frontend/src/api/search.ts | 27 +++++ frontend/src/api/settings.ts | 12 ++ frontend/src/api/share.ts | 40 +++++++ frontend/src/api/tus.ts | 96 +++++++++++++++ frontend/src/api/users.ts | 41 +++++++ frontend/src/api/utils.ts | 86 ++++++++++++++ frontend/src/types/api.d.ts | 37 ++++++ frontend/src/types/file.d.ts | 40 +++++++ frontend/src/types/layout.d.ts | 5 + frontend/src/types/user.d.ts | 7 ++ frontend/src/types/utils.d.ts | 1 + frontend/src/vite-env.d.ts | 1 + 16 files changed, 699 insertions(+) create mode 100644 frontend/src/api/commands.ts create mode 100644 frontend/src/api/files.ts create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/pub.ts create mode 100644 frontend/src/api/search.ts create mode 100644 frontend/src/api/settings.ts create mode 100644 frontend/src/api/share.ts create mode 100644 frontend/src/api/tus.ts create mode 100644 frontend/src/api/users.ts create mode 100644 frontend/src/api/utils.ts create mode 100644 frontend/src/types/api.d.ts create mode 100644 frontend/src/types/file.d.ts create mode 100644 frontend/src/types/layout.d.ts create mode 100644 frontend/src/types/user.d.ts create mode 100644 frontend/src/types/utils.d.ts create mode 100644 frontend/src/vite-env.d.ts diff --git a/frontend/src/api/commands.ts b/frontend/src/api/commands.ts new file mode 100644 index 00000000..12e6af8f --- /dev/null +++ b/frontend/src/api/commands.ts @@ -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; +} diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts new file mode 100644 index 00000000..3c0852f3 --- /dev/null +++ b/frontend/src/api/files.ts @@ -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(); +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 00000000..abc189dc --- /dev/null +++ b/frontend/src/api/index.ts @@ -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 }; diff --git a/frontend/src/api/pub.ts b/frontend/src/api/pub.ts new file mode 100644 index 00000000..526e2b0d --- /dev/null +++ b/frontend/src/api/pub.ts @@ -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); +} diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts new file mode 100644 index 00000000..a6cd6175 --- /dev/null +++ b/frontend/src/api/search.ts @@ -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; +} diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts new file mode 100644 index 00000000..43fc126c --- /dev/null +++ b/frontend/src/api/settings.ts @@ -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), + }); +} diff --git a/frontend/src/api/share.ts b/frontend/src/api/share.ts new file mode 100644 index 00000000..441ea7a7 --- /dev/null +++ b/frontend/src/api/share.ts @@ -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); +} diff --git a/frontend/src/api/tus.ts b/frontend/src/api/tus.ts new file mode 100644 index 00000000..632572d0 --- /dev/null +++ b/frontend/src/api/tus.ts @@ -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((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; +} diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts new file mode 100644 index 00000000..0acfaad5 --- /dev/null +++ b/frontend/src/api/users.ts @@ -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", + }); +} diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts new file mode 100644 index 00000000..5e7467ab --- /dev/null +++ b/frontend/src/api/utils.ts @@ -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(); +} diff --git a/frontend/src/types/api.d.ts b/frontend/src/types/api.d.ts new file mode 100644 index 00000000..79b6a23a --- /dev/null +++ b/frontend/src/types/api.d.ts @@ -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, "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 + diff --git a/frontend/src/types/file.d.ts b/frontend/src/types/file.d.ts new file mode 100644 index 00000000..69709c28 --- /dev/null +++ b/frontend/src/types/file.d.ts @@ -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 +} \ No newline at end of file diff --git a/frontend/src/types/layout.d.ts b/frontend/src/types/layout.d.ts new file mode 100644 index 00000000..0f741a49 --- /dev/null +++ b/frontend/src/types/layout.d.ts @@ -0,0 +1,5 @@ +interface LayoutValue { + prompt: boolean, + confirm: boolean, + action: boolean, +} \ No newline at end of file diff --git a/frontend/src/types/user.d.ts b/frontend/src/types/user.d.ts new file mode 100644 index 00000000..0f465757 --- /dev/null +++ b/frontend/src/types/user.d.ts @@ -0,0 +1,7 @@ +interface user { + id: number, + locale: string, + perm: any +} + +type userKey = keyof user \ No newline at end of file diff --git a/frontend/src/types/utils.d.ts b/frontend/src/types/utils.d.ts new file mode 100644 index 00000000..4a25eb50 --- /dev/null +++ b/frontend/src/types/utils.d.ts @@ -0,0 +1 @@ +type settings = any \ No newline at end of file diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..151aa685 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file