mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-28 12:16:48 +08:00
510 lines
16 KiB
JavaScript
510 lines
16 KiB
JavaScript
import { onDestroy } from "../../lib/skeleton/index.js";
|
|
import rxjs from "../../lib/rx.js";
|
|
import fscache from "./cache.js";
|
|
import { hooks } from "./model_files.js";
|
|
import { extractPath, isDir, currentPath } 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 {
|
|
before() { throw new Error("NOT_IMPLEMENTED"); }
|
|
async afterSuccess() { throw new Error("NOT_IMPLEMENTED"); }
|
|
async afterError() { return rxjs.EMPTY; }
|
|
}
|
|
|
|
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 {
|
|
/**
|
|
* @override
|
|
*/
|
|
before() {
|
|
stateAdd(virtualFiles$, basepath, {
|
|
...file,
|
|
loading: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterSuccess() {
|
|
removeLoading(virtualFiles$, basepath, filename);
|
|
onDestroy(() => statePop(virtualFiles$, basepath, filename));
|
|
await fscache().update(basepath, ({ files = [], ...rest }) => ({
|
|
files: files.concat([file]),
|
|
...rest,
|
|
}));
|
|
hooks.mutation.emit({ op: "touch", path: basepath });
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterError() {
|
|
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 {
|
|
/**
|
|
* @override
|
|
*/
|
|
before() {
|
|
stateAdd(virtualFiles$, basepath, {
|
|
...file,
|
|
loading: true,
|
|
});
|
|
statePop(mutationFiles$, basepath, dirname); // case: rm followed by mkdir
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterSuccess() {
|
|
if (basepath === currentPath()) removeLoading(virtualFiles$, basepath, dirname);
|
|
else onDestroy(() => removeLoading(virtualFiles$, basepath, dirname));
|
|
onDestroy(() => statePop(virtualFiles$, basepath, dirname));
|
|
await fscache().update(basepath, ({ files = [], ...rest }) => ({
|
|
files: files.concat([file]),
|
|
...rest,
|
|
}));
|
|
hooks.mutation.emit({ op: "mkdir", path: basepath });
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterError() {
|
|
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 {
|
|
/**
|
|
* @override
|
|
*/
|
|
before() {
|
|
stateAdd(virtualFiles$, basepath, {
|
|
...file,
|
|
loading: true,
|
|
});
|
|
statePop(mutationFiles$, basepath, filename); // eg: rm followed by save
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterSuccess() {
|
|
if (basepath === currentPath()) removeLoading(virtualFiles$, basepath, filename);
|
|
else onDestroy(() => removeLoading(virtualFiles$, basepath, filename));
|
|
onDestroy(() => statePop(virtualFiles$, basepath, filename));
|
|
await fscache().update(basepath, ({ files = [], ...rest }) => ({
|
|
files: files.concat([file]),
|
|
...rest,
|
|
}));
|
|
hooks.mutation.emit({ op: "save", path: basepath });
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterError() {
|
|
statePop(virtualFiles$, basepath, filename);
|
|
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 {
|
|
/**
|
|
* @override
|
|
*/
|
|
before() {
|
|
for (let i=0; i<arr.length; i+=2) {
|
|
stateAdd(mutationFiles$, arr[i], {
|
|
name: arr[i+1],
|
|
fn: (file) => {
|
|
if (file.name === arr[i+1]) {
|
|
file.loading = true;
|
|
file.last = true;
|
|
}
|
|
return file;
|
|
},
|
|
});
|
|
statePop(virtualFiles$, arr[i], arr[i+1]); // eg: touch followed by rm
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterSuccess() {
|
|
for (let i=0; i<arr.length; i+=2) {
|
|
stateAdd(mutationFiles$, arr[i], {
|
|
name: arr[i+1],
|
|
fn: (file) => {
|
|
for (let i=0; i<arr.length; i+=2) {
|
|
if (file.name === arr[i+1]) return null;
|
|
}
|
|
return file;
|
|
},
|
|
});
|
|
}
|
|
onDestroy(() => {
|
|
for (let i=0; i<arr.length; i+=2) {
|
|
statePop(mutationFiles$, arr[i], arr[i+1]);
|
|
}
|
|
});
|
|
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,
|
|
}));
|
|
if (arr.length > 0) hooks.mutation.emit({ op: "rm", path: arr[0] });
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterError() {
|
|
for (let i=0; i<arr.length; i+=2) {
|
|
stateAdd(mutationFiles$, arr[i], {
|
|
name: arr[i+1],
|
|
fn: (file) => {
|
|
if (file.name === arr[i+1]) {
|
|
file = {
|
|
...file,
|
|
loading: false,
|
|
last: false
|
|
};
|
|
}
|
|
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 {
|
|
/**
|
|
* @override
|
|
*/
|
|
before() {
|
|
if (fromBasepath === toBasepath) this._beforeSamePath();
|
|
else this._beforeDifferentPath();
|
|
}
|
|
|
|
_beforeSamePath() {
|
|
stateAdd(mutationFiles$, fromBasepath, {
|
|
name: fromName,
|
|
fn: (file) => {
|
|
if (file.name === fromName) {
|
|
type = file.type;
|
|
return {
|
|
...file,
|
|
name: toName,
|
|
loading: true,
|
|
};
|
|
}
|
|
return file;
|
|
},
|
|
});
|
|
}
|
|
|
|
_beforeDifferentPath() {
|
|
stateAdd(mutationFiles$, fromBasepath, {
|
|
name: fromName,
|
|
fn: (file) => {
|
|
if (file.name === fromName) {
|
|
type = file.type;
|
|
return {
|
|
...file,
|
|
loading: true,
|
|
last: true,
|
|
};
|
|
}
|
|
return file;
|
|
},
|
|
});
|
|
stateAdd(virtualFiles$, toBasepath, {
|
|
name: toName,
|
|
loading: true,
|
|
type,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
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) {
|
|
file = { ...file, loading: false };
|
|
}
|
|
return file;
|
|
},
|
|
});
|
|
await fscache().update(fromBasepath, ({ files = [], ...rest }) => {
|
|
return {
|
|
files: files.map((file) => {
|
|
if (file.name === fromName) {
|
|
file.name = toName;
|
|
}
|
|
return file;
|
|
}),
|
|
...rest,
|
|
};
|
|
});
|
|
hooks.mutation.emit({ op: "mv", path: fromBasepath });
|
|
}
|
|
|
|
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),
|
|
...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);
|
|
hooks.mutation.emit({ op: "mv", path: fromBasepath });
|
|
hooks.mutation.emit({ op: "mv", path: toBasepath });
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async afterError() {
|
|
if (fromBasepath === toBasepath) stateAdd(mutationFiles$, fromBasepath, {
|
|
name: fromName,
|
|
fn: (file) => {
|
|
if (file.name === toName) return {
|
|
...file,
|
|
name: fromName,
|
|
loading: false,
|
|
};
|
|
return file;
|
|
},
|
|
}); else {
|
|
stateAdd(mutationFiles$, fromBasepath, {
|
|
name: fromName,
|
|
fn: (file) => {
|
|
if (file.name === fromName) return {
|
|
...file,
|
|
loading: false,
|
|
last: false,
|
|
};
|
|
return file;
|
|
},
|
|
});
|
|
statePop(virtualFiles$, toBasepath, toName);
|
|
}
|
|
statePop(mutationFiles$, fromBasepath, fromName);
|
|
return rxjs.EMPTY;
|
|
}
|
|
}();
|
|
}
|
|
|
|
export function ls(path) {
|
|
return rxjs.pipe(
|
|
// case1: 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.map((all) => all[path]),
|
|
rxjs.mergeMap((fns) => {
|
|
const shouldContinue = !!(fns && fns.length > 0);
|
|
if (!shouldContinue) return rxjs.of({ ...res, files });
|
|
for (let i=files.length-1; i>=0; i--) {
|
|
for (let j=0; j<fns.length; j++) {
|
|
files[i] = fns[j].fn(files[i]);
|
|
if (!files[i]) {
|
|
files.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return rxjs.of({ ...res, files });
|
|
}),
|
|
)),
|
|
// case2: virtual files = additional files we want to see displayed in the UI
|
|
rxjs.switchMap(({ files, ...res }) => virtualFiles$.pipe(
|
|
rxjs.map((all) => all[path] || []),
|
|
rxjs.distinctUntilChanged((prev, curr) => { // we only want to get notified of changes within "path"
|
|
if (prev.length !== curr.length) return false;
|
|
for (let i=0; i<prev.length; i++) {
|
|
if (prev[i].name !== curr[i].name) return false;
|
|
else if (prev[i].type !== curr[i].type) return false;
|
|
else if (prev[i].loading !== curr[i].loading) return false;
|
|
}
|
|
return true;
|
|
}),
|
|
rxjs.mergeMap((virtualFiles) => {
|
|
if (virtualFiles.length === 0) return rxjs.of({ ...res, files });
|
|
return rxjs.of({
|
|
...res,
|
|
files: files.concat(virtualFiles),
|
|
});
|
|
}),
|
|
)),
|
|
);
|
|
}
|
|
|
|
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 = arr.concat([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) {
|
|
return { ...file, loading: false };
|
|
}
|
|
return file;
|
|
}),
|
|
});
|
|
}
|