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() {
|
||||
try {
|
||||
let config = {};
|
||||
// await Config.refresh()
|
||||
|
||||
await Promise.all([ // procedure with no outside dependencies
|
||||
setup_translation(),
|
||||
setup_xdg_open(),
|
||||
|
|
@ -16,10 +19,10 @@ export default async function main() {
|
|||
setup_loader(),
|
||||
setup_history(),
|
||||
]);
|
||||
// await Config.refresh()
|
||||
|
||||
await Promise.all([ // procedure with dependency on config
|
||||
// setup_chromecast() // TODO
|
||||
setup_base(config),
|
||||
]);
|
||||
|
||||
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() {
|
||||
// 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;
|
||||
|
||||
export function currentRoute(r, notFoundRoute) {
|
||||
const currentRoute = "/" + trimPrefix(
|
||||
const currentRoute = trimPrefix(
|
||||
window.location.pathname,
|
||||
window.document.head.querySelector("base")?.getAttribute("href") || "/"
|
||||
window.document.head.querySelector("base")?.getAttribute("href") || ""
|
||||
);
|
||||
for (const routeKey in r) {
|
||||
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 { 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) {
|
||||
const $node = createElement(`
|
||||
|
|
|
|||
|
|
@ -21,14 +21,26 @@ import { getAction$, setAction } from "./state_newthing.js";
|
|||
import { setState, getState$ } from "./state_config.js";
|
||||
import { clearCache } from "./cache.js";
|
||||
import { getPermission, calculatePermission } from "./model_acl.js";
|
||||
import { rm, mv } from "./model_files.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 = {
|
||||
withButtonsRight: t("OK"),
|
||||
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) {
|
||||
const $page = createElement(`
|
||||
<div class="component_submenu container">
|
||||
|
|
@ -127,7 +139,7 @@ function componentLeft(render, { $scroll }) {
|
|||
createModal(modalOpt),
|
||||
basename(expandSelection()[0].path.replace(new RegExp("/$"), "")),
|
||||
)),
|
||||
rxjs.mergeMap((val) => {
|
||||
rxjs.mergeMap((val) => { // TODO: migrate to transcient impl
|
||||
const path = expandSelection()[0].path;
|
||||
const [basepath, filename] = extractPath(path);
|
||||
clearSelection();
|
||||
|
|
@ -141,7 +153,7 @@ function componentLeft(render, { $scroll }) {
|
|||
createModal(modalOpt),
|
||||
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;
|
||||
clearSelection();
|
||||
clearCache(selection);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
padding: 20px 20px 5px 20px;
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 100;
|
||||
font-weight: normal;
|
||||
}
|
||||
.component_upload h2 .percent {
|
||||
color: var(--emphasis-primary);
|
||||
|
|
|
|||
|
|
@ -5,23 +5,26 @@ import { loadCSS } from "../../helpers/loader.js";
|
|||
import { qs } from "../../lib/dom.js";
|
||||
import { AjaxError } from "../../lib/error.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";
|
||||
|
||||
const workers$ = new rxjs.BehaviorSubject({ tasks: [], size: null });
|
||||
|
||||
export default function(render) {
|
||||
const $page = createFragment(`
|
||||
<div is="component_filezone"></div>
|
||||
<div is="component_upload_fab"></div>
|
||||
`);
|
||||
const tasks$ = new rxjs.BehaviorSubject([]);
|
||||
|
||||
if (!document.querySelector(`[is="component_upload_queue"]`)) {
|
||||
const $queue = createElement(`<div is="component_upload_queue"></div>`);
|
||||
document.body.appendChild($queue);
|
||||
componentUploadQueue(createRender($queue), { tasks$ });
|
||||
componentUploadQueue(createRender($queue), { workers$ });
|
||||
}
|
||||
|
||||
componentFilezone(createRender($page.children[0]), { tasks$ });
|
||||
componentUploadFAB(createRender($page.children[1]), { tasks$ });
|
||||
componentFilezone(createRender($page.children[0]), { workers$ });
|
||||
componentUploadFAB(createRender($page.children[1]), { workers$ });
|
||||
render($page);
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +32,7 @@ export function init() {
|
|||
return loadCSS(import.meta.url, "./ctrl_upload.css");
|
||||
}
|
||||
|
||||
function componentUploadFAB(render, { tasks$ }) {
|
||||
function componentUploadFAB(render, { workers$ }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_mobilefileupload no-select">
|
||||
<form>
|
||||
|
|
@ -46,17 +49,18 @@ function componentUploadFAB(render, { tasks$ }) {
|
|||
</div>
|
||||
`);
|
||||
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);
|
||||
}
|
||||
|
||||
function componentFilezone(render, { tasks$ }) {
|
||||
function componentFilezone(render, { workers$ }) {
|
||||
const $target = document.body.querySelector(`[data-bind="filemanager-children"]`);
|
||||
$target.ondragenter = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$target.classList.add("dropzone");
|
||||
e.dataTransfer.setData("type", "fileupload");
|
||||
};
|
||||
$target.ondragover = (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -68,9 +72,13 @@ function componentFilezone(render, { tasks$ }) {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const loadID = setTimeout(() => render(createElement("<div>LOADING</div>")), 2000);
|
||||
if (e.dataTransfer.items instanceof window.DataTransferItemList) tasks$.next(await processItems(e.dataTransfer.items));
|
||||
else if (e.dataTransfer.files instanceof window.FileList) tasks$.next(await processFiles(e.dataTransfer.files));
|
||||
else assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry);
|
||||
if (e.dataTransfer.items instanceof window.DataTransferItemList) {
|
||||
workers$.next(await processItems(e.dataTransfer.items));
|
||||
} 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");
|
||||
clearTimeout(loadID);
|
||||
render(createFragment(""));
|
||||
|
|
@ -79,7 +87,7 @@ function componentFilezone(render, { tasks$ }) {
|
|||
|
||||
const MAX_WORKERS = 4;
|
||||
|
||||
function componentUploadQueue(render, { tasks$ }) {
|
||||
function componentUploadQueue(render, { workers$ }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_upload hidden">
|
||||
<h2 class="no-select">${t("Current Upload")}
|
||||
|
|
@ -103,14 +111,15 @@ function componentUploadQueue(render, { tasks$ }) {
|
|||
</div>
|
||||
`);
|
||||
|
||||
// feature1: close the queue and stop the upload
|
||||
effect(onClick(qs($page, `img[alt="close"]`)).pipe(
|
||||
// feature1: close the queue
|
||||
onClick(qs($page, `img[alt="close"]`)).pipe(
|
||||
rxjs.mergeMap(() => animate($page, { time: 200, keyframes: slideYOut(50) })),
|
||||
rxjs.tap(() => $page.classList.add("hidden")),
|
||||
));
|
||||
).subscribe();
|
||||
|
||||
// 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;
|
||||
$page.classList.remove("hidden");
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
|
@ -123,7 +132,7 @@ function componentUploadQueue(render, { tasks$ }) {
|
|||
$task.firstElementChild.nextElementSibling.textContent = t("Waiting");
|
||||
}
|
||||
$content.appendChild($fragment);
|
||||
})));
|
||||
});
|
||||
|
||||
// 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")}">`)
|
||||
|
|
@ -131,10 +140,14 @@ function componentUploadQueue(render, { tasks$ }) {
|
|||
const updateDOMTaskProgress = ($task, text) => $task.firstElementChild.nextElementSibling.textContent = text;
|
||||
const updateDOMTaskSpeed = ($task, text) => $task.firstElementChild.firstElementChild.nextElementSibling.textContent = formatSpeed(text);
|
||||
const updateDOMGlobalSpeed = function (workersSpeed) {
|
||||
let last = 0;
|
||||
return (nworker, currentWorkerSpeed) => {
|
||||
workersSpeed[nworker] = currentWorkerSpeed;
|
||||
if (new Date() - last <= 500) return;
|
||||
last = new Date();
|
||||
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));
|
||||
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));
|
||||
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
|
||||
while(true) {
|
||||
const nworker = reservations.indexOf(false);
|
||||
|
|
@ -210,7 +224,7 @@ function componentUploadQueue(render, { tasks$ }) {
|
|||
reservations[nworker] = true;
|
||||
noFailureAllowed(processWorkerQueue.bind(this, nworker)).then(() => reservations[nworker] = false);
|
||||
}
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
class IExecutor {
|
||||
|
|
@ -219,7 +233,6 @@ class IExecutor {
|
|||
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 }) {
|
||||
return new class Worker extends IExecutor {
|
||||
constructor() {
|
||||
|
|
@ -232,16 +245,12 @@ function workerImplFile({ error, progress, speed }) {
|
|||
this.xhr.abort();
|
||||
}
|
||||
|
||||
run({ stream }) {
|
||||
async run({ entry, path, virtual }) {
|
||||
return new Promise((done, err) => {
|
||||
console.log("EXECUTE", stream)
|
||||
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.setRequestHeader("X-Requested-With", "XmlHttpRequest");
|
||||
this.xhr.onerror = function(e) {
|
||||
err(new AjaxError("failed", e, "FAILED"));
|
||||
};
|
||||
this.xhr.upload.onabort = () => {
|
||||
err(new AjaxError("aborted", null, "ABORTED"));
|
||||
error(new AjaxError("aborted", null, "ABORTED"));
|
||||
|
|
@ -271,17 +280,25 @@ function workerImplFile({ error, progress, speed }) {
|
|||
this.prevProgress.shift();
|
||||
}
|
||||
};
|
||||
// this.xhr.onreadystatechange = () => console.log(this.xhr.readyState);
|
||||
this.xhr.onload = () => {
|
||||
progress(100);
|
||||
virtual.afterSuccess();
|
||||
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 }) {
|
||||
return new class Worker extends IExecutor {
|
||||
constructor() {
|
||||
|
|
@ -293,10 +310,10 @@ function workerImplDirectory({ error, progress }) {
|
|||
this.xhr.abort();
|
||||
}
|
||||
|
||||
run() {
|
||||
run({ virtual, path }) {
|
||||
return new Promise((done, err) => {
|
||||
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.setRequestHeader("X-Requested-With", "XmlHttpRequest");
|
||||
this.xhr.onerror = function(e) {
|
||||
|
|
@ -316,13 +333,14 @@ function workerImplDirectory({ error, progress }) {
|
|||
err(new AjaxError("aborted", null, "ABORTED"));
|
||||
error(new AjaxError("aborted", null, "ABORTED"));
|
||||
clearInterval(id);
|
||||
virtual.afterError();
|
||||
};
|
||||
this.xhr.onload = () => {
|
||||
clearInterval(id);
|
||||
progress(100);
|
||||
virtual.afterSuccess();
|
||||
setTimeout(() => done(), 500);
|
||||
};
|
||||
console.log(stream)
|
||||
this.xhr.send(null);
|
||||
});
|
||||
|
||||
|
|
@ -330,7 +348,7 @@ function workerImplDirectory({ error, progress }) {
|
|||
}
|
||||
}
|
||||
|
||||
async function processFiles(filelist) {
|
||||
async function processFiles(filelist) { // TODO
|
||||
const files = [];
|
||||
const detectFiletype = (file) => {
|
||||
// the 4096 is an heuristic I've observed and taken from:
|
||||
|
|
@ -354,10 +372,10 @@ async function processFiles(filelist) {
|
|||
for (const currentFile of filelist) {
|
||||
const type = await detectFiletype(currentFile);
|
||||
const file = { type, date: currentFile.lastModified, name: currentFile.name, path: currentFile.name };
|
||||
if (type === "file") file.size = currentFile.size;
|
||||
else if (type === "directory") file.path += "/";
|
||||
else assert.fail(`NOT_SUPPORTED type="${type}"`, type);
|
||||
file.stream = currentFile // TODO: put a file object in there
|
||||
if (type === "directory") file.path += "/";
|
||||
else if (type === "file") {
|
||||
fs.push({ type: "file", path, exec: workerImplFile, entry: currentFile });
|
||||
} else assert.fail(`NOT_SUPPORTED type="${type}"`, type);
|
||||
file.exec = workerImplFile.bind(file);
|
||||
files.push(file);
|
||||
}
|
||||
|
|
@ -366,26 +384,42 @@ async function processFiles(filelist) {
|
|||
|
||||
async function processItems(itemList) {
|
||||
const bfs = async (queue) => {
|
||||
const fs = [];
|
||||
let path = ""
|
||||
const tasks = [];
|
||||
let size = 0;
|
||||
let path = "";
|
||||
const basepath = currentPath();
|
||||
while (queue.length > 0) {
|
||||
const entry = queue.shift();
|
||||
const path = entry.fullPath.substring(1);
|
||||
const path = basepath + entry.fullPath.substring(1);
|
||||
let task = null;
|
||||
if (entry === null) continue;
|
||||
else if (entry.isFile) {
|
||||
const file = await new Promise((done) => entry.file((file) => done(file)));
|
||||
fs.push({ type: "file", path, exec: workerImplFile, stream: file });
|
||||
continue;
|
||||
if (entry.isFile) {
|
||||
const entrySize = await new Promise((done) => entry.getMetadata(({ size }) => done(size)));
|
||||
task = {
|
||||
type: "file", entry,
|
||||
path,
|
||||
exec: workerImplFile,
|
||||
virtual: save(path, entrySize),
|
||||
};
|
||||
size += entrySize;
|
||||
} 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) => {
|
||||
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 = [];
|
||||
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 fscache from "./cache.js";
|
||||
import {
|
||||
rm as cacheRm, mv as cacheMv, save as cacheSave,
|
||||
touch as cacheTouch, mkdir as cacheMkdir, ls as middlewareLs,
|
||||
} from "./cache_transcient.js";
|
||||
import { ls as middlewareLs } from "./model_virtual_layer.js";
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
const errorNotification = rxjs.catchError((err) => {
|
||||
const withNotification = rxjs.catchError((err) => {
|
||||
notification.error(err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
export function touch(path) {
|
||||
const ajax$ = ajax({
|
||||
url: `/api/files/touch?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(errorNotification);
|
||||
return cacheTouch(ajax$, path);
|
||||
}
|
||||
export const touch = (path) => ajax({
|
||||
url: `api/files/touch?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(withNotification);
|
||||
|
||||
export function mkdir(path) {
|
||||
const ajax$ = ajax({
|
||||
url: `/api/files/mkdir?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(errorNotification);
|
||||
return cacheMkdir(ajax$, path);
|
||||
}
|
||||
export const mkdir = (path) => ajax({
|
||||
url: `api/files/mkdir?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(withNotification);
|
||||
|
||||
export function rm(...paths) {
|
||||
const ajax$ = rxjs.forkJoin(paths.map((path) => ajax({
|
||||
url: `/api/files/rm?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(errorNotification)));
|
||||
return cacheRm(ajax$, ...paths);
|
||||
}
|
||||
export const rm = (...paths) => rxjs.forkJoin(paths.map((path) => ajax({
|
||||
url: `api/files/rm?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(withNotification)));
|
||||
|
||||
export function mv(from, to) {
|
||||
const ajax$ = ajax({
|
||||
url: `/api/files/mv?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(errorNotification);
|
||||
return cacheMv(ajax$, from, to);
|
||||
}
|
||||
export const mv = (from, to) => ajax({
|
||||
url: `api/files/mv?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
}).pipe(withNotification);
|
||||
|
||||
export function save(path) { // TODO
|
||||
return rxjs.of(null).pipe(rxjs.delay(1000));
|
||||
}
|
||||
export const save = (path) => rxjs.of(null).pipe(rxjs.delay(1000));
|
||||
|
||||
export function ls(path) {
|
||||
export const ls = (path) => {
|
||||
const lsFromCache = (path) => rxjs.from(fscache().get(path));
|
||||
const lsFromHttp = (path) => ajax({
|
||||
url: `/api/files/ls?path=${encodeURIComponent(path)}`,
|
||||
url: `api/files/ls?path=${encodeURIComponent(path)}`,
|
||||
method: "GET",
|
||||
responseType: "json",
|
||||
}).pipe(
|
||||
|
|
@ -114,14 +97,14 @@ export function ls(path) {
|
|||
rxjs.tap(({ permissions }) => setPermissions(path, permissions)),
|
||||
middlewareLs(path),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function search(term) {
|
||||
export const search = (term) => {
|
||||
const path = location.pathname.replace(new RegExp("^/files/"), "/");
|
||||
return ajax({
|
||||
url: `/api/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(term)}`,
|
||||
url: `api/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(term)}`,
|
||||
responseType: "json"
|
||||
}).pipe(rxjs.map(({ responseJSON }) => ({
|
||||
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