Compare commits
4 Commits
d42f0f7601
...
37167a41a6
Author | SHA1 | Date | |
---|---|---|---|
37167a41a6 | |||
63f6008a0a | |||
4fd769cce2 | |||
2695fc67f3 |
@ -138,7 +138,8 @@ const download = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, zip and download
|
// Otherwise, zip and download
|
||||||
linkdl(`/zip/${Array.from(sel.keys).join('+')}/download.zip`)
|
const name = sel.keys.length === 1 ? sel.docs[sel.keys[0]].name : 'download'
|
||||||
|
linkdl(`/zip/${Array.from(sel.keys).join('+')}/${name}.zip`)
|
||||||
documentStore.selected.clear()
|
documentStore.selected.clear()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -55,11 +55,16 @@ async function sendChunk(file: File, start: number, end: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFileChangeHandler(event: Event) {
|
async function uploadHandler(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
const chunkSize = 1 << 20
|
const chunkSize = 1 << 20
|
||||||
if (target && target.files && target.files.length > 0) {
|
if (!target?.files?.length) {
|
||||||
const file = target.files[0]
|
documentStore.error = 'No files selected'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const idx in target.files) {
|
||||||
|
const file = target.files[idx]
|
||||||
|
console.log('Uploading', file)
|
||||||
const numChunks = Math.ceil(file.size / chunkSize)
|
const numChunks = Math.ceil(file.size / chunkSize)
|
||||||
const document = documentStore.pushUploadingDocuments(file.name)
|
const document = documentStore.pushUploadingDocuments(file.name)
|
||||||
open('bottomRight')
|
open('bottomRight')
|
||||||
@ -78,14 +83,14 @@ async function uploadFileChangeHandler(event: Event) {
|
|||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
ref="fileUploadButton"
|
ref="fileUploadButton"
|
||||||
@change="uploadFileChangeHandler"
|
@change="uploadHandler"
|
||||||
class="upload-input"
|
class="upload-input"
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
ref="folderUploadButton"
|
ref="folderUploadButton"
|
||||||
@change="uploadFileChangeHandler"
|
@change="uploadHandler"
|
||||||
class="upload-input"
|
class="upload-input"
|
||||||
type="file"
|
type="file"
|
||||||
webkitdirectory
|
webkitdirectory
|
||||||
|
41
cista/app.py
41
cista/app.py
@ -1,16 +1,22 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
from collections import deque
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from importlib.resources import files
|
from importlib.resources import files
|
||||||
|
from pathlib import Path
|
||||||
|
from stat import S_IFDIR, S_IFREG
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from wsgiref.handlers import format_date_time
|
from wsgiref.handlers import format_date_time
|
||||||
|
|
||||||
import brotli
|
import brotli
|
||||||
import sanic.helpers
|
import sanic.helpers
|
||||||
from blake3 import blake3
|
from blake3 import blake3
|
||||||
|
from natsort import natsorted, ns
|
||||||
from sanic import Blueprint, Sanic, empty, raw
|
from sanic import Blueprint, Sanic, empty, raw
|
||||||
from sanic.exceptions import Forbidden, NotFound
|
from sanic.exceptions import Forbidden, NotFound
|
||||||
from sanic.log import logging
|
from sanic.log import logging
|
||||||
|
from stream_zip import ZIP_AUTO, stream_zip
|
||||||
|
|
||||||
from cista import auth, config, session, watching
|
from cista import auth, config, session, watching
|
||||||
from cista.api import bp
|
from cista.api import bp
|
||||||
@ -168,14 +174,6 @@ async def wwwroot(req, path=""):
|
|||||||
return raw(data, headers=headers)
|
return raw(data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from collections import deque
|
|
||||||
from pathlib import Path
|
|
||||||
from stat import S_IFREG
|
|
||||||
|
|
||||||
from stream_zip import ZIP_AUTO, stream_zip
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/zip/<keys>/<zipfile:ext=zip>")
|
@app.get("/zip/<keys>/<zipfile:ext=zip>")
|
||||||
async def zip_download(req, keys, zipfile, ext):
|
async def zip_download(req, keys, zipfile, ext):
|
||||||
"""Download a zip archive of the given keys"""
|
"""Download a zip archive of the given keys"""
|
||||||
@ -190,17 +188,13 @@ async def zip_download(req, keys, zipfile, ext):
|
|||||||
rel = None
|
rel = None
|
||||||
if relpar or attr.key in wanted:
|
if relpar or attr.key in wanted:
|
||||||
rel = [*relpar, name] if relpar else [name]
|
rel = [*relpar, name] if relpar else [name]
|
||||||
wanted.remove(attr.key)
|
wanted.discard(attr.key)
|
||||||
if isinstance(attr, DirEntry):
|
isdir = isinstance(attr, DirEntry)
|
||||||
|
if isdir:
|
||||||
q.append((loc, rel, attr.dir))
|
q.append((loc, rel, attr.dir))
|
||||||
elif rel:
|
if rel:
|
||||||
files.append(
|
files.append(
|
||||||
(
|
("/".join(rel), Path(watching.rootpath.joinpath(*loc)))
|
||||||
"/".join(rel),
|
|
||||||
Path(watching.rootpath.joinpath(*loc)),
|
|
||||||
attr.mtime,
|
|
||||||
attr.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not files:
|
if not files:
|
||||||
@ -211,13 +205,16 @@ async def zip_download(req, keys, zipfile, ext):
|
|||||||
if wanted:
|
if wanted:
|
||||||
raise NotFound("Files not found", context={"missing": wanted})
|
raise NotFound("Files not found", context={"missing": wanted})
|
||||||
|
|
||||||
for rel, p, mtime, size in files:
|
files = natsorted(files, key=lambda f: f[0], alg=ns.IGNORECASE)
|
||||||
if not p.is_file():
|
|
||||||
raise NotFound(f"File not found {rel}")
|
|
||||||
|
|
||||||
def local_files(files):
|
def local_files(files):
|
||||||
for rel, p, mtime, size in files:
|
for rel, p in files:
|
||||||
modified = datetime.datetime.fromtimestamp(mtime, datetime.UTC)
|
s = p.stat()
|
||||||
|
size = s.st_size
|
||||||
|
modified = datetime.datetime.fromtimestamp(s.st_mtime, datetime.UTC)
|
||||||
|
if p.is_dir():
|
||||||
|
yield rel, modified, S_IFDIR | 0o755, ZIP_AUTO(size), b""
|
||||||
|
else:
|
||||||
yield rel, modified, S_IFREG | 0o644, ZIP_AUTO(size), contents(p)
|
yield rel, modified, S_IFREG | 0o644, ZIP_AUTO(size), contents(p)
|
||||||
|
|
||||||
def contents(name):
|
def contents(name):
|
||||||
|
@ -71,7 +71,7 @@ def verify(request, *, privileged=False):
|
|||||||
raise Forbidden("Access Forbidden: Only for privileged users")
|
raise Forbidden("Access Forbidden: Only for privileged users")
|
||||||
elif config.config.public or request.ctx.user:
|
elif config.config.public or request.ctx.user:
|
||||||
return
|
return
|
||||||
raise Unauthorized("Login required", "cookie", context={"redirect": "/login"})
|
raise Unauthorized("Login required", "cookie")
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("auth")
|
bp = Blueprint("auth")
|
||||||
|
@ -30,7 +30,10 @@ def run(*, dev=False):
|
|||||||
reload_dir={confdir, wwwroot},
|
reload_dir={confdir, wwwroot},
|
||||||
access_log=True,
|
access_log=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
if dev:
|
||||||
Sanic.serve()
|
Sanic.serve()
|
||||||
|
else:
|
||||||
|
Sanic.serve_single()
|
||||||
|
|
||||||
|
|
||||||
def check_cert(certdir, domain):
|
def check_cert(certdir, domain):
|
||||||
|
@ -10,4 +10,7 @@ def sanitize(filename: str) -> str:
|
|||||||
filename = filename.replace("\\", "-")
|
filename = filename.replace("\\", "-")
|
||||||
filename = sanitize_filepath(filename)
|
filename = sanitize_filepath(filename)
|
||||||
filename = filename.strip("/")
|
filename = filename.strip("/")
|
||||||
return PurePosixPath(filename).as_posix()
|
p = PurePosixPath(filename)
|
||||||
|
if any(n.startswith(".") for n in p.parts):
|
||||||
|
raise ValueError("Filenames starting with dot are not allowed")
|
||||||
|
return p.as_posix()
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
|
|
||||||
import inotify.adapters
|
|
||||||
import msgspec
|
import msgspec
|
||||||
from sanic.log import logging
|
from sanic.log import logging
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ disk_usage = None
|
|||||||
|
|
||||||
def watcher_thread(loop):
|
def watcher_thread(loop):
|
||||||
global disk_usage, rootpath
|
global disk_usage, rootpath
|
||||||
|
import inotify.adapters
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
rootpath = config.config.path
|
rootpath = config.config.path
|
||||||
@ -39,7 +40,6 @@ def watcher_thread(loop):
|
|||||||
with tree_lock:
|
with tree_lock:
|
||||||
# Initialize the tree from filesystem
|
# Initialize the tree from filesystem
|
||||||
tree[""] = walk(rootpath)
|
tree[""] = walk(rootpath)
|
||||||
print(" ".join(tree[""].dir.keys()))
|
|
||||||
msg = format_tree()
|
msg = format_tree()
|
||||||
if msg != old:
|
if msg != old:
|
||||||
asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
|
asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
|
||||||
@ -74,6 +74,28 @@ def watcher_thread(loop):
|
|||||||
i = None # Free the inotify object
|
i = None # Free the inotify object
|
||||||
|
|
||||||
|
|
||||||
|
def watcher_thread_poll(loop):
|
||||||
|
global disk_usage, rootpath
|
||||||
|
|
||||||
|
while not quit:
|
||||||
|
rootpath = config.config.path
|
||||||
|
old = format_tree() if tree[""] else None
|
||||||
|
with tree_lock:
|
||||||
|
# Initialize the tree from filesystem
|
||||||
|
tree[""] = walk(rootpath)
|
||||||
|
msg = format_tree()
|
||||||
|
if msg != old:
|
||||||
|
asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
|
||||||
|
|
||||||
|
# Disk usage update
|
||||||
|
du = shutil.disk_usage(rootpath)
|
||||||
|
if du != disk_usage:
|
||||||
|
disk_usage = du
|
||||||
|
asyncio.run_coroutine_threadsafe(broadcast(format_du()), loop)
|
||||||
|
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
|
||||||
def format_du():
|
def format_du():
|
||||||
return msgspec.json.encode(
|
return msgspec.json.encode(
|
||||||
{
|
{
|
||||||
@ -201,7 +223,10 @@ async def broadcast(msg):
|
|||||||
|
|
||||||
async def start(app, loop):
|
async def start(app, loop):
|
||||||
config.load_config()
|
config.load_config()
|
||||||
app.ctx.watcher = threading.Thread(target=watcher_thread, args=[loop])
|
app.ctx.watcher = threading.Thread(
|
||||||
|
target=watcher_thread if sys.platform == "linux" else watcher_thread_poll,
|
||||||
|
args=[loop],
|
||||||
|
)
|
||||||
app.ctx.watcher.start()
|
app.ctx.watcher.start()
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,11 +20,12 @@ dependencies = [
|
|||||||
"docopt",
|
"docopt",
|
||||||
"inotify",
|
"inotify",
|
||||||
"msgspec",
|
"msgspec",
|
||||||
|
"natsort",
|
||||||
"pathvalidate",
|
"pathvalidate",
|
||||||
"pyjwt",
|
"pyjwt",
|
||||||
"sanic",
|
"sanic",
|
||||||
"tomli_w",
|
|
||||||
"stream-zip",
|
"stream-zip",
|
||||||
|
"tomli_w",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user