mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
chore (rewrite): upload component
This commit is contained in:
parent
11a01420db
commit
87c41adfe7
9 changed files with 553 additions and 452 deletions
|
|
@ -6,6 +6,9 @@ import { report } from "../helpers/log.js";
|
||||||
|
|
||||||
export default async function main() {
|
export default async function main() {
|
||||||
try {
|
try {
|
||||||
|
let config = {};
|
||||||
|
// await Config.refresh()
|
||||||
|
|
||||||
await Promise.all([ // procedure with no outside dependencies
|
await Promise.all([ // procedure with no outside dependencies
|
||||||
setup_translation(),
|
setup_translation(),
|
||||||
setup_xdg_open(),
|
setup_xdg_open(),
|
||||||
|
|
@ -16,10 +19,10 @@ export default async function main() {
|
||||||
setup_loader(),
|
setup_loader(),
|
||||||
setup_history(),
|
setup_history(),
|
||||||
]);
|
]);
|
||||||
// await Config.refresh()
|
|
||||||
|
|
||||||
await Promise.all([ // procedure with dependency on config
|
await Promise.all([ // procedure with dependency on config
|
||||||
// setup_chromecast() // TODO
|
// setup_chromecast() // TODO
|
||||||
|
setup_base(config),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
window.dispatchEvent(new window.Event("pagechange"));
|
window.dispatchEvent(new window.Event("pagechange"));
|
||||||
|
|
@ -68,6 +71,13 @@ async function setup_device() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setup_base(config) {
|
||||||
|
// TODO: base as config in admin
|
||||||
|
const $meta = document.createElement("base");
|
||||||
|
$meta.setAttribute("href", location.origin);
|
||||||
|
document.head.appendChild($meta);
|
||||||
|
}
|
||||||
|
|
||||||
// async function setup_sw() {
|
// async function setup_sw() {
|
||||||
// if (!("serviceWorker" in window.navigator)) return;
|
// if (!("serviceWorker" in window.navigator)) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ export async function navigate(href) {
|
||||||
const trimPrefix = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
const trimPrefix = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
||||||
|
|
||||||
export function currentRoute(r, notFoundRoute) {
|
export function currentRoute(r, notFoundRoute) {
|
||||||
const currentRoute = "/" + trimPrefix(
|
const currentRoute = trimPrefix(
|
||||||
window.location.pathname,
|
window.location.pathname,
|
||||||
window.document.head.querySelector("base")?.getAttribute("href") || "/"
|
window.document.head.querySelector("base")?.getAttribute("href") || ""
|
||||||
);
|
);
|
||||||
for (const routeKey in r) {
|
for (const routeKey in r) {
|
||||||
if (new RegExp("^" + routeKey + "$").test(currentRoute)) {
|
if (new RegExp("^" + routeKey + "$").test(currentRoute)) {
|
||||||
|
|
|
||||||
|
|
@ -1,350 +0,0 @@
|
||||||
import { onDestroy } from "../../lib/skeleton/index.js";
|
|
||||||
import rxjs from "../../lib/rx.js";
|
|
||||||
import fscache from "./cache.js";
|
|
||||||
import { extractPath, isDir } from "./helper.js";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The transcient cache is used to rerender the list of files in a particular location. That's used
|
|
||||||
* when the user is doing either of a touch, mkdir, rm or mv. It is split onto 2 parts:
|
|
||||||
* - the virtualFiles$: which are things we want to display in addition to what is currently
|
|
||||||
* visible on the screen
|
|
||||||
* - the mutationFiles$: which are things already on the screen which we need to mutate. For
|
|
||||||
* example when we want a particular file to show a loading spinner
|
|
||||||
*/
|
|
||||||
|
|
||||||
const virtualFiles$ = new rxjs.BehaviorSubject({
|
|
||||||
// "/tmp/": [],
|
|
||||||
// "/home/": [{ name: "test", type: "directory" }]
|
|
||||||
});
|
|
||||||
const mutationFiles$ = new rxjs.BehaviorSubject({
|
|
||||||
// "/home/": [{ name: "test", fn: (file) => file, ...]
|
|
||||||
});
|
|
||||||
|
|
||||||
export function touch(ajax$, path) {
|
|
||||||
const [basepath, filename] = extractPath(path);
|
|
||||||
const file = {
|
|
||||||
name: filename,
|
|
||||||
type: "file",
|
|
||||||
size: 0,
|
|
||||||
time: new Date().getTime(),
|
|
||||||
};
|
|
||||||
stateAdd(virtualFiles$, basepath, {
|
|
||||||
...file,
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
const onSuccess = async () => {
|
|
||||||
removeLoading(virtualFiles$, basepath, filename);
|
|
||||||
onDestroy(() => statePop(virtualFiles$, basepath, filename));
|
|
||||||
await fscache().update(basepath, ({ files, ...rest }) => ({
|
|
||||||
files: files.concat([file]),
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
const onFailure = (err, caught) => {
|
|
||||||
statePop(virtualFiles$, basepath, filename);
|
|
||||||
return rxjs.of(fscache().remove(basepath)).pipe(
|
|
||||||
rxjs.mergeMap(() => rxjs.EMPTY),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return ajax$.pipe(
|
|
||||||
rxjs.mergeMap(onSuccess),
|
|
||||||
rxjs.catchError(onFailure),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mkdir(ajax$, path) {
|
|
||||||
const [basepath, dirname] = extractPath(path);
|
|
||||||
const file = {
|
|
||||||
name: dirname,
|
|
||||||
type: "directory",
|
|
||||||
size: 0,
|
|
||||||
time: new Date().getTime(),
|
|
||||||
};
|
|
||||||
stateAdd(virtualFiles$, basepath, {
|
|
||||||
...file,
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
const onSuccess = async () => {
|
|
||||||
removeLoading(virtualFiles$, basepath, dirname);
|
|
||||||
onDestroy(() => statePop(virtualFiles$, basepath, dirname));
|
|
||||||
await fscache().update(basepath, ({ files, ...rest }) => ({
|
|
||||||
files: files.concat([file]),
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
const onFailure = () => {
|
|
||||||
statePop(virtualFiles$, basepath, dirname);
|
|
||||||
return rxjs.of(fscache().remove(basepath)).pipe(
|
|
||||||
rxjs.mergeMap(() => rxjs.EMPTY),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return ajax$.pipe(
|
|
||||||
rxjs.mergeMap(onSuccess),
|
|
||||||
rxjs.catchError(onFailure),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function save(ajax$, path, size) {
|
|
||||||
const [basepath, filename] = extractPath(path);
|
|
||||||
const file = {
|
|
||||||
name: dirname,
|
|
||||||
type: "file",
|
|
||||||
size,
|
|
||||||
time: new Date().getTime(),
|
|
||||||
};
|
|
||||||
stateAdd(virtualFiles$, basepath, {
|
|
||||||
...file,
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
const onSuccess = async () => {
|
|
||||||
removeLoading(virtualFiles$, basepath, filename);
|
|
||||||
onDestroy(() => statePop(virtualFiles$, basepath, filename));
|
|
||||||
await fscache().update(basepath, ({ files, ...rest }) => ({
|
|
||||||
files: files.concat([file]),
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
const onFailure = () => {
|
|
||||||
statePop(virtualFiles$, basepath, dirname);
|
|
||||||
return rxjs.EMPTY;
|
|
||||||
};
|
|
||||||
return ajax$.pipe(
|
|
||||||
rxjs.tap(onSuccess),
|
|
||||||
rxjs.catchError(onFailure),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rm(ajax$, ...paths) {
|
|
||||||
if (paths.length === 0) return rxjs.of(null);
|
|
||||||
const arr = new Array(paths.length * 2);
|
|
||||||
let basepath = null;
|
|
||||||
for (let i=0;i<paths.length;i++) {
|
|
||||||
[arr[2*i], arr[2*i+1]] = extractPath(paths[i]);
|
|
||||||
if (i === 0) basepath = arr[2*i];
|
|
||||||
else if (basepath !== arr[2*i]) throw new Error("NOT_IMPLEMENTED");
|
|
||||||
}
|
|
||||||
stateAdd(mutationFiles$, basepath, {
|
|
||||||
name: basepath,
|
|
||||||
fn: (file) => {
|
|
||||||
for (let i=0;i<arr.length;i+=2) {
|
|
||||||
if (file.name === arr[i+1]) {
|
|
||||||
file.loading = true;
|
|
||||||
file.last = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const onSuccess = async () => {
|
|
||||||
stateAdd(mutationFiles$, basepath, {
|
|
||||||
name: basepath,
|
|
||||||
fn: (file) => {
|
|
||||||
for (let i=0;i<arr.length;i+=2) {
|
|
||||||
if (file.name === arr[i+1]) return null;
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
onDestroy(() => statePop(mutationFiles$, basepath, basepath));
|
|
||||||
await Promise.all(paths.map((path) => fscache().remove(path, false)));
|
|
||||||
await fscache().update(basepath, ({ files, ...rest }) => ({
|
|
||||||
files: files.filter(({ name }) => {
|
|
||||||
for (let i=0;i<arr.length;i+=2) {
|
|
||||||
if (name === arr[i+1]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
const onFailure = () => {
|
|
||||||
stateAdd(mutationFiles$, basepath, {
|
|
||||||
name: basepath,
|
|
||||||
fn: (file) => {
|
|
||||||
for (let i=0;i<arr.length;i+=2) {
|
|
||||||
if (file.name === arr[i+1]) {
|
|
||||||
delete file.loading;
|
|
||||||
delete file.last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return rxjs.EMPTY;
|
|
||||||
};
|
|
||||||
return ajax$.pipe(
|
|
||||||
rxjs.tap(onSuccess),
|
|
||||||
rxjs.catchError(onFailure),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mv(ajax$, fromPath, toPath) {
|
|
||||||
const [fromBasepath, fromName] = extractPath(fromPath);
|
|
||||||
const [toBasepath, toName] = extractPath(toPath);
|
|
||||||
let type = null;
|
|
||||||
if (fromBasepath === toBasepath) {
|
|
||||||
stateAdd(mutationFiles$, fromBasepath, {
|
|
||||||
name: fromName,
|
|
||||||
fn: (file) => {
|
|
||||||
if (file.name === fromName) {
|
|
||||||
file.loading = true;
|
|
||||||
file.name = toName;
|
|
||||||
type = file.type;
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
stateAdd(mutationFiles$, fromBasepath, {
|
|
||||||
name: fromName,
|
|
||||||
fn: (file) => {
|
|
||||||
if (file.name === fromName) {
|
|
||||||
file.loading = true;
|
|
||||||
file.last = true;
|
|
||||||
type = file.type;
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
stateAdd(virtualFiles$, toBasepath, {
|
|
||||||
name: toName,
|
|
||||||
loading: true,
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const onSuccess = async () => {
|
|
||||||
fscache().remove(fromPath, false);
|
|
||||||
if (fromBasepath === toBasepath) {
|
|
||||||
stateAdd(mutationFiles$, fromBasepath, {
|
|
||||||
name: fromName,
|
|
||||||
fn: (file) => {
|
|
||||||
if (file.name === toName) delete file.loading;
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fscache().update(fromBasepath, ({ files, ...rest }) => {
|
|
||||||
return {
|
|
||||||
files: files.map((file) => {
|
|
||||||
if (file.name === fromName) {
|
|
||||||
file.name = toName;
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
}),
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
stateAdd(mutationFiles$, fromBasepath, {
|
|
||||||
name: fromName,
|
|
||||||
fn: (file) => {
|
|
||||||
if (file.name === fromName) return null;
|
|
||||||
return file;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
onDestroy(() => statePop(mutationFiles$, fromBasepath, fromName));
|
|
||||||
statePop(virtualFiles$, toBasepath, toName);
|
|
||||||
await fscache().update(fromBasepath, ({ files, ...rest }) => ({
|
|
||||||
files: files.filter((file) => file.name === fromName ? false : true),
|
|
||||||
...rest,
|
|
||||||
}))
|
|
||||||
await fscache().update(toBasepath, ({ files, ...rest }) => ({
|
|
||||||
files: files.concat([{
|
|
||||||
name: fromName,
|
|
||||||
time: new Date().getTime(),
|
|
||||||
type,
|
|
||||||
}]),
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
if (isDir(fromPath)) await fscache.remove(fromPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onFailure = () => {
|
|
||||||
statePop(mutationFiles$, fromBasepath, fromName);
|
|
||||||
if (fromBasepath !== toBasepath) {
|
|
||||||
statePop(virtualFiles$, toBasepath, toName);
|
|
||||||
}
|
|
||||||
return rxjs.EMPTY;
|
|
||||||
};
|
|
||||||
return ajax$.pipe(
|
|
||||||
rxjs.tap(onSuccess),
|
|
||||||
rxjs.catchError(onFailure),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ls(path) {
|
|
||||||
return rxjs.pipe(
|
|
||||||
// case1: virtual files = additional files we want to see displayed in the UI
|
|
||||||
rxjs.switchMap(({ files, ...res }) => virtualFiles$.pipe(rxjs.mergeMap((virtualFiles) => {
|
|
||||||
const shouldContinue = !!(virtualFiles[path] && virtualFiles[path].length > 0);
|
|
||||||
if (!shouldContinue) return rxjs.of({ ...res, files });
|
|
||||||
return rxjs.of({
|
|
||||||
...res,
|
|
||||||
files: files.concat(virtualFiles[path]),
|
|
||||||
});
|
|
||||||
}))),
|
|
||||||
// case2: file mutation = update a file state, typically to add a loading state to an
|
|
||||||
// file or remove it entirely
|
|
||||||
rxjs.switchMap(({ files, ...res }) => mutationFiles$.pipe(rxjs.mergeMap((fns) => {
|
|
||||||
const shouldContinue = !!(fns[path] && fns[path].length > 0);
|
|
||||||
if (!shouldContinue) return rxjs.of({ ...res, files });
|
|
||||||
for (let i=files.length-1; i>=0; i--) {
|
|
||||||
for (let j=0; j<fns[path].length; j++) {
|
|
||||||
files[i] = fns[path][j].fn(files[i]);
|
|
||||||
if (!files[i]) {
|
|
||||||
files.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rxjs.of({ ...res, files });
|
|
||||||
}))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stateAdd(behavior, path, obj) {
|
|
||||||
let arr = behavior.value[path];
|
|
||||||
if (!arr) arr = [];
|
|
||||||
let alreadyKnown = false;
|
|
||||||
for (let i=0; i<arr.length; i++) {
|
|
||||||
if (arr[i].name === obj.name) {
|
|
||||||
alreadyKnown = true;
|
|
||||||
arr[i] = obj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!alreadyKnown) arr.push(obj);
|
|
||||||
behavior.next({
|
|
||||||
...behavior.value,
|
|
||||||
[path]: arr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function statePop(behavior, path, filename) {
|
|
||||||
const arr = behavior.value[path];
|
|
||||||
if (!arr) return;
|
|
||||||
const newArr = arr.filter(({ name }) => name !== filename)
|
|
||||||
if (newArr.length === 0) {
|
|
||||||
const newState = { ...behavior.value };
|
|
||||||
delete newState[path];
|
|
||||||
behavior.next(newState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
behavior.next({
|
|
||||||
...behavior.value,
|
|
||||||
[path]: newArr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLoading(behavior, path, filename) {
|
|
||||||
const arr = behavior.value[path];
|
|
||||||
if (!arr) return;
|
|
||||||
virtualFiles$.next({
|
|
||||||
...virtualFiles$.value,
|
|
||||||
[path]: arr.map((file) => {
|
|
||||||
if (file.name === filename) delete file.loading;
|
|
||||||
return file;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,17 @@ import { loadCSS } from "../../helpers/loader.js";
|
||||||
|
|
||||||
import { getAction$, setAction } from "./state_newthing.js";
|
import { getAction$, setAction } from "./state_newthing.js";
|
||||||
import { currentPath } from "./helper.js";
|
import { currentPath } from "./helper.js";
|
||||||
import { mkdir, touch } from "./model_files.js";
|
import { mkdir as mkdir$, touch as touch$ } from "./model_files.js";
|
||||||
|
import { mkdir as mkdirVL, touch as touchVL, withVirtualLayer } from "./model_virtual_layer.js";
|
||||||
|
|
||||||
|
const touch = (path) => withVirtualLayer(
|
||||||
|
touch$(path),
|
||||||
|
touchVL(path),
|
||||||
|
);
|
||||||
|
const mkdir = (path) => withVirtualLayer(
|
||||||
|
mkdir$(path),
|
||||||
|
mkdirVL(path),
|
||||||
|
);
|
||||||
|
|
||||||
export default async function(render) {
|
export default async function(render) {
|
||||||
const $node = createElement(`
|
const $node = createElement(`
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,26 @@ import { getAction$, setAction } from "./state_newthing.js";
|
||||||
import { setState, getState$ } from "./state_config.js";
|
import { setState, getState$ } from "./state_config.js";
|
||||||
import { clearCache } from "./cache.js";
|
import { clearCache } from "./cache.js";
|
||||||
import { getPermission, calculatePermission } from "./model_acl.js";
|
import { getPermission, calculatePermission } from "./model_acl.js";
|
||||||
import { rm, mv } from "./model_files.js";
|
|
||||||
import { currentPath, extractPath } from "./helper.js";
|
import { currentPath, extractPath } from "./helper.js";
|
||||||
|
|
||||||
|
import { rm as rm$, mv as mv$ } from "./model_files.js";
|
||||||
|
import { rm as rmVL, mv as mvVL, withVirtualLayer } from "./model_virtual_layer.js";
|
||||||
|
|
||||||
const modalOpt = {
|
const modalOpt = {
|
||||||
withButtonsRight: t("OK"),
|
withButtonsRight: t("OK"),
|
||||||
withButtonsLeft: t("CANCEL"),
|
withButtonsLeft: t("CANCEL"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rm = (...paths) => withVirtualLayer(
|
||||||
|
rm$(...paths),
|
||||||
|
rmVL(...paths),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mv = (from, to) => withVirtualLayer(
|
||||||
|
mv$(from, to),
|
||||||
|
mvVL(from, to),
|
||||||
|
);
|
||||||
|
|
||||||
export default async function(render) {
|
export default async function(render) {
|
||||||
const $page = createElement(`
|
const $page = createElement(`
|
||||||
<div class="component_submenu container">
|
<div class="component_submenu container">
|
||||||
|
|
@ -127,7 +139,7 @@ function componentLeft(render, { $scroll }) {
|
||||||
createModal(modalOpt),
|
createModal(modalOpt),
|
||||||
basename(expandSelection()[0].path.replace(new RegExp("/$"), "")),
|
basename(expandSelection()[0].path.replace(new RegExp("/$"), "")),
|
||||||
)),
|
)),
|
||||||
rxjs.mergeMap((val) => {
|
rxjs.mergeMap((val) => { // TODO: migrate to transcient impl
|
||||||
const path = expandSelection()[0].path;
|
const path = expandSelection()[0].path;
|
||||||
const [basepath, filename] = extractPath(path);
|
const [basepath, filename] = extractPath(path);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
@ -141,7 +153,7 @@ function componentLeft(render, { $scroll }) {
|
||||||
createModal(modalOpt),
|
createModal(modalOpt),
|
||||||
basename(expandSelection()[0].path.replace(new RegExp("/$"), "")).substr(0, 15),
|
basename(expandSelection()[0].path.replace(new RegExp("/$"), "")).substr(0, 15),
|
||||||
)),
|
)),
|
||||||
rxjs.mergeMap((val) => {
|
rxjs.mergeMap((val) => { // TODO: migrate to transcient impl
|
||||||
const selection = expandSelection()[0].path;
|
const selection = expandSelection()[0].path;
|
||||||
clearSelection();
|
clearSelection();
|
||||||
clearCache(selection);
|
clearCache(selection);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
padding: 20px 20px 5px 20px;
|
padding: 20px 20px 5px 20px;
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: 100;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
.component_upload h2 .percent {
|
.component_upload h2 .percent {
|
||||||
color: var(--emphasis-primary);
|
color: var(--emphasis-primary);
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,26 @@ import { loadCSS } from "../../helpers/loader.js";
|
||||||
import { qs } from "../../lib/dom.js";
|
import { qs } from "../../lib/dom.js";
|
||||||
import { AjaxError } from "../../lib/error.js";
|
import { AjaxError } from "../../lib/error.js";
|
||||||
import assert from "../../lib/assert.js";
|
import assert from "../../lib/assert.js";
|
||||||
|
import { currentPath } from "./helper.js";
|
||||||
|
import { mkdir, save } from "./model_virtual_layer.js";
|
||||||
import t from "../../locales/index.js";
|
import t from "../../locales/index.js";
|
||||||
|
|
||||||
|
const workers$ = new rxjs.BehaviorSubject({ tasks: [], size: null });
|
||||||
|
|
||||||
export default function(render) {
|
export default function(render) {
|
||||||
const $page = createFragment(`
|
const $page = createFragment(`
|
||||||
<div is="component_filezone"></div>
|
<div is="component_filezone"></div>
|
||||||
<div is="component_upload_fab"></div>
|
<div is="component_upload_fab"></div>
|
||||||
`);
|
`);
|
||||||
const tasks$ = new rxjs.BehaviorSubject([]);
|
|
||||||
|
|
||||||
if (!document.querySelector(`[is="component_upload_queue"]`)) {
|
if (!document.querySelector(`[is="component_upload_queue"]`)) {
|
||||||
const $queue = createElement(`<div is="component_upload_queue"></div>`);
|
const $queue = createElement(`<div is="component_upload_queue"></div>`);
|
||||||
document.body.appendChild($queue);
|
document.body.appendChild($queue);
|
||||||
componentUploadQueue(createRender($queue), { tasks$ });
|
componentUploadQueue(createRender($queue), { workers$ });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentFilezone(createRender($page.children[0]), { tasks$ });
|
componentFilezone(createRender($page.children[0]), { workers$ });
|
||||||
componentUploadFAB(createRender($page.children[1]), { tasks$ });
|
componentUploadFAB(createRender($page.children[1]), { workers$ });
|
||||||
render($page);
|
render($page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,7 +32,7 @@ export function init() {
|
||||||
return loadCSS(import.meta.url, "./ctrl_upload.css");
|
return loadCSS(import.meta.url, "./ctrl_upload.css");
|
||||||
}
|
}
|
||||||
|
|
||||||
function componentUploadFAB(render, { tasks$ }) {
|
function componentUploadFAB(render, { workers$ }) {
|
||||||
const $page = createElement(`
|
const $page = createElement(`
|
||||||
<div class="component_mobilefileupload no-select">
|
<div class="component_mobilefileupload no-select">
|
||||||
<form>
|
<form>
|
||||||
|
|
@ -46,17 +49,18 @@ function componentUploadFAB(render, { tasks$ }) {
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
effect(rxjs.fromEvent(qs($page, `input[type="file"]`), "change").pipe(
|
effect(rxjs.fromEvent(qs($page, `input[type="file"]`), "change").pipe(
|
||||||
rxjs.tap(async (e) => tasks$.next(await processFiles(e.target.files)))
|
rxjs.tap(async (e) => workers$.next(await processFiles(e.target.files))),
|
||||||
));
|
));
|
||||||
render($page);
|
render($page);
|
||||||
}
|
}
|
||||||
|
|
||||||
function componentFilezone(render, { tasks$ }) {
|
function componentFilezone(render, { workers$ }) {
|
||||||
const $target = document.body.querySelector(`[data-bind="filemanager-children"]`);
|
const $target = document.body.querySelector(`[data-bind="filemanager-children"]`);
|
||||||
$target.ondragenter = (e) => {
|
$target.ondragenter = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
$target.classList.add("dropzone");
|
$target.classList.add("dropzone");
|
||||||
|
e.dataTransfer.setData("type", "fileupload");
|
||||||
};
|
};
|
||||||
$target.ondragover = (e) => {
|
$target.ondragover = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -68,9 +72,13 @@ function componentFilezone(render, { tasks$ }) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const loadID = setTimeout(() => render(createElement("<div>LOADING</div>")), 2000);
|
const loadID = setTimeout(() => render(createElement("<div>LOADING</div>")), 2000);
|
||||||
if (e.dataTransfer.items instanceof window.DataTransferItemList) tasks$.next(await processItems(e.dataTransfer.items));
|
if (e.dataTransfer.items instanceof window.DataTransferItemList) {
|
||||||
else if (e.dataTransfer.files instanceof window.FileList) tasks$.next(await processFiles(e.dataTransfer.files));
|
workers$.next(await processItems(e.dataTransfer.items));
|
||||||
else assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry);
|
} else if (e.dataTransfer.files instanceof window.FileList) {
|
||||||
|
workers$.next(await processFiles(e.dataTransfer.files));
|
||||||
|
} else {
|
||||||
|
assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry);
|
||||||
|
}
|
||||||
$target.classList.remove("dropzone");
|
$target.classList.remove("dropzone");
|
||||||
clearTimeout(loadID);
|
clearTimeout(loadID);
|
||||||
render(createFragment(""));
|
render(createFragment(""));
|
||||||
|
|
@ -79,7 +87,7 @@ function componentFilezone(render, { tasks$ }) {
|
||||||
|
|
||||||
const MAX_WORKERS = 4;
|
const MAX_WORKERS = 4;
|
||||||
|
|
||||||
function componentUploadQueue(render, { tasks$ }) {
|
function componentUploadQueue(render, { workers$ }) {
|
||||||
const $page = createElement(`
|
const $page = createElement(`
|
||||||
<div class="component_upload hidden">
|
<div class="component_upload hidden">
|
||||||
<h2 class="no-select">${t("Current Upload")}
|
<h2 class="no-select">${t("Current Upload")}
|
||||||
|
|
@ -103,14 +111,15 @@ function componentUploadQueue(render, { tasks$ }) {
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// feature1: close the queue and stop the upload
|
// feature1: close the queue
|
||||||
effect(onClick(qs($page, `img[alt="close"]`)).pipe(
|
onClick(qs($page, `img[alt="close"]`)).pipe(
|
||||||
rxjs.mergeMap(() => animate($page, { time: 200, keyframes: slideYOut(50) })),
|
rxjs.mergeMap(() => animate($page, { time: 200, keyframes: slideYOut(50) })),
|
||||||
rxjs.tap(() => $page.classList.add("hidden")),
|
rxjs.tap(() => $page.classList.add("hidden")),
|
||||||
));
|
).subscribe();
|
||||||
|
|
||||||
// feature2: setup the task queue in the dom
|
// feature2: setup the task queue in the dom
|
||||||
effect(tasks$.asObservable().pipe(rxjs.tap((tasks) => {
|
workers$.subscribe(({ tasks }) => {
|
||||||
|
console.log("TASKS SETUP DOM", tasks);
|
||||||
if (tasks.length === 0) return;
|
if (tasks.length === 0) return;
|
||||||
$page.classList.remove("hidden");
|
$page.classList.remove("hidden");
|
||||||
const $fragment = document.createDocumentFragment();
|
const $fragment = document.createDocumentFragment();
|
||||||
|
|
@ -123,7 +132,7 @@ function componentUploadQueue(render, { tasks$ }) {
|
||||||
$task.firstElementChild.nextElementSibling.textContent = t("Waiting");
|
$task.firstElementChild.nextElementSibling.textContent = t("Waiting");
|
||||||
}
|
}
|
||||||
$content.appendChild($fragment);
|
$content.appendChild($fragment);
|
||||||
})));
|
});
|
||||||
|
|
||||||
// feature3: process tasks
|
// feature3: process tasks
|
||||||
const $icon = createElement(`<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgdmlld0JveD0iMCAwIDM4NCA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggc3R5bGU9ImZpbGw6ICM2MjY0Njk7IiBkPSJNMCAxMjhDMCA5Mi43IDI4LjcgNjQgNjQgNjRIMzIwYzM1LjMgMCA2NCAyOC43IDY0IDY0VjM4NGMwIDM1LjMtMjguNyA2NC02NCA2NEg2NGMtMzUuMyAwLTY0LTI4LjctNjQtNjRWMTI4eiIgLz4KPC9zdmc+Cg==" alt="stop" title="${t("Aborted")}">`)
|
const $icon = createElement(`<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgdmlld0JveD0iMCAwIDM4NCA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggc3R5bGU9ImZpbGw6ICM2MjY0Njk7IiBkPSJNMCAxMjhDMCA5Mi43IDI4LjcgNjQgNjQgNjRIMzIwYzM1LjMgMCA2NCAyOC43IDY0IDY0VjM4NGMwIDM1LjMtMjguNyA2NC02NCA2NEg2NGMtMzUuMyAwLTY0LTI4LjctNjQtNjRWMTI4eiIgLz4KPC9zdmc+Cg==" alt="stop" title="${t("Aborted")}">`)
|
||||||
|
|
@ -131,10 +140,14 @@ function componentUploadQueue(render, { tasks$ }) {
|
||||||
const updateDOMTaskProgress = ($task, text) => $task.firstElementChild.nextElementSibling.textContent = text;
|
const updateDOMTaskProgress = ($task, text) => $task.firstElementChild.nextElementSibling.textContent = text;
|
||||||
const updateDOMTaskSpeed = ($task, text) => $task.firstElementChild.firstElementChild.nextElementSibling.textContent = formatSpeed(text);
|
const updateDOMTaskSpeed = ($task, text) => $task.firstElementChild.firstElementChild.nextElementSibling.textContent = formatSpeed(text);
|
||||||
const updateDOMGlobalSpeed = function (workersSpeed) {
|
const updateDOMGlobalSpeed = function (workersSpeed) {
|
||||||
|
let last = 0;
|
||||||
return (nworker, currentWorkerSpeed) => {
|
return (nworker, currentWorkerSpeed) => {
|
||||||
workersSpeed[nworker] = currentWorkerSpeed;
|
workersSpeed[nworker] = currentWorkerSpeed;
|
||||||
|
if (new Date() - last <= 500) return;
|
||||||
|
last = new Date();
|
||||||
const speed = workersSpeed.reduce((acc, el) => acc + el, 0);
|
const speed = workersSpeed.reduce((acc, el) => acc + el, 0);
|
||||||
$page.firstElementChild.nextElementSibling.firstElementChild.textContent = formatSpeed(speed);
|
const $speed = $page.firstElementChild.nextElementSibling.firstElementChild;
|
||||||
|
$speed.textContent = formatSpeed(speed);
|
||||||
};
|
};
|
||||||
}(new Array(MAX_WORKERS).fill(0));
|
}(new Array(MAX_WORKERS).fill(0));
|
||||||
const updateDOMGlobalTitle = ($page, text) => $page.firstElementChild.nextElementSibling.childNodes[0].textContent = text;
|
const updateDOMGlobalTitle = ($page, text) => $page.firstElementChild.nextElementSibling.childNodes[0].textContent = text;
|
||||||
|
|
@ -202,7 +215,8 @@ function componentUploadQueue(render, { tasks$ }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const noFailureAllowed = (fn) => fn().catch(() => noFailureAllowed(fn));
|
const noFailureAllowed = (fn) => fn().catch(() => noFailureAllowed(fn));
|
||||||
effect(tasks$.pipe(rxjs.tap(async (newTasks) => {
|
workers$.subscribe(async ({ tasks: newTasks }) => {
|
||||||
|
console.log("TASKS PROCESS", newTasks);
|
||||||
tasks = tasks.concat(newTasks); // add new tasks to the pool
|
tasks = tasks.concat(newTasks); // add new tasks to the pool
|
||||||
while(true) {
|
while(true) {
|
||||||
const nworker = reservations.indexOf(false);
|
const nworker = reservations.indexOf(false);
|
||||||
|
|
@ -210,7 +224,7 @@ function componentUploadQueue(render, { tasks$ }) {
|
||||||
reservations[nworker] = true;
|
reservations[nworker] = true;
|
||||||
noFailureAllowed(processWorkerQueue.bind(this, nworker)).then(() => reservations[nworker] = false);
|
noFailureAllowed(processWorkerQueue.bind(this, nworker)).then(() => reservations[nworker] = false);
|
||||||
}
|
}
|
||||||
})));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class IExecutor {
|
class IExecutor {
|
||||||
|
|
@ -219,7 +233,6 @@ class IExecutor {
|
||||||
run() { throw new Error("NOT_IMPLEMENTED"); }
|
run() { throw new Error("NOT_IMPLEMENTED"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob(new Array(2 * 1024 * 1024).fill('a'), { type: "text/plain" });
|
|
||||||
function workerImplFile({ error, progress, speed }) {
|
function workerImplFile({ error, progress, speed }) {
|
||||||
return new class Worker extends IExecutor {
|
return new class Worker extends IExecutor {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -232,16 +245,12 @@ function workerImplFile({ error, progress, speed }) {
|
||||||
this.xhr.abort();
|
this.xhr.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
run({ stream }) {
|
async run({ entry, path, virtual }) {
|
||||||
return new Promise((done, err) => {
|
return new Promise((done, err) => {
|
||||||
console.log("EXECUTE", stream)
|
|
||||||
this.xhr = new XMLHttpRequest();
|
this.xhr = new XMLHttpRequest();
|
||||||
this.xhr.open("POST", "http://localhost:8334/api/files/cat?path=" + encodeURIComponent("/filestashtest/test/dummy.txt"));
|
this.xhr.open("POST", "api/files/cat?path=" + encodeURIComponent(path));
|
||||||
this.xhr.withCredentials = true;
|
this.xhr.withCredentials = true;
|
||||||
this.xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
|
this.xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
|
||||||
this.xhr.onerror = function(e) {
|
|
||||||
err(new AjaxError("failed", e, "FAILED"));
|
|
||||||
};
|
|
||||||
this.xhr.upload.onabort = () => {
|
this.xhr.upload.onabort = () => {
|
||||||
err(new AjaxError("aborted", null, "ABORTED"));
|
err(new AjaxError("aborted", null, "ABORTED"));
|
||||||
error(new AjaxError("aborted", null, "ABORTED"));
|
error(new AjaxError("aborted", null, "ABORTED"));
|
||||||
|
|
@ -271,17 +280,25 @@ function workerImplFile({ error, progress, speed }) {
|
||||||
this.prevProgress.shift();
|
this.prevProgress.shift();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// this.xhr.onreadystatechange = () => console.log(this.xhr.readyState);
|
|
||||||
this.xhr.onload = () => {
|
this.xhr.onload = () => {
|
||||||
progress(100);
|
progress(100);
|
||||||
|
virtual.afterSuccess();
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
this.xhr.send(stream);
|
this.xhr.onerror = function(e) {
|
||||||
|
err(new AjaxError("failed", e, "FAILED"));
|
||||||
|
vitual.afterError();
|
||||||
|
};
|
||||||
|
entry.file(
|
||||||
|
(file) => this.xhr.send(file),
|
||||||
|
(err) => this.xhr.onerror(err),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function workerImplDirectory({ error, progress }) {
|
function workerImplDirectory({ error, progress }) {
|
||||||
return new class Worker extends IExecutor {
|
return new class Worker extends IExecutor {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -293,10 +310,10 @@ function workerImplDirectory({ error, progress }) {
|
||||||
this.xhr.abort();
|
this.xhr.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
run() {
|
run({ virtual, path }) {
|
||||||
return new Promise((done, err) => {
|
return new Promise((done, err) => {
|
||||||
this.xhr = new XMLHttpRequest();
|
this.xhr = new XMLHttpRequest();
|
||||||
this.xhr.open("POST", "http://localhost:8334/api/files/mkdir?path=" + encodeURIComponent("/filestashtest/test/dummy.txt"));
|
this.xhr.open("POST", "api/files/mkdir?path=" + encodeURIComponent(path));
|
||||||
this.xhr.withCredentials = true;
|
this.xhr.withCredentials = true;
|
||||||
this.xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
|
this.xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
|
||||||
this.xhr.onerror = function(e) {
|
this.xhr.onerror = function(e) {
|
||||||
|
|
@ -316,13 +333,14 @@ function workerImplDirectory({ error, progress }) {
|
||||||
err(new AjaxError("aborted", null, "ABORTED"));
|
err(new AjaxError("aborted", null, "ABORTED"));
|
||||||
error(new AjaxError("aborted", null, "ABORTED"));
|
error(new AjaxError("aborted", null, "ABORTED"));
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
|
virtual.afterError();
|
||||||
};
|
};
|
||||||
this.xhr.onload = () => {
|
this.xhr.onload = () => {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
progress(100);
|
progress(100);
|
||||||
|
virtual.afterSuccess();
|
||||||
setTimeout(() => done(), 500);
|
setTimeout(() => done(), 500);
|
||||||
};
|
};
|
||||||
console.log(stream)
|
|
||||||
this.xhr.send(null);
|
this.xhr.send(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -330,7 +348,7 @@ function workerImplDirectory({ error, progress }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processFiles(filelist) {
|
async function processFiles(filelist) { // TODO
|
||||||
const files = [];
|
const files = [];
|
||||||
const detectFiletype = (file) => {
|
const detectFiletype = (file) => {
|
||||||
// the 4096 is an heuristic I've observed and taken from:
|
// the 4096 is an heuristic I've observed and taken from:
|
||||||
|
|
@ -354,10 +372,10 @@ async function processFiles(filelist) {
|
||||||
for (const currentFile of filelist) {
|
for (const currentFile of filelist) {
|
||||||
const type = await detectFiletype(currentFile);
|
const type = await detectFiletype(currentFile);
|
||||||
const file = { type, date: currentFile.lastModified, name: currentFile.name, path: currentFile.name };
|
const file = { type, date: currentFile.lastModified, name: currentFile.name, path: currentFile.name };
|
||||||
if (type === "file") file.size = currentFile.size;
|
if (type === "directory") file.path += "/";
|
||||||
else if (type === "directory") file.path += "/";
|
else if (type === "file") {
|
||||||
else assert.fail(`NOT_SUPPORTED type="${type}"`, type);
|
fs.push({ type: "file", path, exec: workerImplFile, entry: currentFile });
|
||||||
file.stream = currentFile // TODO: put a file object in there
|
} else assert.fail(`NOT_SUPPORTED type="${type}"`, type);
|
||||||
file.exec = workerImplFile.bind(file);
|
file.exec = workerImplFile.bind(file);
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
|
|
@ -366,26 +384,42 @@ async function processFiles(filelist) {
|
||||||
|
|
||||||
async function processItems(itemList) {
|
async function processItems(itemList) {
|
||||||
const bfs = async (queue) => {
|
const bfs = async (queue) => {
|
||||||
const fs = [];
|
const tasks = [];
|
||||||
let path = ""
|
let size = 0;
|
||||||
|
let path = "";
|
||||||
|
const basepath = currentPath();
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const entry = queue.shift();
|
const entry = queue.shift();
|
||||||
const path = entry.fullPath.substring(1);
|
const path = basepath + entry.fullPath.substring(1);
|
||||||
|
let task = null;
|
||||||
if (entry === null) continue;
|
if (entry === null) continue;
|
||||||
else if (entry.isFile) {
|
if (entry.isFile) {
|
||||||
const file = await new Promise((done) => entry.file((file) => done(file)));
|
const entrySize = await new Promise((done) => entry.getMetadata(({ size }) => done(size)));
|
||||||
fs.push({ type: "file", path, exec: workerImplFile, stream: file });
|
task = {
|
||||||
continue;
|
type: "file", entry,
|
||||||
|
path,
|
||||||
|
exec: workerImplFile,
|
||||||
|
virtual: save(path, entrySize),
|
||||||
|
};
|
||||||
|
size += entrySize;
|
||||||
} else if (entry.isDirectory) {
|
} else if (entry.isDirectory) {
|
||||||
fs.push({ type: "directory", path: path + "/", exec: workerImplDirectory });
|
task = {
|
||||||
|
type: "directory",
|
||||||
|
path: path + "/",
|
||||||
|
exec: workerImplDirectory,
|
||||||
|
virtual: mkdir(path),
|
||||||
|
};
|
||||||
|
size += 5000; // that's to calculate the remaining time for an upload, aka made up size is ok
|
||||||
queue = queue.concat(await new Promise((done) => {
|
queue = queue.concat(await new Promise((done) => {
|
||||||
entry.createReader().readEntries(done)
|
entry.createReader().readEntries(done);
|
||||||
}));
|
}));
|
||||||
continue;
|
} else {
|
||||||
|
assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry);
|
||||||
}
|
}
|
||||||
assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry);
|
task.virtual.before();
|
||||||
|
tasks.push(task);
|
||||||
}
|
}
|
||||||
return fs;
|
return { tasks, size: 1000 };
|
||||||
}
|
}
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const item of itemList) entries.push(item.webkitGetAsEntry());
|
for (const item of itemList) entries.push(item.webkitGetAsEntry());
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@ import notification from "../../components/notification.js";
|
||||||
|
|
||||||
import { setPermissions } from "./model_acl.js";
|
import { setPermissions } from "./model_acl.js";
|
||||||
import fscache from "./cache.js";
|
import fscache from "./cache.js";
|
||||||
import {
|
import { ls as middlewareLs } from "./model_virtual_layer.js";
|
||||||
rm as cacheRm, mv as cacheMv, save as cacheSave,
|
|
||||||
touch as cacheTouch, mkdir as cacheMkdir, ls as middlewareLs,
|
|
||||||
} from "./cache_transcient.js";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The naive approach would be to make an API call and refresh the screen after an action
|
* The naive approach would be to make an API call and refresh the screen after an action
|
||||||
|
|
@ -22,55 +19,41 @@ import {
|
||||||
* 3. the new file is being persisted in the screen if the API call is a success
|
* 3. the new file is being persisted in the screen if the API call is a success
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const errorNotification = rxjs.catchError((err) => {
|
const withNotification = rxjs.catchError((err) => {
|
||||||
notification.error(err);
|
notification.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
export function touch(path) {
|
export const touch = (path) => ajax({
|
||||||
const ajax$ = ajax({
|
url: `api/files/touch?path=${encodeURIComponent(path)}`,
|
||||||
url: `/api/files/touch?path=${encodeURIComponent(path)}`,
|
method: "POST",
|
||||||
method: "POST",
|
responseType: "json",
|
||||||
responseType: "json",
|
}).pipe(withNotification);
|
||||||
}).pipe(errorNotification);
|
|
||||||
return cacheTouch(ajax$, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mkdir(path) {
|
export const mkdir = (path) => ajax({
|
||||||
const ajax$ = ajax({
|
url: `api/files/mkdir?path=${encodeURIComponent(path)}`,
|
||||||
url: `/api/files/mkdir?path=${encodeURIComponent(path)}`,
|
method: "POST",
|
||||||
method: "POST",
|
responseType: "json",
|
||||||
responseType: "json",
|
}).pipe(withNotification);
|
||||||
}).pipe(errorNotification);
|
|
||||||
return cacheMkdir(ajax$, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rm(...paths) {
|
export const rm = (...paths) => rxjs.forkJoin(paths.map((path) => ajax({
|
||||||
const ajax$ = rxjs.forkJoin(paths.map((path) => ajax({
|
url: `api/files/rm?path=${encodeURIComponent(path)}`,
|
||||||
url: `/api/files/rm?path=${encodeURIComponent(path)}`,
|
method: "POST",
|
||||||
method: "POST",
|
responseType: "json",
|
||||||
responseType: "json",
|
}).pipe(withNotification)));
|
||||||
}).pipe(errorNotification)));
|
|
||||||
return cacheRm(ajax$, ...paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mv(from, to) {
|
export const mv = (from, to) => ajax({
|
||||||
const ajax$ = ajax({
|
url: `api/files/mv?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
||||||
url: `/api/files/mv?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
method: "POST",
|
||||||
method: "POST",
|
responseType: "json",
|
||||||
responseType: "json",
|
}).pipe(withNotification);
|
||||||
}).pipe(errorNotification);
|
|
||||||
return cacheMv(ajax$, from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function save(path) { // TODO
|
export const save = (path) => rxjs.of(null).pipe(rxjs.delay(1000));
|
||||||
return rxjs.of(null).pipe(rxjs.delay(1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ls(path) {
|
export const ls = (path) => {
|
||||||
const lsFromCache = (path) => rxjs.from(fscache().get(path));
|
const lsFromCache = (path) => rxjs.from(fscache().get(path));
|
||||||
const lsFromHttp = (path) => ajax({
|
const lsFromHttp = (path) => ajax({
|
||||||
url: `/api/files/ls?path=${encodeURIComponent(path)}`,
|
url: `api/files/ls?path=${encodeURIComponent(path)}`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|
@ -114,14 +97,14 @@ export function ls(path) {
|
||||||
rxjs.tap(({ permissions }) => setPermissions(path, permissions)),
|
rxjs.tap(({ permissions }) => setPermissions(path, permissions)),
|
||||||
middlewareLs(path),
|
middlewareLs(path),
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function search(term) {
|
export const search = (term) => {
|
||||||
const path = location.pathname.replace(new RegExp("^/files/"), "/");
|
const path = location.pathname.replace(new RegExp("^/files/"), "/");
|
||||||
return ajax({
|
return ajax({
|
||||||
url: `/api/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(term)}`,
|
url: `api/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(term)}`,
|
||||||
responseType: "json"
|
responseType: "json"
|
||||||
}).pipe(rxjs.map(({ responseJSON }) => ({
|
}).pipe(rxjs.map(({ responseJSON }) => ({
|
||||||
files: responseJSON.results,
|
files: responseJSON.results,
|
||||||
})));
|
})));
|
||||||
}
|
};
|
||||||
|
|
|
||||||
402
public/assets/pages/filespage/model_virtual_layer.js
Normal file
402
public/assets/pages/filespage/model_virtual_layer.js
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
import { onDestroy } from "../../lib/skeleton/index.js";
|
||||||
|
import rxjs from "../../lib/rx.js";
|
||||||
|
import fscache from "./cache.js";
|
||||||
|
import { extractPath, isDir } from "./helper.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The virtual files is used to rerender the list of files in a particular location. That's used
|
||||||
|
* when we want to update the dom when doing either of a touch, mkdir, rm, mv, ...
|
||||||
|
*
|
||||||
|
* |---------------| |---------------|
|
||||||
|
* | LS | ---> | Virtual Layer | ---> Observable
|
||||||
|
* |---------------| |---------------|
|
||||||
|
*
|
||||||
|
* It is split onto 2 parts:
|
||||||
|
* - the virtualFiles$: which are things we want to display in addition to what is currently
|
||||||
|
* visible on the screen
|
||||||
|
* - the mutationFiles$: which are things already on the screen which we need to mutate. For
|
||||||
|
* example when we want a particular file to show a loading spinner, ...
|
||||||
|
*/
|
||||||
|
|
||||||
|
const virtualFiles$ = new rxjs.BehaviorSubject({
|
||||||
|
// "/tmp/": [],
|
||||||
|
// "/home/": [{ name: "test", type: "directory" }]
|
||||||
|
});
|
||||||
|
const mutationFiles$ = new rxjs.BehaviorSubject({
|
||||||
|
// "/home/": [{ name: "test", fn: (file) => file, ...]
|
||||||
|
});
|
||||||
|
|
||||||
|
class IVirtualLayer {
|
||||||
|
constructor() {}
|
||||||
|
before() { throw new Error("NOT_IMPLEMENTED"); }
|
||||||
|
async afterSuccess() { throw new Error("NOT_IMPLEMENTED"); }
|
||||||
|
async afterError() { throw new Error("NOT_IMPLEMENTED"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withVirtualLayer(ajax$, mutate) {
|
||||||
|
mutate.before();
|
||||||
|
return ajax$.pipe(
|
||||||
|
rxjs.tap((resp) => mutate.afterSuccess(resp)),
|
||||||
|
rxjs.catchError(mutate.afterError),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function touch(path) {
|
||||||
|
const [basepath, filename] = extractPath(path);
|
||||||
|
const file = {
|
||||||
|
name: filename,
|
||||||
|
type: "file",
|
||||||
|
size: 0,
|
||||||
|
time: new Date().getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new class TouchVL extends IVirtualLayer {
|
||||||
|
constructor() { super(); }
|
||||||
|
|
||||||
|
before() {
|
||||||
|
stateAdd(virtualFiles$, basepath, {
|
||||||
|
...file,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterSuccess() {
|
||||||
|
removeLoading(virtualFiles$, basepath, filename);
|
||||||
|
onDestroy(() => statePop(virtualFiles$, basepath, filename));
|
||||||
|
await fscache().update(basepath, ({ files, ...rest }) => ({
|
||||||
|
files: files.concat([file]),
|
||||||
|
...rest,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterError(err, caught) {
|
||||||
|
statePop(virtualFiles$, basepath, filename);
|
||||||
|
return rxjs.of(fscache().remove(basepath)).pipe(
|
||||||
|
rxjs.mergeMap(() => rxjs.EMPTY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mkdir(path) {
|
||||||
|
const [basepath, dirname] = extractPath(path);
|
||||||
|
const file = {
|
||||||
|
name: dirname,
|
||||||
|
type: "directory",
|
||||||
|
size: 0,
|
||||||
|
time: new Date().getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new class MkdirVL extends IVirtualLayer {
|
||||||
|
constructor() { super(); }
|
||||||
|
|
||||||
|
before() {
|
||||||
|
stateAdd(virtualFiles$, basepath, {
|
||||||
|
...file,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterSuccess() {
|
||||||
|
removeLoading(virtualFiles$, basepath, dirname);
|
||||||
|
onDestroy(() => statePop(virtualFiles$, basepath, dirname));
|
||||||
|
await fscache().update(basepath, ({ files, ...rest }) => ({
|
||||||
|
files: files.concat([file]),
|
||||||
|
...rest,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterError(err, caught) {
|
||||||
|
statePop(virtualFiles$, basepath, dirname);
|
||||||
|
return rxjs.of(fscache().remove(basepath)).pipe(
|
||||||
|
rxjs.mergeMap(() => rxjs.EMPTY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save(path, size) {
|
||||||
|
const [basepath, filename] = extractPath(path);
|
||||||
|
const file = {
|
||||||
|
name: filename,
|
||||||
|
type: "file",
|
||||||
|
size,
|
||||||
|
time: new Date().getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new class SaveVL extends IVirtualLayer {
|
||||||
|
constructor() { super(); }
|
||||||
|
|
||||||
|
before() {
|
||||||
|
stateAdd(virtualFiles$, basepath, {
|
||||||
|
...file,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterSuccess() {
|
||||||
|
removeLoading(virtualFiles$, basepath, filename);
|
||||||
|
onDestroy(() => statePop(virtualFiles$, basepath, filename));
|
||||||
|
await fscache().update(basepath, ({ files, ...rest }) => ({
|
||||||
|
files: files.concat([file]),
|
||||||
|
...rest,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterError() {
|
||||||
|
statePop(virtualFiles$, basepath, dirname);
|
||||||
|
return rxjs.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rm(...paths) {
|
||||||
|
if (paths.length === 0) return rxjs.of(null);
|
||||||
|
const arr = new Array(paths.length * 2);
|
||||||
|
let basepath = null;
|
||||||
|
for (let i=0;i<paths.length;i++) {
|
||||||
|
[arr[2*i], arr[2*i+1]] = extractPath(paths[i]);
|
||||||
|
if (i === 0) basepath = arr[2*i];
|
||||||
|
else if (basepath !== arr[2*i]) throw new Error("NOT_IMPLEMENTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new class RmVL extends IVirtualLayer {
|
||||||
|
constructor() { super(); }
|
||||||
|
|
||||||
|
before() {
|
||||||
|
stateAdd(mutationFiles$, basepath, {
|
||||||
|
name: basepath,
|
||||||
|
fn: (file) => {
|
||||||
|
for (let i=0;i<arr.length;i+=2) {
|
||||||
|
if (file.name === arr[i+1]) {
|
||||||
|
file.loading = true;
|
||||||
|
file.last = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterSuccess() {
|
||||||
|
stateAdd(mutationFiles$, basepath, {
|
||||||
|
name: basepath,
|
||||||
|
fn: (file) => {
|
||||||
|
for (let i=0;i<arr.length;i+=2) {
|
||||||
|
if (file.name === arr[i+1]) return null;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onDestroy(() => statePop(mutationFiles$, basepath, basepath));
|
||||||
|
await Promise.all(paths.map((path) => fscache().remove(path, false)));
|
||||||
|
await fscache().update(basepath, ({ files, ...rest }) => ({
|
||||||
|
files: files.filter(({ name }) => {
|
||||||
|
for (let i=0;i<arr.length;i+=2) {
|
||||||
|
if (name === arr[i+1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
...rest,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterError() {
|
||||||
|
stateAdd(mutationFiles$, basepath, {
|
||||||
|
name: basepath,
|
||||||
|
fn: (file) => {
|
||||||
|
for (let i=0;i<arr.length;i+=2) {
|
||||||
|
if (file.name === arr[i+1]) {
|
||||||
|
delete file.loading;
|
||||||
|
delete file.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return rxjs.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mv(fromPath, toPath) {
|
||||||
|
const [fromBasepath, fromName] = extractPath(fromPath);
|
||||||
|
const [toBasepath, toName] = extractPath(toPath);
|
||||||
|
let type = null;
|
||||||
|
|
||||||
|
return new class MvVL extends IVirtualLayer {
|
||||||
|
constructor(){ super(); }
|
||||||
|
|
||||||
|
before() {
|
||||||
|
if (fromBasepath === toBasepath) this._beforeSamePath();
|
||||||
|
else this._beforeSamePath();
|
||||||
|
}
|
||||||
|
_beforeSamePath() {
|
||||||
|
stateAdd(mutationFiles$, fromBasepath, {
|
||||||
|
name: fromName,
|
||||||
|
fn: (file) => {
|
||||||
|
if (file.name === fromName) {
|
||||||
|
file.loading = true;
|
||||||
|
file.name = toName;
|
||||||
|
type = file.type;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_beforeDifferentPath() {
|
||||||
|
stateAdd(mutationFiles$, fromBasepath, {
|
||||||
|
name: fromName,
|
||||||
|
fn: (file) => {
|
||||||
|
if (file.name === fromName) {
|
||||||
|
file.loading = true;
|
||||||
|
file.last = true;
|
||||||
|
type = file.type;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
stateAdd(virtualFiles$, toBasepath, {
|
||||||
|
name: toName,
|
||||||
|
loading: true,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterSuccess() {
|
||||||
|
fscache().remove(fromPath, false);
|
||||||
|
if (fromBasepath === toBasepath) await this._afterSuccessSamePath();
|
||||||
|
else await this._afterSuccessDifferentPath();
|
||||||
|
}
|
||||||
|
async _afterSuccessSamePath() {
|
||||||
|
stateAdd(mutationFiles$, fromBasepath, {
|
||||||
|
name: fromName,
|
||||||
|
fn: (file) => {
|
||||||
|
if (file.name === toName) delete file.loading;
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fscache().update(fromBasepath, ({ files, ...rest }) => {
|
||||||
|
return {
|
||||||
|
files: files.map((file) => {
|
||||||
|
if (file.name === fromName) {
|
||||||
|
file.name = toName;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}),
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _afterSuccessDifferentPath() {
|
||||||
|
stateAdd(mutationFiles$, fromBasepath, {
|
||||||
|
name: fromName,
|
||||||
|
fn: (file) => {
|
||||||
|
if (file.name === fromName) return null;
|
||||||
|
return file;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onDestroy(() => statePop(mutationFiles$, fromBasepath, fromName));
|
||||||
|
statePop(virtualFiles$, toBasepath, toName);
|
||||||
|
await fscache().update(fromBasepath, ({ files, ...rest }) => ({
|
||||||
|
files: files.filter((file) => file.name === fromName ? false : true),
|
||||||
|
...rest,
|
||||||
|
}))
|
||||||
|
await fscache().update(toBasepath, ({ files, ...rest }) => ({
|
||||||
|
files: files.concat([{
|
||||||
|
name: fromName,
|
||||||
|
time: new Date().getTime(),
|
||||||
|
type,
|
||||||
|
}]),
|
||||||
|
...rest,
|
||||||
|
}));
|
||||||
|
if (isDir(fromPath)) await fscache.remove(fromPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterError() {
|
||||||
|
statePop(mutationFiles$, fromBasepath, fromName);
|
||||||
|
if (fromBasepath !== toBasepath) {
|
||||||
|
statePop(virtualFiles$, toBasepath, toName);
|
||||||
|
}
|
||||||
|
return rxjs.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ls(path) {
|
||||||
|
return rxjs.pipe(
|
||||||
|
// case1: virtual files = additional files we want to see displayed in the UI
|
||||||
|
rxjs.switchMap(({ files, ...res }) => virtualFiles$.pipe(rxjs.mergeMap((virtualFiles) => {
|
||||||
|
const shouldContinue = !!(virtualFiles[path] && virtualFiles[path].length > 0);
|
||||||
|
if (!shouldContinue) return rxjs.of({ ...res, files });
|
||||||
|
return rxjs.of({
|
||||||
|
...res,
|
||||||
|
files: files.concat(virtualFiles[path]),
|
||||||
|
});
|
||||||
|
}))),
|
||||||
|
// case2: file mutation = update a file state, typically to add a loading state to an
|
||||||
|
// file or remove it entirely
|
||||||
|
rxjs.switchMap(({ files, ...res }) => mutationFiles$.pipe(rxjs.mergeMap((fns) => {
|
||||||
|
const shouldContinue = !!(fns[path] && fns[path].length > 0);
|
||||||
|
if (!shouldContinue) return rxjs.of({ ...res, files });
|
||||||
|
for (let i=files.length-1; i>=0; i--) {
|
||||||
|
for (let j=0; j<fns[path].length; j++) {
|
||||||
|
files[i] = fns[path][j].fn(files[i]);
|
||||||
|
if (!files[i]) {
|
||||||
|
files.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rxjs.of({ ...res, files });
|
||||||
|
}))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateAdd(behavior, path, obj) {
|
||||||
|
let arr = behavior.value[path];
|
||||||
|
if (!arr) arr = [];
|
||||||
|
let alreadyKnown = false;
|
||||||
|
for (let i=0; i<arr.length; i++) {
|
||||||
|
if (arr[i].name === obj.name) {
|
||||||
|
alreadyKnown = true;
|
||||||
|
arr[i] = obj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!alreadyKnown) arr.push(obj);
|
||||||
|
behavior.next({
|
||||||
|
...behavior.value,
|
||||||
|
[path]: arr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statePop(behavior, path, filename) {
|
||||||
|
const arr = behavior.value[path];
|
||||||
|
if (!arr) return;
|
||||||
|
const newArr = arr.filter(({ name }) => name !== filename)
|
||||||
|
if (newArr.length === 0) {
|
||||||
|
const newState = { ...behavior.value };
|
||||||
|
delete newState[path];
|
||||||
|
behavior.next(newState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
behavior.next({
|
||||||
|
...behavior.value,
|
||||||
|
[path]: newArr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoading(behavior, path, filename) {
|
||||||
|
const arr = behavior.value[path];
|
||||||
|
if (!arr) return;
|
||||||
|
virtualFiles$.next({
|
||||||
|
...virtualFiles$.value,
|
||||||
|
[path]: arr.map((file) => {
|
||||||
|
if (file.name === filename) delete file.loading;
|
||||||
|
return file;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue