Compare commits
No commits in common. "37167a41a6a5f643acde3485d2a6e287a6e31415" and "d42f0f76018cf97bd473910802b1194e1b983124" have entirely different histories.
37167a41a6
...
d42f0f7601
@ -138,8 +138,7 @@ const download = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, zip and download
|
// Otherwise, zip and download
|
||||||
const name = sel.keys.length === 1 ? sel.docs[sel.keys[0]].name : 'download'
|
linkdl(`/zip/${Array.from(sel.keys).join('+')}/download.zip`)
|
||||||
linkdl(`/zip/${Array.from(sel.keys).join('+')}/${name}.zip`)
|
|
||||||
documentStore.selected.clear()
|
documentStore.selected.clear()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -55,16 +55,11 @@ async function sendChunk(file: File, start: number, end: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadHandler(event: Event) {
|
async function uploadFileChangeHandler(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
const chunkSize = 1 << 20
|
const chunkSize = 1 << 20
|
||||||
if (!target?.files?.length) {
|
if (target && target.files && target.files.length > 0) {
|
||||||
documentStore.error = 'No files selected'
|
const file = target.files[0]
|
||||||
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')
|
||||||
@ -83,14 +78,14 @@ async function uploadHandler(event: Event) {
|
|||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
ref="fileUploadButton"
|
ref="fileUploadButton"
|
||||||
@change="uploadHandler"
|
@change="uploadFileChangeHandler"
|
||||||
class="upload-input"
|
class="upload-input"
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
ref="folderUploadButton"
|
ref="folderUploadButton"
|
||||||
@change="uploadHandler"
|
@change="uploadFileChangeHandler"
|
||||||
class="upload-input"
|
class="upload-input"
|
||||||
type="file"
|
type="file"
|
||||||
webkitdirectory
|
webkitdirectory
|
||||||
|
43
cista/app.py
43
cista/app.py
@ -1,22 +1,16 @@
|
|||||||
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
|
||||||
@ -174,6 +168,14 @@ 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"""
|
||||||
@ -188,13 +190,17 @@ 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.discard(attr.key)
|
wanted.remove(attr.key)
|
||||||
isdir = isinstance(attr, DirEntry)
|
if isinstance(attr, DirEntry):
|
||||||
if isdir:
|
|
||||||
q.append((loc, rel, attr.dir))
|
q.append((loc, rel, attr.dir))
|
||||||
if rel:
|
elif 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:
|
||||||
@ -205,17 +211,14 @@ 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})
|
||||||
|
|
||||||
files = natsorted(files, key=lambda f: f[0], alg=ns.IGNORECASE)
|
for rel, p, mtime, size in files:
|
||||||
|
if not p.is_file():
|
||||||
|
raise NotFound(f"File not found {rel}")
|
||||||
|
|
||||||
def local_files(files):
|
def local_files(files):
|
||||||
for rel, p in files:
|
for rel, p, mtime, size in files:
|
||||||
s = p.stat()
|
modified = datetime.datetime.fromtimestamp(mtime, datetime.UTC)
|
||||||
size = s.st_size
|
yield rel, modified, S_IFREG | 0o644, ZIP_AUTO(size), contents(p)
|
||||||
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)
|
|
||||||
|
|
||||||
def contents(name):
|
def contents(name):
|
||||||
with name.open("rb") as f:
|
with name.open("rb") as f:
|
||||||
|
@ -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")
|
raise Unauthorized("Login required", "cookie", context={"redirect": "/login"})
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("auth")
|
bp = Blueprint("auth")
|
||||||
|
@ -30,10 +30,7 @@ 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,7 +10,4 @@ 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("/")
|
||||||
p = PurePosixPath(filename)
|
return PurePosixPath(filename).as_posix()
|
||||||
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,7 +31,6 @@ 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
|
||||||
@ -40,6 +39,7 @@ 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,28 +74,6 @@ 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(
|
||||||
{
|
{
|
||||||
@ -223,10 +201,7 @@ 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(
|
app.ctx.watcher = threading.Thread(target=watcher_thread, args=[loop])
|
||||||
target=watcher_thread if sys.platform == "linux" else watcher_thread_poll,
|
|
||||||
args=[loop],
|
|
||||||
)
|
|
||||||
app.ctx.watcher.start()
|
app.ctx.watcher.start()
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,12 +20,11 @@ dependencies = [
|
|||||||
"docopt",
|
"docopt",
|
||||||
"inotify",
|
"inotify",
|
||||||
"msgspec",
|
"msgspec",
|
||||||
"natsort",
|
|
||||||
"pathvalidate",
|
"pathvalidate",
|
||||||
"pyjwt",
|
"pyjwt",
|
||||||
"sanic",
|
"sanic",
|
||||||
"stream-zip",
|
|
||||||
"tomli_w",
|
"tomli_w",
|
||||||
|
"stream-zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user