cista 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
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.
- cista/_version.py +1 -1
- cista/api.py +16 -5
- cista/app.py +40 -27
- cista/auth.py +33 -1
- cista/preview.py +114 -0
- cista/protocol.py +5 -2
- cista/serve.py +1 -1
- cista/util/apphelpers.py +3 -3
- cista/watching.py +271 -210
- cista/wwwroot/assets/{add-file-78f95102.js → add-file-e19e18ff.js} +1 -1
- cista/wwwroot/assets/{add-folder-ef91bb0e.js → add-folder-cf930a27.js} +1 -1
- cista/wwwroot/assets/{arrow-c279db10.js → arrow-f09bf4bc.js} +1 -1
- cista/wwwroot/assets/{arrows-h-0090937e.js → arrows-h-74714fc1.js} +1 -1
- cista/wwwroot/assets/{arrows-v-05d0dc3e.js → arrows-v-900e3e20.js} +1 -1
- cista/wwwroot/assets/{check-736f9156.js → check-5d5030f9.js} +1 -1
- cista/wwwroot/assets/{code-f442e1ae.js → code-08d0327a.js} +1 -1
- cista/wwwroot/assets/{copy-f9304d76.js → copy-d4e131a3.js} +1 -1
- cista/wwwroot/assets/{create-file-5cfbca5b.js → create-file-7ee36655.js} +1 -1
- cista/wwwroot/assets/{create-folder-f3416e7f.js → create-folder-4baad31c.js} +1 -1
- cista/wwwroot/assets/{cross-34310f33.js → cross-5dc70d0a.js} +1 -1
- cista/wwwroot/assets/{disk-9de14290.js → disk-1c8c6bb2.js} +1 -1
- cista/wwwroot/assets/{download-40f6e8d9.js → download-4c58df40.js} +1 -1
- cista/wwwroot/assets/{exclamation-67d2f487.js → exclamation-b23fc5a1.js} +1 -1
- cista/wwwroot/assets/{eye-1a9cae12.js → eye-fc97097a.js} +1 -1
- cista/wwwroot/assets/{find-f49bedc3.js → find-9f5d36c7.js} +1 -1
- cista/wwwroot/assets/{fullscreen-6e83f85d.js → fullscreen-72bac89d.js} +1 -1
- cista/wwwroot/assets/{github-33da7a52.js → github-8781f34a.js} +1 -1
- cista/wwwroot/assets/index-5ab4ce9a.css +1 -0
- cista/wwwroot/assets/index-c828fba8.js +13 -0
- cista/wwwroot/assets/{info-9a003b6c.js → info-81c1e2fa.js} +1 -1
- cista/wwwroot/assets/{link-b5bf40a7.js → link-ddc2f9ba.js} +1 -1
- cista/wwwroot/assets/{logo-adde2ca4.js → logo-10d7b218.js} +1 -1
- cista/wwwroot/assets/{loop-762ab430.js → loop-a579040b.js} +1 -1
- cista/wwwroot/assets/{menu-633b2a61.js → menu-cb4bdef2.js} +1 -1
- cista/wwwroot/assets/{next-d9d1d510.js → next-bb1c5152.js} +1 -1
- cista/wwwroot/assets/{open-91351e45.js → open-f8e4da33.js} +1 -1
- cista/wwwroot/assets/{paste-d3f337c1.js → paste-0bef6dfd.js} +1 -1
- cista/wwwroot/assets/{pause-8f82e536.js → pause-27898a74.js} +1 -1
- cista/wwwroot/assets/{pencil-0b434534.js → pencil-19d33c49.js} +1 -1
- cista/wwwroot/assets/{play-e0e51167.js → play-fe6706ce.js} +1 -1
- cista/wwwroot/assets/{plus-e2a2ec0f.js → plus-ab9d4dbd.js} +1 -1
- cista/wwwroot/assets/{previous-cd61ebe6.js → previous-5be4e762.js} +1 -1
- cista/wwwroot/assets/{reload-a9c668b2.js → reload-84455f3f.js} +1 -1
- cista/wwwroot/assets/{rename-bd15329b.js → rename-88ab1b4d.js} +1 -1
- cista/wwwroot/assets/{scissors-dcbf78c0.js → scissors-1266cf56.js} +1 -1
- cista/wwwroot/assets/{shuffle-74e9ea1c.js → shuffle-0412a143.js} +1 -1
- cista/wwwroot/assets/{signin-bbf26a1b.js → signin-dcc16c88.js} +1 -1
- cista/wwwroot/assets/{signout-caa34d68.js → signout-935b6b65.js} +1 -1
- cista/wwwroot/assets/{skip-423d5cf0.js → skip-adeae5b8.js} +1 -1
- cista/wwwroot/assets/{spinner-b299e14e.js → spinner-7c5e1e66.js} +1 -1
- cista/wwwroot/assets/{stop-91578a62.js → stop-6ec4fac4.js} +1 -1
- cista/wwwroot/assets/{trash-3b7b72a3.js → trash-218fd3df.js} +1 -1
- cista/wwwroot/assets/{triangle-724a2314.js → triangle-40a425a9.js} +1 -1
- cista/wwwroot/assets/{unfullscreen-29f4977c.js → unfullscreen-3d51fed5.js} +1 -1
- cista/wwwroot/assets/{up-arrow-ceb58d59.js → up-arrow-af385124.js} +1 -1
- cista/wwwroot/assets/{upload-cloud-936fb8b2.js → upload-cloud-68d0fe9f.js} +1 -1
- cista/wwwroot/assets/{user-cog-887c6f3f.js → user-cog-bca0b085.js} +1 -1
- cista/wwwroot/assets/{user-ab4ed9ac.js → user-dd4fef53.js} +1 -1
- cista/wwwroot/assets/{volume-high-74a17568.js → volume-high-0f8f0e3d.js} +1 -1
- cista/wwwroot/assets/{volume-low-f7170d5f.js → volume-low-b1bd663e.js} +1 -1
- cista/wwwroot/assets/{volume-medium-7b16c1db.js → volume-medium-cd88f329.js} +1 -1
- cista/wwwroot/assets/{volume-mute-0c7078a1.js → volume-mute-458a2f48.js} +1 -1
- cista/wwwroot/assets/{window-f7f79ada.js → window-b63b3c5a.js} +1 -1
- cista/wwwroot/assets/{window-cross-e3a75b33.js → window-cross-b0bd1b60.js} +1 -1
- cista/wwwroot/assets/{wordwrap-3bcce83c.js → wordwrap-26b55346.js} +1 -1
- cista/wwwroot/assets/{zoomin-bd27188c.js → zoomin-cb10ed9f.js} +1 -1
- cista/wwwroot/assets/{zoomout-31844707.js → zoomout-90bcc471.js} +1 -1
- cista/wwwroot/index.html +3 -4
- {cista-0.5.0.dist-info → cista-0.7.0.dist-info}/METADATA +15 -10
- cista-0.7.0.dist-info/RECORD +86 -0
- cista/wwwroot/assets/cog-fcdd928d.js +0 -1
- cista/wwwroot/assets/index-6716899e.js +0 -11
- cista/wwwroot/assets/index-bcee9add.css +0 -1
- cista-0.5.0.dist-info/RECORD +0 -86
- {cista-0.5.0.dist-info → cista-0.7.0.dist-info}/WHEEL +0 -0
- {cista-0.5.0.dist-info → cista-0.7.0.dist-info}/entry_points.txt +0 -0
cista/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This file is automatically generated by hatch build.
|
|
2
|
-
__version__ = '0.
|
|
2
|
+
__version__ = '0.7.0'
|
cista/api.py
CHANGED
|
@@ -111,13 +111,24 @@ async def watch(req, ws):
|
|
|
111
111
|
)
|
|
112
112
|
uuid = token_bytes(16)
|
|
113
113
|
try:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
q, space, root = await asyncio.get_event_loop().run_in_executor(
|
|
115
|
+
req.app.ctx.threadexec, subscribe, uuid, ws
|
|
116
|
+
)
|
|
117
|
+
await ws.send(space)
|
|
118
|
+
await ws.send(root)
|
|
119
119
|
# Send updates
|
|
120
120
|
while True:
|
|
121
121
|
await ws.send(await q.get())
|
|
122
122
|
finally:
|
|
123
123
|
del watching.pubsub[uuid]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def subscribe(uuid, ws):
|
|
127
|
+
with watching.state.lock:
|
|
128
|
+
q = watching.pubsub[uuid] = asyncio.Queue()
|
|
129
|
+
# Init with disk usage and full tree
|
|
130
|
+
return (
|
|
131
|
+
q,
|
|
132
|
+
watching.format_space(watching.state.space),
|
|
133
|
+
watching.format_root(watching.state.root),
|
|
134
|
+
)
|
cista/app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime
|
|
3
3
|
import mimetypes
|
|
4
|
+
import threading
|
|
4
5
|
from concurrent.futures import ThreadPoolExecutor
|
|
5
6
|
from pathlib import Path, PurePath, PurePosixPath
|
|
6
7
|
from stat import S_IFDIR, S_IFREG
|
|
@@ -10,12 +11,12 @@ from wsgiref.handlers import format_date_time
|
|
|
10
11
|
import brotli
|
|
11
12
|
import sanic.helpers
|
|
12
13
|
from blake3 import blake3
|
|
13
|
-
from sanic import Blueprint, Sanic, empty, raw
|
|
14
|
+
from sanic import Blueprint, Sanic, empty, raw, redirect
|
|
14
15
|
from sanic.exceptions import Forbidden, NotFound
|
|
15
|
-
from sanic.log import
|
|
16
|
+
from sanic.log import logger
|
|
16
17
|
from stream_zip import ZIP_AUTO, stream_zip
|
|
17
18
|
|
|
18
|
-
from cista import auth, config, session, watching
|
|
19
|
+
from cista import auth, config, preview, session, watching
|
|
19
20
|
from cista.api import bp
|
|
20
21
|
from cista.util.apphelpers import handle_sanic_exception
|
|
21
22
|
|
|
@@ -24,6 +25,7 @@ sanic.helpers._ENTITY_HEADERS = frozenset()
|
|
|
24
25
|
|
|
25
26
|
app = Sanic("cista", strict_slashes=True)
|
|
26
27
|
app.blueprint(auth.bp)
|
|
28
|
+
app.blueprint(preview.bp)
|
|
27
29
|
app.blueprint(bp)
|
|
28
30
|
app.exception(Exception)(handle_sanic_exception)
|
|
29
31
|
|
|
@@ -31,14 +33,15 @@ app.exception(Exception)(handle_sanic_exception)
|
|
|
31
33
|
@app.before_server_start
|
|
32
34
|
async def main_start(app, loop):
|
|
33
35
|
config.load_config()
|
|
34
|
-
await watching.start(app, loop)
|
|
35
36
|
app.ctx.threadexec = ThreadPoolExecutor(
|
|
36
37
|
max_workers=8, thread_name_prefix="cista-ioworker"
|
|
37
38
|
)
|
|
39
|
+
await watching.start(app, loop)
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
@app.after_server_stop
|
|
41
43
|
async def main_stop(app, loop):
|
|
44
|
+
quit.set()
|
|
42
45
|
await watching.stop(app, loop)
|
|
43
46
|
app.ctx.threadexec.shutdown()
|
|
44
47
|
|
|
@@ -122,7 +125,7 @@ def _load_wwwroot(www):
|
|
|
122
125
|
if not wwwnew:
|
|
123
126
|
msg = f"Web frontend missing from {base}\n Did you forget: hatch build\n"
|
|
124
127
|
if not www:
|
|
125
|
-
|
|
128
|
+
logger.warning(msg)
|
|
126
129
|
if not app.debug:
|
|
127
130
|
msg = "Web frontend missing. Cista installation is broken.\n"
|
|
128
131
|
wwwnew[""] = (
|
|
@@ -141,7 +144,7 @@ def _load_wwwroot(www):
|
|
|
141
144
|
async def start(app):
|
|
142
145
|
await load_wwwroot(app)
|
|
143
146
|
if app.debug:
|
|
144
|
-
app.add_task(refresh_wwwroot())
|
|
147
|
+
app.add_task(refresh_wwwroot(), name="refresh_wwwroot")
|
|
145
148
|
|
|
146
149
|
|
|
147
150
|
async def load_wwwroot(app):
|
|
@@ -151,27 +154,31 @@ async def load_wwwroot(app):
|
|
|
151
154
|
)
|
|
152
155
|
|
|
153
156
|
|
|
157
|
+
quit = threading.Event()
|
|
158
|
+
|
|
159
|
+
|
|
154
160
|
async def refresh_wwwroot():
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
161
|
+
try:
|
|
162
|
+
while not quit.is_set():
|
|
163
|
+
try:
|
|
164
|
+
wwwold = www
|
|
165
|
+
await load_wwwroot(app)
|
|
166
|
+
changes = ""
|
|
167
|
+
for name in sorted(www):
|
|
168
|
+
attr = www[name]
|
|
169
|
+
if wwwold.get(name) == attr:
|
|
170
|
+
continue
|
|
171
|
+
headers = attr[2]
|
|
172
|
+
changes += f"{headers['last-modified']} {headers['etag']} /{name}\n"
|
|
173
|
+
for name in sorted(set(wwwold) - set(www)):
|
|
174
|
+
changes += f"Deleted /{name}\n"
|
|
175
|
+
if changes:
|
|
176
|
+
print(f"Updated wwwroot:\n{changes}", end="", flush=True)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f"Error loading wwwroot: {e!r}")
|
|
179
|
+
await asyncio.sleep(0.5)
|
|
180
|
+
except asyncio.CancelledError:
|
|
181
|
+
pass
|
|
175
182
|
|
|
176
183
|
|
|
177
184
|
@app.route("/<path:path>", methods=["GET", "HEAD"])
|
|
@@ -191,6 +198,12 @@ async def wwwroot(req, path=""):
|
|
|
191
198
|
return raw(data, headers=headers)
|
|
192
199
|
|
|
193
200
|
|
|
201
|
+
@app.route("/favicon.ico", methods=["GET", "HEAD"])
|
|
202
|
+
async def favicon(req):
|
|
203
|
+
# Browsers keep asking for it when viewing files (not HTML with icon link)
|
|
204
|
+
return redirect("/assets/logo-97d1d7eb.svg", status=308)
|
|
205
|
+
|
|
206
|
+
|
|
194
207
|
def get_files(wanted: set) -> list[tuple[PurePosixPath, Path]]:
|
|
195
208
|
loc = PurePosixPath()
|
|
196
209
|
idx = 0
|
|
@@ -251,7 +264,7 @@ async def zip_download(req, keys, zipfile, ext):
|
|
|
251
264
|
for chunk in stream_zip(local_files(files)):
|
|
252
265
|
asyncio.run_coroutine_threadsafe(queue.put(chunk), loop).result()
|
|
253
266
|
except Exception:
|
|
254
|
-
|
|
267
|
+
logger.exception("Error streaming ZIP")
|
|
255
268
|
raise
|
|
256
269
|
finally:
|
|
257
270
|
asyncio.run_coroutine_threadsafe(queue.put(None), loop)
|
cista/auth.py
CHANGED
|
@@ -71,7 +71,7 @@ def verify(request, *, privileged=False):
|
|
|
71
71
|
raise Forbidden("Access Forbidden: Only for privileged users", quiet=True)
|
|
72
72
|
elif config.config.public or request.ctx.user:
|
|
73
73
|
return
|
|
74
|
-
raise Unauthorized("Login required", "cookie", quiet=True)
|
|
74
|
+
raise Unauthorized(f"Login required for {request.path}", "cookie", quiet=True)
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
bp = Blueprint("auth")
|
|
@@ -159,3 +159,35 @@ async def logout_post(request):
|
|
|
159
159
|
res = json({"message": msg})
|
|
160
160
|
session.delete(res)
|
|
161
161
|
return res
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@bp.post("/password-change")
|
|
165
|
+
async def change_password(request):
|
|
166
|
+
try:
|
|
167
|
+
if request.headers.content_type == "application/json":
|
|
168
|
+
username = request.json["username"]
|
|
169
|
+
pwchange = request.json["passwordChange"]
|
|
170
|
+
password = request.json["password"]
|
|
171
|
+
else:
|
|
172
|
+
username = request.form["username"][0]
|
|
173
|
+
pwchange = request.form["passwordChange"][0]
|
|
174
|
+
password = request.form["password"][0]
|
|
175
|
+
if not username or not password:
|
|
176
|
+
raise KeyError
|
|
177
|
+
except KeyError:
|
|
178
|
+
raise BadRequest(
|
|
179
|
+
"Missing username, passwordChange or password",
|
|
180
|
+
) from None
|
|
181
|
+
try:
|
|
182
|
+
user = login(username, password)
|
|
183
|
+
set_password(user, pwchange)
|
|
184
|
+
except ValueError as e:
|
|
185
|
+
raise Forbidden(str(e), context={"redirect": "/login"}) from e
|
|
186
|
+
|
|
187
|
+
if "text/html" in request.headers.accept:
|
|
188
|
+
res = redirect("/")
|
|
189
|
+
session.flash(res, "Password updated")
|
|
190
|
+
else:
|
|
191
|
+
res = json({"message": "Password updated"})
|
|
192
|
+
session.create(res, username)
|
|
193
|
+
return res
|
cista/preview.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import mimetypes
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from urllib.parse import unquote
|
|
7
|
+
from wsgiref.handlers import format_date_time
|
|
8
|
+
|
|
9
|
+
import av
|
|
10
|
+
import av.datasets
|
|
11
|
+
import fitz # PyMuPDF
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from sanic import Blueprint, empty, raw
|
|
14
|
+
from sanic.exceptions import NotFound
|
|
15
|
+
from sanic.log import logger
|
|
16
|
+
|
|
17
|
+
from cista import config
|
|
18
|
+
from cista.util.filename import sanitize
|
|
19
|
+
|
|
20
|
+
bp = Blueprint("preview", url_prefix="/preview")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@bp.get("/<path:path>")
|
|
24
|
+
async def preview(req, path):
|
|
25
|
+
"""Preview a file"""
|
|
26
|
+
maxsize = int(req.args.get("px", 1024))
|
|
27
|
+
maxzoom = float(req.args.get("zoom", 2.0))
|
|
28
|
+
quality = int(req.args.get("q", 40))
|
|
29
|
+
rel = PurePosixPath(sanitize(unquote(path)))
|
|
30
|
+
path = config.config.path / rel
|
|
31
|
+
stat = path.lstat()
|
|
32
|
+
etag = config.derived_secret(
|
|
33
|
+
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
|
34
|
+
).hex()
|
|
35
|
+
savename = PurePosixPath(path.name).with_suffix(".webp")
|
|
36
|
+
headers = {
|
|
37
|
+
"etag": etag,
|
|
38
|
+
"last-modified": format_date_time(stat.st_mtime),
|
|
39
|
+
"cache-control": "max-age=604800, immutable"
|
|
40
|
+
+ ("" if config.config.public else ", private"),
|
|
41
|
+
"content-type": "image/webp",
|
|
42
|
+
"content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
|
|
43
|
+
}
|
|
44
|
+
if req.headers.if_none_match == etag:
|
|
45
|
+
# The client has it cached, respond 304 Not Modified
|
|
46
|
+
return empty(304, headers=headers)
|
|
47
|
+
|
|
48
|
+
if not path.is_file():
|
|
49
|
+
raise NotFound("File not found")
|
|
50
|
+
|
|
51
|
+
img = await asyncio.get_event_loop().run_in_executor(
|
|
52
|
+
req.app.ctx.threadexec, dispatch, path, quality, maxsize, maxzoom
|
|
53
|
+
)
|
|
54
|
+
return raw(img, headers=headers)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def dispatch(path, quality, maxsize, maxzoom):
|
|
58
|
+
if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"):
|
|
59
|
+
return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
|
|
60
|
+
if mimetypes.guess_type(path.name)[0].startswith("video/"):
|
|
61
|
+
return process_video(path, quality=quality, maxsize=maxsize)
|
|
62
|
+
return process_image(path, quality=quality, maxsize=maxsize)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def process_image(path, *, maxsize, quality):
|
|
66
|
+
img = Image.open(path)
|
|
67
|
+
w, h = img.size
|
|
68
|
+
img.thumbnail((min(w, maxsize), min(h, maxsize)))
|
|
69
|
+
# Fix rotation based on EXIF data
|
|
70
|
+
try:
|
|
71
|
+
rotate_values = {3: 180, 6: 270, 8: 90}
|
|
72
|
+
orientation = img._getexif().get(274)
|
|
73
|
+
if orientation in rotate_values:
|
|
74
|
+
logger.debug(f"Rotating preview {path} by {rotate_values[orientation]}")
|
|
75
|
+
img = img.rotate(rotate_values[orientation], expand=True)
|
|
76
|
+
except AttributeError:
|
|
77
|
+
...
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error rotating preview image: {e}")
|
|
80
|
+
# Save as webp
|
|
81
|
+
imgdata = io.BytesIO()
|
|
82
|
+
img.save(imgdata, format="webp", quality=quality, method=4)
|
|
83
|
+
return imgdata.getvalue()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
|
87
|
+
pdf = fitz.open(path)
|
|
88
|
+
page = pdf.load_page(page_number)
|
|
89
|
+
w, h = page.rect[2:4]
|
|
90
|
+
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
|
91
|
+
mat = fitz.Matrix(zoom, zoom)
|
|
92
|
+
pix = page.get_pixmap(matrix=mat)
|
|
93
|
+
return pix.pil_tobytes(format="webp", quality=quality, method=4)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def process_video(path, *, maxsize, quality):
|
|
97
|
+
with av.open(str(path)) as container:
|
|
98
|
+
stream = container.streams.video[0]
|
|
99
|
+
rotation = (
|
|
100
|
+
stream.side_data
|
|
101
|
+
and stream.side_data.get(av.stream.SideData.DISPLAYMATRIX)
|
|
102
|
+
or 0
|
|
103
|
+
)
|
|
104
|
+
stream.codec_context.skip_frame = "NONKEY"
|
|
105
|
+
container.seek(container.duration // 8)
|
|
106
|
+
frame = next(container.decode(stream))
|
|
107
|
+
img = frame.to_image()
|
|
108
|
+
|
|
109
|
+
img.thumbnail((maxsize, maxsize))
|
|
110
|
+
imgdata = io.BytesIO()
|
|
111
|
+
if rotation:
|
|
112
|
+
img = img.rotate(rotation, expand=True)
|
|
113
|
+
img.save(imgdata, format="webp", quality=quality, method=4)
|
|
114
|
+
return imgdata.getvalue()
|
cista/protocol.py
CHANGED
|
@@ -112,7 +112,7 @@ class ErrorMsg(msgspec.Struct):
|
|
|
112
112
|
## Directory listings
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
class FileEntry(msgspec.Struct, array_like=True):
|
|
115
|
+
class FileEntry(msgspec.Struct, array_like=True, frozen=True):
|
|
116
116
|
level: int
|
|
117
117
|
name: str
|
|
118
118
|
key: str
|
|
@@ -120,9 +120,12 @@ class FileEntry(msgspec.Struct, array_like=True):
|
|
|
120
120
|
size: int
|
|
121
121
|
isfile: int
|
|
122
122
|
|
|
123
|
-
def
|
|
123
|
+
def __str__(self):
|
|
124
124
|
return self.key or "FileEntry()"
|
|
125
125
|
|
|
126
|
+
def __repr__(self):
|
|
127
|
+
return f"{self.name} ({self.size}, {self.mtime})"
|
|
128
|
+
|
|
126
129
|
|
|
127
130
|
class Update(msgspec.Struct, array_like=True):
|
|
128
131
|
...
|
cista/serve.py
CHANGED
|
@@ -51,7 +51,7 @@ def parse_listen(listen):
|
|
|
51
51
|
raise ValueError(
|
|
52
52
|
f"Directory for unix socket does not exist: {unix.parent}/",
|
|
53
53
|
)
|
|
54
|
-
return "http://localhost", {"unix": unix}
|
|
54
|
+
return "http://localhost", {"unix": unix.as_posix()}
|
|
55
55
|
if re.fullmatch(r"(\w+(-\w+)*\.)+\w{2,}", listen, re.UNICODE):
|
|
56
56
|
return f"https://{listen}", {"host": listen, "port": 443, "ssl": True}
|
|
57
57
|
try:
|
cista/util/apphelpers.py
CHANGED
|
@@ -21,7 +21,6 @@ def jres(data, **kwargs):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
async def handle_sanic_exception(request, e):
|
|
24
|
-
logger.exception(e)
|
|
25
24
|
context, code = {}, 500
|
|
26
25
|
message = str(e)
|
|
27
26
|
if isinstance(e, SanicException):
|
|
@@ -42,7 +41,7 @@ async def handle_sanic_exception(request, e):
|
|
|
42
41
|
res.cookies.add_cookie("message", message, max_age=5)
|
|
43
42
|
return res
|
|
44
43
|
# Otherwise use Sanic's default error page
|
|
45
|
-
return errorpages.HTMLRenderer(request, e, debug=request.app.debug).
|
|
44
|
+
return errorpages.HTMLRenderer(request, e, debug=request.app.debug).render()
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
def websocket_wrapper(handler):
|
|
@@ -54,13 +53,14 @@ def websocket_wrapper(handler):
|
|
|
54
53
|
auth.verify(request)
|
|
55
54
|
await handler(request, ws, *args, **kwargs)
|
|
56
55
|
except Exception as e:
|
|
57
|
-
logger.exception(e)
|
|
58
56
|
context, code, message = {}, 500, str(e) or "Internal Server Error"
|
|
59
57
|
if isinstance(e, SanicException):
|
|
60
58
|
context = e.context or {}
|
|
61
59
|
code = e.status_code
|
|
62
60
|
message = f"⚠️ {message}" if code < 500 else f"🛑 {message}"
|
|
63
61
|
await asend(ws, ErrorMsg({"code": code, "message": message, **context}))
|
|
62
|
+
if not getattr(e, "quiet", False) or code == 500:
|
|
63
|
+
logger.exception(f"{code} {e!r}")
|
|
64
64
|
raise
|
|
65
65
|
|
|
66
66
|
return wrapper
|