hfs 0.27.2 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/admin/assets/{index-509bb1d6.js → index-cbb42a0e.js} +104 -104
- package/admin/assets/{index-60a380a7.css → index-f8049da8.css} +1 -1
- package/{frontend/assets/sha512-bf915587.js → admin/assets/sha512-3273321f.js} +1 -1
- package/admin/index.html +2 -2
- package/frontend/assets/index-72e96bb2.js +85 -0
- package/frontend/assets/index-cbcc6ac5.css +1 -0
- package/{admin/assets/sha512-738f0943.js → frontend/assets/sha512-2c2fa926.js} +1 -1
- package/frontend/index.html +2 -2
- package/package.json +7 -5
- package/plugins/vhosting/plugin.js +1 -1
- package/src/QuickZipStream.js +1 -24
- package/src/adminApis.js +2 -4
- package/src/api.accounts.js +1 -1
- package/src/api.auth.js +4 -0
- package/src/api.vfs.js +4 -9
- package/src/apiMiddleware.js +10 -6
- package/src/config.js +2 -1
- package/src/const.js +2 -1
- package/src/frontEndApis.js +22 -1
- package/src/github.js +2 -0
- package/src/index.js +2 -1
- package/src/listen.js +5 -2
- package/src/log.js +2 -2
- package/src/middlewares.js +22 -36
- package/src/misc.js +26 -1
- package/src/plugins.js +5 -7
- package/src/serveFile.js +4 -2
- package/src/serveGuiFiles.js +1 -24
- package/src/update.js +2 -2
- package/src/upload.js +92 -0
- package/src/util-files.js +3 -12
- package/src/util-os.js +41 -0
- package/src/vfs.js +15 -10
- package/src/zip.js +6 -4
- package/frontend/assets/index-6e178dfd.css +0 -1
- package/frontend/assets/index-aea7654e.js +0 -85
package/src/serveGuiFiles.js
CHANGED
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
-
if (k2 === undefined) k2 = k;
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
-
if (mod && mod.__esModule) return mod;
|
|
21
|
-
var result = {};
|
|
22
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
-
__setModuleDefault(result, mod);
|
|
24
|
-
return result;
|
|
25
|
-
};
|
|
26
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
5
|
};
|
|
@@ -89,7 +66,7 @@ function serveProxied(port, uri) {
|
|
|
89
66
|
return;
|
|
90
67
|
console.debug('proxied on port', port);
|
|
91
68
|
let proxy;
|
|
92
|
-
|
|
69
|
+
import('koa-better-http-proxy').then(lib => // dynamic import to avoid having this in final distribution
|
|
93
70
|
proxy = lib.default('127.0.0.1:' + port, {
|
|
94
71
|
proxyReqPathResolver: (ctx) => shouldServeApp(ctx) ? '/' : ctx.path,
|
|
95
72
|
userResDecorator(res, data, ctx) {
|
package/src/update.js
CHANGED
|
@@ -30,7 +30,7 @@ async function update() {
|
|
|
30
30
|
throw "asset not found";
|
|
31
31
|
const url = asset.browser_download_url;
|
|
32
32
|
console.log("downloading", url);
|
|
33
|
-
const bin = process.
|
|
33
|
+
const bin = process.argv0;
|
|
34
34
|
const binPath = (0, path_1.dirname)(bin);
|
|
35
35
|
const binFile = (0, path_1.basename)(bin);
|
|
36
36
|
const newBinFile = 'new-' + binFile;
|
|
@@ -64,7 +64,7 @@ async function update() {
|
|
|
64
64
|
}
|
|
65
65
|
exports.update = update;
|
|
66
66
|
if (const_1.argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
|
|
67
|
-
const bin = process.
|
|
67
|
+
const bin = process.argv0;
|
|
68
68
|
(0, fs_1.renameSync)(bin, (0, path_1.join)((0, path_1.dirname)(bin), const_1.argv.updating));
|
|
69
69
|
console.log("renamed binary file to", const_1.argv.updating);
|
|
70
70
|
}
|
package/src/upload.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadWriter = exports.minAvailableMb = exports.deleteUnfinishedUploadsAfter = void 0;
|
|
7
|
+
const vfs_1 = require("./vfs");
|
|
8
|
+
const const_1 = require("./const");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const misc_1 = require("./misc");
|
|
12
|
+
const frontEndApis_1 = require("./frontEndApis");
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const util_os_1 = require("./util-os");
|
|
15
|
+
exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after');
|
|
16
|
+
exports.minAvailableMb = (0, config_1.defineConfig)('min_available_mb', 100);
|
|
17
|
+
const waitingToBeDeleted = {};
|
|
18
|
+
function uploadWriter(base, path, ctx) {
|
|
19
|
+
if (!base.source || !(0, vfs_1.hasPermission)(base, 'can_upload', ctx))
|
|
20
|
+
return fail(base.can_upload === false ? const_1.HTTP_FORBIDDEN : const_1.HTTP_UNAUTHORIZED);
|
|
21
|
+
const fullPath = (0, path_1.join)(base.source, path);
|
|
22
|
+
const dir = (0, path_1.dirname)(fullPath);
|
|
23
|
+
const min = exports.minAvailableMb.get() * (1 << 20);
|
|
24
|
+
const reqSize = Number(ctx.headers["content-length"]);
|
|
25
|
+
if (min && reqSize)
|
|
26
|
+
try {
|
|
27
|
+
if (reqSize > (0, util_os_1.getFreeDiskSync)(dir) - min)
|
|
28
|
+
return fail(const_1.HTTP_PAYLOAD_TOO_LARGE);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.warn("can't check disk size", String(e));
|
|
32
|
+
}
|
|
33
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
34
|
+
const keepName = (0, path_1.basename)(fullPath).slice(-200);
|
|
35
|
+
let tempName = (0, path_1.join)(dir, 'hfs$upload-' + keepName);
|
|
36
|
+
const resumable = fs_1.default.existsSync(tempName) && tempName;
|
|
37
|
+
if (resumable)
|
|
38
|
+
tempName = (0, path_1.join)(dir, 'hfs$upload2-' + keepName);
|
|
39
|
+
const resume = Number(ctx.query.resume);
|
|
40
|
+
const size = resumable && (0, misc_1.try_)(() => fs_1.default.statSync(resumable).size);
|
|
41
|
+
if (size === undefined) // stat failed
|
|
42
|
+
return fail(const_1.HTTP_SERVER_ERROR);
|
|
43
|
+
if (resume > size)
|
|
44
|
+
return fail(const_1.HTTP_RANGE_NOT_SATISFIABLE);
|
|
45
|
+
if (!resume && resumable) {
|
|
46
|
+
const timeout = 30;
|
|
47
|
+
(0, frontEndApis_1.notifyClient)(ctx, 'upload.resumable', { [path]: size, expires: Date.now() + timeout * 1000 });
|
|
48
|
+
delayedDelete(resumable, timeout, () => fs_1.default.rename(tempName, resumable, err => {
|
|
49
|
+
if (!err)
|
|
50
|
+
tempName = resumable;
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
const resuming = resume && resumable;
|
|
54
|
+
const ret = resuming ? fs_1.default.createWriteStream(resumable, { flags: 'r+', start: resume })
|
|
55
|
+
: fs_1.default.createWriteStream(tempName);
|
|
56
|
+
if (resuming) {
|
|
57
|
+
fs_1.default.rm(tempName, () => { });
|
|
58
|
+
tempName = resumable;
|
|
59
|
+
}
|
|
60
|
+
cancelDeletion(tempName);
|
|
61
|
+
ret.on('close', () => {
|
|
62
|
+
if (!ctx.req.aborted)
|
|
63
|
+
return fs_1.default.rename(tempName, fullPath, err => {
|
|
64
|
+
err && console.error("couldn't rename temp to", fullPath, String(err));
|
|
65
|
+
if (resumable)
|
|
66
|
+
delayedDelete(resumable, 0);
|
|
67
|
+
});
|
|
68
|
+
if (resumable) // we don't want to be left with 2 temp files
|
|
69
|
+
return delayedDelete(tempName, 0);
|
|
70
|
+
const sec = exports.deleteUnfinishedUploadsAfter.get();
|
|
71
|
+
if (typeof sec !== 'number')
|
|
72
|
+
return;
|
|
73
|
+
delayedDelete(tempName, sec);
|
|
74
|
+
});
|
|
75
|
+
return ret;
|
|
76
|
+
function delayedDelete(path, secs, cb) {
|
|
77
|
+
clearTimeout(waitingToBeDeleted[path]);
|
|
78
|
+
waitingToBeDeleted[path] = setTimeout(() => {
|
|
79
|
+
delete waitingToBeDeleted[path];
|
|
80
|
+
fs_1.default.rm(path, () => cb === null || cb === void 0 ? void 0 : cb());
|
|
81
|
+
}, secs * 1000);
|
|
82
|
+
}
|
|
83
|
+
function cancelDeletion(path) {
|
|
84
|
+
clearTimeout(waitingToBeDeleted[path]);
|
|
85
|
+
delete waitingToBeDeleted[path];
|
|
86
|
+
}
|
|
87
|
+
function fail(status) {
|
|
88
|
+
ctx.status = status;
|
|
89
|
+
(0, frontEndApis_1.notifyClient)(ctx, 'upload.status', { [path]: ctx.status }); // allow browsers to detect failure while still sending body
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.uploadWriter = uploadWriter;
|
package/src/util-files.js
CHANGED
|
@@ -4,14 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.prepareFolder = exports.unzip = exports.
|
|
7
|
+
exports.prepareFolder = exports.unzip = exports.dirStream = exports.adjustStaticPathForGlob = exports.isWindowsDrive = exports.dirTraversal = exports.watchDir = exports.readFileBusy = exports.isFile = exports.isDirectory = void 0;
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const misc_1 = require("./misc");
|
|
10
10
|
const fs_1 = require("fs");
|
|
11
11
|
const path_1 = require("path");
|
|
12
12
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
13
13
|
const const_1 = require("./const");
|
|
14
|
-
const
|
|
14
|
+
const util_os_1 = require("./util-os");
|
|
15
15
|
const stream_1 = require("stream");
|
|
16
16
|
// @ts-ignore
|
|
17
17
|
const unzip_stream_1 = __importDefault(require("unzip-stream"));
|
|
@@ -115,21 +115,12 @@ async function* dirStream(path, deep) {
|
|
|
115
115
|
async function getItemsToSkip(path) {
|
|
116
116
|
if (!const_1.IS_WINDOWS)
|
|
117
117
|
return;
|
|
118
|
-
const out = await
|
|
118
|
+
const out = await (0, util_os_1.runCmd)('dir', ['/ah', '/b', path.replace(/\//g, '\\')])
|
|
119
119
|
.catch(() => ''); // error in case of no matching file
|
|
120
120
|
return out.split('\r\n').slice(0, -1);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
exports.dirStream = dirStream;
|
|
124
|
-
function run(cmd, args = []) {
|
|
125
|
-
return new Promise((resolve, reject) => (0, child_process_1.execFile)('cmd', ['/c', cmd, ...args], (err, stdout) => {
|
|
126
|
-
if (err)
|
|
127
|
-
reject(err);
|
|
128
|
-
else
|
|
129
|
-
resolve(stdout);
|
|
130
|
-
}));
|
|
131
|
-
}
|
|
132
|
-
exports.run = run;
|
|
133
124
|
async function unzip(stream, cb) {
|
|
134
125
|
let pending = Promise.resolve();
|
|
135
126
|
return new Promise(resolve => stream.pipe(unzip_stream_1.default.Parse())
|
package/src/util-os.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCmd = exports.getDrives = exports.getFreeDiskSync = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const misc_1 = require("./misc");
|
|
7
|
+
const util_1 = require("util");
|
|
8
|
+
const const_1 = require("./const");
|
|
9
|
+
function getFreeDiskSync(path) {
|
|
10
|
+
var _a;
|
|
11
|
+
if (const_1.IS_WINDOWS) {
|
|
12
|
+
const drive = (0, path_1.resolve)(path).slice(0, 2).toUpperCase();
|
|
13
|
+
const out = (0, child_process_1.execSync)('wmic logicaldisk get FreeSpace,name /format:list').toString().replace(/\r/g, '');
|
|
14
|
+
const one = out.split(/\n\n+/).find(x => x.includes('Name=' + drive));
|
|
15
|
+
if (!one)
|
|
16
|
+
throw Error('miss');
|
|
17
|
+
return Number((_a = /FreeSpace=(\d+)/.exec(one)) === null || _a === void 0 ? void 0 : _a[1]);
|
|
18
|
+
}
|
|
19
|
+
const out = (0, misc_1.try_)(() => (0, child_process_1.execSync)(`df -k ${path}`).toString(), err => {
|
|
20
|
+
throw err.status === 1 ? Error('miss')
|
|
21
|
+
: err.status === 127 ? Error('unsupported')
|
|
22
|
+
: err;
|
|
23
|
+
});
|
|
24
|
+
if (!(out === null || out === void 0 ? void 0 : out.startsWith('Filesystem')))
|
|
25
|
+
throw Error('unsupported');
|
|
26
|
+
const one = out.split('\n')[1];
|
|
27
|
+
const free = Number(one.split(/\s+/)[3]);
|
|
28
|
+
return free * 1024;
|
|
29
|
+
}
|
|
30
|
+
exports.getFreeDiskSync = getFreeDiskSync;
|
|
31
|
+
async function getDrives() {
|
|
32
|
+
const { stdout } = await (0, util_1.promisify)(child_process_1.exec)('wmic logicaldisk get name');
|
|
33
|
+
return stdout.split('\n').slice(1).map(x => x.trim()).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
exports.getDrives = getDrives;
|
|
36
|
+
// execute win32 shell commands
|
|
37
|
+
async function runCmd(cmd, args = []) {
|
|
38
|
+
const { stdout, stderr } = await (0, util_1.promisify)(child_process_1.execFile)('cmd', ['/c', cmd, ...args]);
|
|
39
|
+
return stderr || stdout;
|
|
40
|
+
}
|
|
41
|
+
exports.runCmd = runCmd;
|
package/src/vfs.js
CHANGED
|
@@ -103,11 +103,17 @@ function saveVfs() {
|
|
|
103
103
|
}
|
|
104
104
|
exports.saveVfs = saveVfs;
|
|
105
105
|
function getNodeName(node) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const { name, source: s } = node;
|
|
107
|
+
if (name)
|
|
108
|
+
return name;
|
|
109
|
+
if (!s)
|
|
110
|
+
return ''; // should happen only for root
|
|
111
|
+
if (/^[a-zA-Z]:\\?$/.test(s))
|
|
112
|
+
return s.slice(0, 2); // exclude trailing slash
|
|
113
|
+
const base = (0, path_1.basename)(s);
|
|
114
|
+
if (/^[./\\]*$/.test(base)) // if empty or special-chars-only
|
|
115
|
+
return (0, path_1.basename)((0, path_1.resolve)(s)); // resolve to try to get more
|
|
116
|
+
return base;
|
|
111
117
|
}
|
|
112
118
|
exports.getNodeName = getNodeName;
|
|
113
119
|
async function nodeIsDirectory(node) {
|
|
@@ -125,8 +131,7 @@ async function* walkNode(parent, ctx, depth = 0, prefixPath = '') {
|
|
|
125
131
|
const { children, source } = parent;
|
|
126
132
|
const took = prefixPath ? undefined : new Set();
|
|
127
133
|
if (children)
|
|
128
|
-
for (
|
|
129
|
-
const child = children[idx];
|
|
134
|
+
for (const child of children) {
|
|
130
135
|
const name = prefixPath + getNodeName(child);
|
|
131
136
|
took === null || took === void 0 ? void 0 : took.add(name);
|
|
132
137
|
yield* workItem({
|
|
@@ -185,11 +190,11 @@ function inheritMasks(item, parent, virtualBasename) {
|
|
|
185
190
|
if (!masks)
|
|
186
191
|
return;
|
|
187
192
|
const o = {};
|
|
188
|
-
for (const k
|
|
193
|
+
for (const [k, v] of Object.entries(masks))
|
|
189
194
|
if (k.startsWith('**/'))
|
|
190
|
-
o[k.slice(3)] =
|
|
195
|
+
o[k.slice(3)] = v;
|
|
191
196
|
else if (k.startsWith(virtualBasename + '/'))
|
|
192
|
-
o[k.slice(virtualBasename.length + 1)] =
|
|
197
|
+
o[k.slice(virtualBasename.length + 1)] = v;
|
|
193
198
|
if (Object.keys(o).length)
|
|
194
199
|
item.masks = o;
|
|
195
200
|
}
|
package/src/zip.js
CHANGED
|
@@ -15,15 +15,17 @@ const path_1 = require("path");
|
|
|
15
15
|
const serveFile_1 = require("./serveFile");
|
|
16
16
|
const const_1 = require("./const");
|
|
17
17
|
async function zipStreamFromFolder(node, ctx) {
|
|
18
|
+
var _a;
|
|
18
19
|
ctx.status = const_1.HTTP_OK;
|
|
19
20
|
ctx.mime = 'zip';
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// ctx.query.list is undefined | string | string[]
|
|
22
|
+
const list = (_a = (0, misc_1.wantArray)(ctx.query.list)[0]) === null || _a === void 0 ? void 0 : _a.split('*'); // we are using * as separator because it cannot be used in a file name and doesn't need url encoding
|
|
23
|
+
const name = (list === null || list === void 0 ? void 0 : list.length) === 1 ? (0, path_1.basename)(list[0]) : (0, vfs_1.getNodeName)(node);
|
|
24
|
+
ctx.attachment(((0, misc_1.isWindowsDrive)(name) ? name[0] : (name || 'archive')) + '.zip');
|
|
22
25
|
const filter = (0, misc_1.pattern2filter)(String(ctx.query.search || ''));
|
|
23
|
-
const { list } = ctx.query;
|
|
24
26
|
const walker = !list ? (0, vfs_1.walkNode)(node, ctx, Infinity)
|
|
25
27
|
: (async function* () {
|
|
26
|
-
for await (const el of
|
|
28
|
+
for await (const el of list) {
|
|
27
29
|
const subNode = await (0, vfs_1.urlToNode)(el, ctx, node);
|
|
28
30
|
if (!subNode || !(0, vfs_1.hasPermission)(subNode, 'can_read', ctx))
|
|
29
31
|
continue;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--bg: #fff;--text: #555;--faint-contrast: #0002;--mild-contrast: #0005;--good-contrast: #000a;--button-bg: #68a;--button-text: #fff;--focus-color: #468}:root .theme-dark{--bg: #000;--text: #999;--faint-contrast: #fff2;--mild-contrast: #fff5;--good-contrast: #fffa;--button-bg: #345;--button-text: #999}:root .theme-dark body{color-scheme:dark}:root .theme-dark a{color:#8ac}:root .theme-dark .dialog-closer{background:#633}:root .theme-dark .dialog-icon{color:#ccc}:root .theme-dark .dialog-icon .icon{color:#aaa;margin-left:-1px;font-size:95%}:root .theme-dark .dialog-backdrop{background:rgba(51,51,51,.7333333333)}:root .theme-dark .error-msg{color:#b88;background-color:#623}body{background:var(--bg);margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,button,select,input{font-size:12pt}#root{max-width:50em;margin:auto;min-height:100vh;display:flex;flex-direction:column}body,input{color:var(--text)}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}input,select{padding:.3em .4em;border-radius:.5em;background:var(--bg);border-color:var(--mild-contrast);color:var(--good-contrast);max-width:100%;box-sizing:border-box}input[type=checkbox]{margin:0 1.3em 0 .8em;transform:scale(1.7);accent-color:var(--button-bg)}.hidden{display:none!important}.icon{font-size:1.2em}.icon.mirror:before{transform:scaleX(-1)}a{text-decoration:none;color:#57a}button{background-color:var(--button-bg);color:var(--button-text);padding:.5em 1em;border:transparent;text-decoration:none;border-radius:.3em;vertical-align:middle;cursor:pointer}button.toggled{opacity:.6}button:focus-visible,.breadcrumb:focus-visible{outline:3px solid var(--focus-color)}input:focus-visible,select:focus-visible,ul a:focus-visible{border-radius:.3em;border-color:transparent;outline:2px solid var(--focus-color)}.error-msg{background-color:#faa;color:#833;padding:.5em 1em}header{position:sticky;top:0;background:var(--bg);padding:.2em;z-index:1}.ani-working{animation:1s blink infinite}@keyframes blink{0%{opacity:1}50%{opacity:.2}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.spinner,.icon.spinner:before{animation:1.5s spin infinite linear}.icon.emoji.spinner{display:inline-block}.breadcrumb{padding:.1em .6em .2em;line-height:1.8em;border-radius:.7em;background-color:var(--button-bg);color:var(--button-text);border-top:1px solid #666;margin-right:-.1em}.breadcrumb:nth-child(-n+3) .icon{padding:0 .2em}#folder-stats{font-size:90%;margin:.4em 0 0 .5em;float:right}#folder-stats .icon{margin-right:.3em}header input{width:100%;margin:.2em auto;box-sizing:border-box}#filter-bar{display:flex;gap:.3em;margin:.5em 0}#filter-bar input{flex:1}#filter-bar button{padding:0 .5em}ul.dir{flex:1;padding:0;margin:0;clear:both}ul.dir li{display:block;list-style-type:none;margin-bottom:.3em;padding:.3em;border-top:1px solid var(--button-bg)}ul.dir li a{word-break:break-word;padding-right:.3em}ul.dir li a .icon{margin-right:.3em}ul.dir li a.container-folder:hover{text-decoration:underline}ul.dir li .entry-props{float:right;font-size:90%;margin-left:12px;margin-top:.2em}ul.dir li .entry-props .icon{margin:0 .3em}ul.dir li .entry-props .entry-size{display:inline-block}#menu-panel{margin-bottom:.2em}#menu-bar{display:flex;justify-content:space-evenly;flex-wrap:wrap}#menu-bar>*{flex:auto;margin:.1em}#menu-bar button{padding-left:0;padding-right:0}#menu-bar>a>button{width:100%}#searched{margin:.2em}#user-panel{display:flex;flex-direction:column;gap:1em}#user-panel a>button{width:100%}button label{cursor:inherit;margin-left:.5em}.dialog-backdrop.working{font-size:5em;animation:1s fade-in}.dialog-content{padding:.2em}.dialog{--color: var(--button-bg)}#paging{display:flex;position:sticky;bottom:0;background:var(--bg);gap:.5em;overflow-x:auto}#paging>button{flex:1;background:var(--button-bg);text-align:center}.upload-list td:nth-child(1){width:0}.upload-list td:nth-child(2){text-align:right;width:0;white-space:nowrap;padding-left:.5em}.upload-list td:nth-child(3){padding:.2em .5em;word-break:break-word}*{scrollbar-width:thin;scrollbar-color:var(--button-bg) var(--faint-contrast)}*::-webkit-scrollbar{width:12px}*::-webkit-scrollbar-track{background:var(--faint-contrast)}*::-webkit-scrollbar-thumb{background-color:var(--button-bg);border-radius:20px;border:1px solid var(--faint-contrast)}@media (max-width: 42em){body,button,select{font-size:14pt}#menu-bar button label{display:none}#filter-bar{margin:.2em 0}#filter-bar label{display:none}#filter-bar button{width:17.6vw;height:2.3em}.breadcrumb{word-break:break-all}.breadcrumb .icon{font-size:24px}}.dialog-backdrop{position:fixed;inset:0;background:#888a;display:flex;justify-content:center;align-items:center;z-index:1000}.dialog{background:#fff;background:var(--bg);padding:max(.5em,1vw);border-radius:1em;position:relative;margin:0 3vw;overflow:hidden;max-height:calc(100vh - 2em)}.dialog-icon{color:#fff;background-color:var(--color);position:absolute;top:0;width:1.8em;height:1.7em;text-align:center;border-radius:.8em 0}.dialog-title{margin-top:-.4em;font-size:110%}.dialog-icon~.dialog-title{text-align:center}.dialog-closer{border-radius:0 .8em;right:0;padding:0;background-color:#c99}.dialog-icon~.dialog-content{margin-top:2em}.dialog-type{left:0;top:0;overflow:hidden;line-height:1.7em}.dialog-content{overflow:auto;max-height:calc(100vh - 4em)}.dialog-content p{white-space:pre-wrap;margin:.5em 0}.dialog-confirm .dialog-content button{margin-top:1em}.dialog-alert-info{--color: #282 }.dialog-alert-warning{--color: #c91 }.dialog-alert-error{--color: #822}@media (max-width: 50em){.dialog-closer{font-size:120%}.dialog-icon~.dialog-content{margin-top:2em}}
|