feat: migrate to vue 3 (#2689)
--------- Co-authored-by: Joep <jcbuhre@gmail.com> Co-authored-by: Omar Hussein <omarmohammad1951@gmail.com> Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
import store from "@/store";
|
||||
import router from "@/router";
|
||||
import { Base64 } from "js-base64";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
|
||||
export function parseToken(token) {
|
||||
const parts = token.split(".");
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("token malformed");
|
||||
}
|
||||
|
||||
const data = JSON.parse(Base64.decode(parts[1]));
|
||||
|
||||
document.cookie = `auth=${token}; path=/`;
|
||||
|
||||
localStorage.setItem("jwt", token);
|
||||
store.commit("setJWT", token);
|
||||
store.commit("setUser", data.user);
|
||||
}
|
||||
|
||||
export async function validateLogin() {
|
||||
try {
|
||||
if (localStorage.getItem("jwt")) {
|
||||
await renew(localStorage.getItem("jwt"));
|
||||
}
|
||||
} catch (_) {
|
||||
console.warn("Invalid JWT token in storage"); // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(username, password, recaptcha) {
|
||||
const data = { username, password, recaptcha };
|
||||
|
||||
const res = await fetch(`${baseURL}/api/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const body = await res.text();
|
||||
|
||||
if (res.status === 200) {
|
||||
parseToken(body);
|
||||
} else {
|
||||
throw new Error(body);
|
||||
}
|
||||
}
|
||||
|
||||
export async function renew(jwt) {
|
||||
const res = await fetch(`${baseURL}/api/renew`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Auth": jwt,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.text();
|
||||
|
||||
if (res.status === 200) {
|
||||
parseToken(body);
|
||||
} else {
|
||||
throw new Error(body);
|
||||
}
|
||||
}
|
||||
|
||||
export async function signup(username, password) {
|
||||
const data = { username, password };
|
||||
|
||||
const res = await fetch(`${baseURL}/api/signup`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
|
||||
|
||||
store.commit("setJWT", "");
|
||||
store.commit("setUser", null);
|
||||
localStorage.setItem("jwt", null);
|
||||
router.push({ path: "/login" });
|
||||
}
|
||||
102
frontend/src/utils/auth.ts
Normal file
102
frontend/src/utils/auth.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import router from "@/router";
|
||||
import { JwtPayload, jwtDecode } from "jwt-decode";
|
||||
import { baseURL } from "./constants";
|
||||
import { StatusError } from "@/api/utils";
|
||||
|
||||
export function parseToken(token: string) {
|
||||
// falsy or malformed jwt will throw InvalidTokenError
|
||||
const data = jwtDecode<JwtPayload & { user: IUser }>(token);
|
||||
|
||||
document.cookie = `auth=${token}; Path=/; SameSite=Strict;`;
|
||||
|
||||
localStorage.setItem("jwt", token);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
authStore.jwt = token;
|
||||
authStore.setUser(data.user);
|
||||
}
|
||||
|
||||
export async function validateLogin() {
|
||||
try {
|
||||
if (localStorage.getItem("jwt")) {
|
||||
await renew(<string>localStorage.getItem("jwt"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Invalid JWT token in storage"); // eslint-disable-line
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(
|
||||
username: string,
|
||||
password: string,
|
||||
recaptcha: string
|
||||
) {
|
||||
const data = { username, password, recaptcha };
|
||||
|
||||
const res = await fetch(`${baseURL}/api/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const body = await res.text();
|
||||
|
||||
if (res.status === 200) {
|
||||
parseToken(body);
|
||||
} else {
|
||||
throw new StatusError(
|
||||
body || `${res.status} ${res.statusText}`,
|
||||
res.status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function renew(jwt: string) {
|
||||
const res = await fetch(`${baseURL}/api/renew`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Auth": jwt,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.text();
|
||||
|
||||
if (res.status === 200) {
|
||||
parseToken(body);
|
||||
} else {
|
||||
throw new StatusError(
|
||||
body || `${res.status} ${res.statusText}`,
|
||||
res.status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function signup(username: string, password: string) {
|
||||
const data = { username, password };
|
||||
|
||||
const res = await fetch(`${baseURL}/api/signup`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
|
||||
}
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
document.cookie = "auth=; Max-Age=0; Path=/; SameSite=Strict;";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
authStore.clearUser();
|
||||
|
||||
localStorage.setItem("jwt", "");
|
||||
router.push({ path: "/login" });
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
function loading(button) {
|
||||
let el = document.querySelector(`#${button}-button > i`);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log('Error getting button ' + button) // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.icon = el.innerHTML;
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.add("spin");
|
||||
el.innerHTML = "autorenew";
|
||||
el.style.opacity = 1;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function done(button) {
|
||||
let el = document.querySelector(`#${button}-button > i`);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log('Error getting button ' + button) // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = el.dataset.icon;
|
||||
el.style.opacity = 1;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function success(button) {
|
||||
let el = document.querySelector(`#${button}-button > i`);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log('Error getting button ' + button) // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = "done";
|
||||
el.style.opacity = 1;
|
||||
|
||||
setTimeout(() => {
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.innerHTML = el.dataset.icon;
|
||||
el.style.opacity = 1;
|
||||
}, 100);
|
||||
}, 500);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export default {
|
||||
loading,
|
||||
done,
|
||||
success,
|
||||
};
|
||||
83
frontend/src/utils/buttons.ts
Normal file
83
frontend/src/utils/buttons.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
function loading(button: string) {
|
||||
const el: HTMLButtonElement | null = document.querySelector(
|
||||
`#${button}-button > i`
|
||||
);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.icon = el.innerHTML;
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el) {
|
||||
el.classList.add("spin");
|
||||
el.innerHTML = "autorenew";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function done(button: string) {
|
||||
const el: HTMLButtonElement | null = document.querySelector(
|
||||
`#${button}-button > i`
|
||||
);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = el?.dataset?.icon || "";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function success(button: string) {
|
||||
const el: HTMLButtonElement | null = document.querySelector(
|
||||
`#${button}-button > i`
|
||||
);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = "done";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (el) el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.innerHTML = el?.dataset?.icon || "";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}, 100);
|
||||
}, 500);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export default {
|
||||
loading,
|
||||
done,
|
||||
success,
|
||||
};
|
||||
66
frontend/src/utils/clipboard.ts
Normal file
66
frontend/src/utils/clipboard.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Based on code provided by Amir Fo
|
||||
// https://stackoverflow.com/a/74528564
|
||||
export function copy(text: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (
|
||||
typeof navigator !== "undefined" &&
|
||||
typeof navigator.clipboard !== "undefined" &&
|
||||
// @ts-ignore
|
||||
navigator.permissions !== "undefined"
|
||||
) {
|
||||
navigator.permissions
|
||||
// @ts-ignore
|
||||
.query({ name: "clipboard-write" })
|
||||
.then((permission) => {
|
||||
if (permission.state === "granted" || permission.state === "prompt") {
|
||||
const type = "text/plain";
|
||||
const blob = new Blob([text], { type });
|
||||
const data = [new ClipboardItem({ [type]: blob })];
|
||||
navigator.clipboard.write(data).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(new Error("Permission not granted!"));
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// Firefox doesnt support clipboard-write permission
|
||||
if (navigator.userAgent.indexOf("Firefox") != -1) {
|
||||
navigator.clipboard.writeText(text).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
document.queryCommandSupported &&
|
||||
document.queryCommandSupported("copy")
|
||||
) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.textContent = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.fontSize = "12pt";
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.width = "2em";
|
||||
textarea.style.height = "2em";
|
||||
textarea.style.padding = "0";
|
||||
textarea.style.margin = "0";
|
||||
textarea.style.border = "none";
|
||||
textarea.style.outline = "none";
|
||||
textarea.style.boxShadow = "none";
|
||||
textarea.style.background = "transparent";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
document.body.removeChild(textarea);
|
||||
reject(e);
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error("None of copying methods are supported by this browser!")
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
const name = window.FileBrowser.Name || "File Browser";
|
||||
const disableExternal = window.FileBrowser.DisableExternal;
|
||||
const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
|
||||
const baseURL = window.FileBrowser.BaseURL;
|
||||
const staticURL = window.FileBrowser.StaticURL;
|
||||
const recaptcha = window.FileBrowser.ReCaptcha;
|
||||
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
|
||||
const signup = window.FileBrowser.Signup;
|
||||
const version = window.FileBrowser.Version;
|
||||
const logoURL = `${staticURL}/img/logo.svg`;
|
||||
const noAuth = window.FileBrowser.NoAuth;
|
||||
const authMethod = window.FileBrowser.AuthMethod;
|
||||
const loginPage = window.FileBrowser.LoginPage;
|
||||
const theme = window.FileBrowser.Theme;
|
||||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||
const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const tusSettings = window.FileBrowser.TusSettings;
|
||||
const origin = window.location.origin;
|
||||
const tusEndpoint = `/api/tus`;
|
||||
|
||||
export {
|
||||
name,
|
||||
disableExternal,
|
||||
disableUsedPercentage,
|
||||
baseURL,
|
||||
logoURL,
|
||||
recaptcha,
|
||||
recaptchaKey,
|
||||
signup,
|
||||
version,
|
||||
noAuth,
|
||||
authMethod,
|
||||
loginPage,
|
||||
theme,
|
||||
enableThumbs,
|
||||
resizePreview,
|
||||
enableExec,
|
||||
tusSettings,
|
||||
origin,
|
||||
tusEndpoint,
|
||||
};
|
||||
42
frontend/src/utils/constants.ts
Normal file
42
frontend/src/utils/constants.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
const name: string = window.FileBrowser.Name || "File Browser";
|
||||
const disableExternal: boolean = window.FileBrowser.DisableExternal;
|
||||
const disableUsedPercentage: boolean = window.FileBrowser.DisableUsedPercentage;
|
||||
const baseURL: string = window.FileBrowser.BaseURL;
|
||||
const staticURL: string = window.FileBrowser.StaticURL;
|
||||
const recaptcha: string = window.FileBrowser.ReCaptcha;
|
||||
const recaptchaKey: string = window.FileBrowser.ReCaptchaKey;
|
||||
const signup: boolean = window.FileBrowser.Signup;
|
||||
const version: string = window.FileBrowser.Version;
|
||||
const logoURL = `${staticURL}/img/logo.svg`;
|
||||
const noAuth: boolean = window.FileBrowser.NoAuth;
|
||||
const authMethod = window.FileBrowser.AuthMethod;
|
||||
const loginPage: boolean = window.FileBrowser.LoginPage;
|
||||
const theme: UserTheme = window.FileBrowser.Theme;
|
||||
const enableThumbs: boolean = window.FileBrowser.EnableThumbs;
|
||||
const resizePreview: boolean = window.FileBrowser.ResizePreview;
|
||||
const enableExec: boolean = window.FileBrowser.EnableExec;
|
||||
const tusSettings = window.FileBrowser.TusSettings;
|
||||
const origin = window.location.origin;
|
||||
const tusEndpoint = `/api/tus`;
|
||||
|
||||
export {
|
||||
name,
|
||||
disableExternal,
|
||||
disableUsedPercentage,
|
||||
baseURL,
|
||||
logoURL,
|
||||
recaptcha,
|
||||
recaptchaKey,
|
||||
signup,
|
||||
version,
|
||||
noAuth,
|
||||
authMethod,
|
||||
loginPage,
|
||||
theme,
|
||||
enableThumbs,
|
||||
resizePreview,
|
||||
enableExec,
|
||||
tusSettings,
|
||||
origin,
|
||||
tusEndpoint,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function (name) {
|
||||
let re = new RegExp(
|
||||
export default function (name: string) {
|
||||
const re = new RegExp(
|
||||
"(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"
|
||||
);
|
||||
return document.cookie.replace(re, "$1");
|
||||
@@ -1,10 +1,10 @@
|
||||
export default function getRule(rules) {
|
||||
export default function getRule(rules: string[]) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
rules[i] = rules[i].toLowerCase();
|
||||
}
|
||||
|
||||
let result = null;
|
||||
let find = Array.prototype.find;
|
||||
const find = Array.prototype.find;
|
||||
|
||||
find.call(document.styleSheets, (styleSheet) => {
|
||||
result = find.call(styleSheet.cssRules, (cssRule) => {
|
||||
34
frontend/src/utils/theme.ts
Normal file
34
frontend/src/utils/theme.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { theme } from "./constants";
|
||||
|
||||
export const getTheme = (): UserTheme => {
|
||||
return (document.documentElement.className as UserTheme) || theme;
|
||||
};
|
||||
|
||||
export const setTheme = (theme: UserTheme) => {
|
||||
const html = document.documentElement;
|
||||
if (!theme) {
|
||||
html.className = getMediaPreference();
|
||||
} else {
|
||||
html.className = theme;
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleTheme = (): void => {
|
||||
const activeTheme = getTheme();
|
||||
if (activeTheme === "light") {
|
||||
setTheme("dark");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
};
|
||||
|
||||
export const getMediaPreference = (): UserTheme => {
|
||||
const hasDarkPreference = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
if (hasDarkPreference) {
|
||||
return "dark";
|
||||
} else {
|
||||
return "light";
|
||||
}
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import store from "@/store";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export function checkConflict(files, items) {
|
||||
if (typeof items === "undefined" || items === null) {
|
||||
items = [];
|
||||
}
|
||||
|
||||
let folder_upload = files[0].fullPath !== undefined;
|
||||
|
||||
let conflict = false;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
let name = file.name;
|
||||
|
||||
if (folder_upload) {
|
||||
let dirs = file.fullPath.split("/");
|
||||
if (dirs.length > 1) {
|
||||
name = dirs[0];
|
||||
}
|
||||
}
|
||||
|
||||
let res = items.findIndex(function hasConflict(element) {
|
||||
return element.name === this;
|
||||
}, name);
|
||||
|
||||
if (res >= 0) {
|
||||
conflict = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return conflict;
|
||||
}
|
||||
|
||||
export function scanFiles(dt) {
|
||||
return new Promise((resolve) => {
|
||||
let reading = 0;
|
||||
const contents = [];
|
||||
|
||||
if (dt.items !== undefined) {
|
||||
for (let item of dt.items) {
|
||||
if (
|
||||
item.kind === "file" &&
|
||||
typeof item.webkitGetAsEntry === "function"
|
||||
) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
readEntry(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolve(dt.files);
|
||||
}
|
||||
|
||||
function readEntry(entry, directory = "") {
|
||||
if (entry.isFile) {
|
||||
reading++;
|
||||
entry.file((file) => {
|
||||
reading--;
|
||||
|
||||
file.fullPath = `${directory}${file.name}`;
|
||||
contents.push(file);
|
||||
|
||||
if (reading === 0) {
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
const dir = {
|
||||
isDir: true,
|
||||
size: 0,
|
||||
fullPath: `${directory}${entry.name}`,
|
||||
name: entry.name,
|
||||
};
|
||||
|
||||
contents.push(dir);
|
||||
|
||||
readReaderContent(entry.createReader(), `${directory}${entry.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function readReaderContent(reader, directory) {
|
||||
reading++;
|
||||
|
||||
reader.readEntries(function (entries) {
|
||||
reading--;
|
||||
if (entries.length > 0) {
|
||||
for (const entry of entries) {
|
||||
readEntry(entry, `${directory}/`);
|
||||
}
|
||||
|
||||
readReaderContent(reader, `${directory}/`);
|
||||
}
|
||||
|
||||
if (reading === 0) {
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function detectType(mimetype) {
|
||||
if (mimetype.startsWith("video")) return "video";
|
||||
if (mimetype.startsWith("audio")) return "audio";
|
||||
if (mimetype.startsWith("image")) return "image";
|
||||
if (mimetype.startsWith("pdf")) return "pdf";
|
||||
if (mimetype.startsWith("text")) return "text";
|
||||
return "blob";
|
||||
}
|
||||
|
||||
export function handleFiles(files, base, overwrite = false) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let id = store.state.upload.id;
|
||||
let path = base;
|
||||
let file = files[i];
|
||||
|
||||
if (file.fullPath !== undefined) {
|
||||
path += url.encodePath(file.fullPath);
|
||||
} else {
|
||||
path += url.encodeRFC5987ValueChars(file.name);
|
||||
}
|
||||
|
||||
if (file.isDir) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
const item = {
|
||||
id,
|
||||
path,
|
||||
file,
|
||||
overwrite,
|
||||
...(!file.isDir && { type: detectType(file.type) }),
|
||||
};
|
||||
|
||||
store.dispatch("upload/upload", item);
|
||||
}
|
||||
}
|
||||
154
frontend/src/utils/upload.ts
Normal file
154
frontend/src/utils/upload.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export function checkConflict(
|
||||
files: UploadList,
|
||||
dest: ResourceItem[]
|
||||
): boolean {
|
||||
if (typeof dest === "undefined" || dest === null) {
|
||||
dest = [];
|
||||
}
|
||||
|
||||
const folder_upload = files[0].fullPath !== undefined;
|
||||
|
||||
const names = new Set<string>();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
let name = file.name;
|
||||
|
||||
if (folder_upload) {
|
||||
const dirs = file.fullPath?.split("/");
|
||||
if (dirs && dirs.length > 1) {
|
||||
name = dirs[0];
|
||||
}
|
||||
}
|
||||
|
||||
names.add(name);
|
||||
}
|
||||
|
||||
return dest.some((d) => names.has(d.name));
|
||||
}
|
||||
|
||||
export function scanFiles(dt: DataTransfer): Promise<UploadList | FileList> {
|
||||
return new Promise((resolve) => {
|
||||
let reading = 0;
|
||||
const contents: UploadList = [];
|
||||
|
||||
if (dt.items) {
|
||||
// ts didnt like the for of loop even tho
|
||||
// it is the official example on MDN
|
||||
// for (const item of dt.items) {
|
||||
for (let i = 0; i < dt.items.length; i++) {
|
||||
const item = dt.items[i];
|
||||
if (
|
||||
item.kind === "file" &&
|
||||
typeof item.webkitGetAsEntry === "function"
|
||||
) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
entry && readEntry(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolve(dt.files);
|
||||
}
|
||||
|
||||
function readEntry(entry: FileSystemEntry, directory = ""): void {
|
||||
if (entry.isFile) {
|
||||
reading++;
|
||||
(entry as FileSystemFileEntry).file((file) => {
|
||||
reading--;
|
||||
|
||||
contents.push({
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
isDir: false,
|
||||
fullPath: `${directory}${file.name}`,
|
||||
});
|
||||
|
||||
if (reading === 0) {
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
const dir = {
|
||||
isDir: true,
|
||||
size: 0,
|
||||
fullPath: `${directory}${entry.name}`,
|
||||
name: entry.name,
|
||||
};
|
||||
|
||||
contents.push(dir);
|
||||
|
||||
readReaderContent(
|
||||
(entry as FileSystemDirectoryEntry).createReader(),
|
||||
`${directory}${entry.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function readReaderContent(
|
||||
reader: FileSystemDirectoryReader,
|
||||
directory: string
|
||||
): void {
|
||||
reading++;
|
||||
|
||||
reader.readEntries((entries) => {
|
||||
reading--;
|
||||
if (entries.length > 0) {
|
||||
for (const entry of entries) {
|
||||
readEntry(entry, `${directory}/`);
|
||||
}
|
||||
|
||||
readReaderContent(reader, `${directory}/`);
|
||||
}
|
||||
|
||||
if (reading === 0) {
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function detectType(mimetype: string): ResourceType {
|
||||
if (mimetype.startsWith("video")) return "video";
|
||||
if (mimetype.startsWith("audio")) return "audio";
|
||||
if (mimetype.startsWith("image")) return "image";
|
||||
if (mimetype.startsWith("pdf")) return "pdf";
|
||||
if (mimetype.startsWith("text")) return "text";
|
||||
return "blob";
|
||||
}
|
||||
|
||||
export function handleFiles(
|
||||
files: UploadList,
|
||||
base: string,
|
||||
overwrite = false
|
||||
) {
|
||||
const uploadStore = useUploadStore();
|
||||
|
||||
for (const file of files) {
|
||||
const id = uploadStore.id;
|
||||
let path = base;
|
||||
|
||||
if (file.fullPath !== undefined) {
|
||||
path += url.encodePath(file.fullPath);
|
||||
} else {
|
||||
path += url.encodeRFC5987ValueChars(file.name);
|
||||
}
|
||||
|
||||
if (file.isDir) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
const item: UploadItem = {
|
||||
id,
|
||||
path,
|
||||
file,
|
||||
overwrite,
|
||||
...(!file.isDir && { type: detectType((file.file as File).type) }),
|
||||
};
|
||||
|
||||
uploadStore.upload(item);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
export function removeLastDir(url) {
|
||||
var arr = url.split("/");
|
||||
if (arr.pop() === "") {
|
||||
arr.pop();
|
||||
}
|
||||
|
||||
return arr.join("/");
|
||||
}
|
||||
|
||||
// this code borrow from mozilla
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
|
||||
export function encodeRFC5987ValueChars(str) {
|
||||
return (
|
||||
encodeURIComponent(str)
|
||||
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
||||
// so we do not need to escape it
|
||||
.replace(/['()]/g, escape) // i.e., %27 %28 %29
|
||||
.replace(/\*/g, "%2A")
|
||||
// The following are not required for percent-encoding per RFC5987,
|
||||
// so we can allow for a little better readability over the wire: |`^
|
||||
.replace(/%(?:7C|60|5E)/g, unescape)
|
||||
);
|
||||
}
|
||||
|
||||
export function encodePath(str) {
|
||||
return str
|
||||
.split("/")
|
||||
.map((v) => encodeURIComponent(v))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export default {
|
||||
encodeRFC5987ValueChars,
|
||||
removeLastDir,
|
||||
encodePath,
|
||||
};
|
||||
42
frontend/src/utils/url.ts
Normal file
42
frontend/src/utils/url.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export function removeLastDir(url: string) {
|
||||
const arr = url.split("/");
|
||||
if (arr.pop() === "") {
|
||||
arr.pop();
|
||||
}
|
||||
|
||||
return arr.join("/");
|
||||
}
|
||||
|
||||
// this function is taken from mozilla
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
|
||||
export function encodeRFC5987ValueChars(str: string) {
|
||||
return (
|
||||
encodeURIComponent(str)
|
||||
// The following creates the sequences %27 %28 %29 %2A (Note that
|
||||
// the valid encoding of "*" is %2A, which necessitates calling
|
||||
// toUpperCase() to properly encode). Although RFC3986 reserves "!",
|
||||
// RFC5987 does not, so we do not need to escape it.
|
||||
.replace(
|
||||
/['()*]/g,
|
||||
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||
)
|
||||
// The following are not required for percent-encoding per RFC5987,
|
||||
// so we can allow for a little better readability over the wire: |`^
|
||||
.replace(/%(7C|60|5E)/g, (str, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function encodePath(str: string) {
|
||||
return str
|
||||
.split("/")
|
||||
.map((v) => encodeURIComponent(v))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export default {
|
||||
encodeRFC5987ValueChars,
|
||||
removeLastDir,
|
||||
encodePath,
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import Vue from "vue";
|
||||
import Noty from "noty";
|
||||
import VueLazyload from "vue-lazyload";
|
||||
import i18n from "@/i18n";
|
||||
import { disableExternal } from "@/utils/constants";
|
||||
import AsyncComputed from "vue-async-computed";
|
||||
|
||||
Vue.use(VueLazyload);
|
||||
Vue.use(AsyncComputed);
|
||||
|
||||
Vue.config.productionTip = true;
|
||||
|
||||
const notyDefault = {
|
||||
type: "info",
|
||||
layout: "bottomRight",
|
||||
timeout: 1000,
|
||||
progressBar: true,
|
||||
};
|
||||
|
||||
Vue.prototype.$noty = (opts) => {
|
||||
new Noty(Object.assign({}, notyDefault, opts)).show();
|
||||
};
|
||||
|
||||
Vue.prototype.$showSuccess = (message) => {
|
||||
new Noty(
|
||||
Object.assign({}, notyDefault, {
|
||||
text: message,
|
||||
type: "success",
|
||||
})
|
||||
).show();
|
||||
};
|
||||
|
||||
Vue.prototype.$showError = (error, displayReport = true) => {
|
||||
let btns = [
|
||||
Noty.button(i18n.t("buttons.close"), "", function () {
|
||||
n.close();
|
||||
}),
|
||||
];
|
||||
|
||||
if (!disableExternal && displayReport) {
|
||||
btns.unshift(
|
||||
Noty.button(i18n.t("buttons.reportIssue"), "", function () {
|
||||
window.open(
|
||||
"https://github.com/filebrowser/filebrowser/issues/new/choose"
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let n = new Noty(
|
||||
Object.assign({}, notyDefault, {
|
||||
text: error.message || error,
|
||||
type: "error",
|
||||
timeout: null,
|
||||
buttons: btns,
|
||||
})
|
||||
);
|
||||
|
||||
n.show();
|
||||
};
|
||||
|
||||
Vue.directive("focus", {
|
||||
inserted: function (el) {
|
||||
el.focus();
|
||||
},
|
||||
});
|
||||
|
||||
export default Vue;
|
||||
Reference in New Issue
Block a user