cista 1.4.0__tar.gz → 1.4.2__tar.gz
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-1.4.0 → cista-1.4.2}/PKG-INFO +34 -1
- {cista-1.4.0 → cista-1.4.2}/README.md +33 -0
- {cista-1.4.0 → cista-1.4.2}/cista/__main__.py +36 -9
- {cista-1.4.0 → cista-1.4.2}/cista/_version.py +1 -1
- {cista-1.4.0 → cista-1.4.2}/cista/api.py +13 -8
- {cista-1.4.0 → cista-1.4.2}/cista/app.py +25 -11
- {cista-1.4.0 → cista-1.4.2}/cista/auth.py +13 -16
- cista-1.4.2/cista/frontend-build/assets/index-C8Gp9T8Z.js +32 -0
- cista-1.4.2/cista/frontend-build/assets/index-zDODUQOB.css +1 -0
- cista-1.4.2/cista/frontend-build/assets/searchWorker-CxfnO8mP.js +1 -0
- {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/index.html +2 -2
- cista-1.4.0/cista/frontend-build/assets/index-D4WrmH4f.js +0 -32
- cista-1.4.0/cista/frontend-build/assets/index-DQ2iYrs-.css +0 -1
- {cista-1.4.0 → cista-1.4.2}/.gitignore +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/__init__.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/config.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/droppy.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/fileio.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/assets/icons-DMD182WZ.js +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/assets/logo-ctv8tVwU.svg +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/robots.txt +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/preview.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/protocol.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/serve.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/server80.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/session.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/sso.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/util/__init__.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/util/apphelpers.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/util/asynclink.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/util/filename.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/util/lrucache.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/util/pwgen.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/cista/watching.py +0 -0
- {cista-1.4.0 → cista-1.4.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cista
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Dropbox-like file server with modern web interface
|
|
5
5
|
Project-URL: Homepage, https://git.zi.fi/Vasanko/cista-storage
|
|
6
6
|
Author: Vasanko
|
|
@@ -89,6 +89,39 @@ pip install cista --break-system-packages
|
|
|
89
89
|
|
|
90
90
|
The server remembers its settings in the config folder (default `~/.local/share/cista/`), including the listen port and directory, for future runs without arguments.
|
|
91
91
|
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
Cista supports three authentication modes:
|
|
95
|
+
|
|
96
|
+
### Built-in Authentication (default)
|
|
97
|
+
|
|
98
|
+
User accounts are managed directly by Cista. Create users with the `--user` flag:
|
|
99
|
+
|
|
100
|
+
```fish
|
|
101
|
+
uvx cista --user admin --privileged # Create admin user
|
|
102
|
+
uvx cista --user guest # Create regular user
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Privileged users can manage other users and change settings via the Admin Settings menu.
|
|
106
|
+
|
|
107
|
+
### Public Mode
|
|
108
|
+
|
|
109
|
+
In public mode, anyone can read, send and even delete files without without logging in. Privileged users can still log in via the menu to access admin settings, from where the public mode can be toggled on or off.
|
|
110
|
+
|
|
111
|
+
### Paskia SSO Authentication
|
|
112
|
+
|
|
113
|
+
For centralized authentication, Cista can integrate with [Paskia](https://git.zi.fi/LeoVasanko/paskia) SSO server. Set the `PASKIA_BACKEND_URL` environment variable:
|
|
114
|
+
|
|
115
|
+
```fish
|
|
116
|
+
PASKIA_BACKEND_URL=http://localhost:4401 uvx cista
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
In Paskia mode:
|
|
120
|
+
- All `/auth/*` requests are proxied to the Paskia backend
|
|
121
|
+
- Users with `cista:login` permission can access files
|
|
122
|
+
- Users with `cista:admin` permission get privileged access (Admin Settings)
|
|
123
|
+
- Public mode works with Paskia: unauthenticated users can browse, while the menu has option to login
|
|
124
|
+
|
|
92
125
|
### Internet Access
|
|
93
126
|
|
|
94
127
|
Most admins find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
|
|
@@ -38,6 +38,39 @@ pip install cista --break-system-packages
|
|
|
38
38
|
|
|
39
39
|
The server remembers its settings in the config folder (default `~/.local/share/cista/`), including the listen port and directory, for future runs without arguments.
|
|
40
40
|
|
|
41
|
+
## Authentication
|
|
42
|
+
|
|
43
|
+
Cista supports three authentication modes:
|
|
44
|
+
|
|
45
|
+
### Built-in Authentication (default)
|
|
46
|
+
|
|
47
|
+
User accounts are managed directly by Cista. Create users with the `--user` flag:
|
|
48
|
+
|
|
49
|
+
```fish
|
|
50
|
+
uvx cista --user admin --privileged # Create admin user
|
|
51
|
+
uvx cista --user guest # Create regular user
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Privileged users can manage other users and change settings via the Admin Settings menu.
|
|
55
|
+
|
|
56
|
+
### Public Mode
|
|
57
|
+
|
|
58
|
+
In public mode, anyone can read, send and even delete files without without logging in. Privileged users can still log in via the menu to access admin settings, from where the public mode can be toggled on or off.
|
|
59
|
+
|
|
60
|
+
### Paskia SSO Authentication
|
|
61
|
+
|
|
62
|
+
For centralized authentication, Cista can integrate with [Paskia](https://git.zi.fi/LeoVasanko/paskia) SSO server. Set the `PASKIA_BACKEND_URL` environment variable:
|
|
63
|
+
|
|
64
|
+
```fish
|
|
65
|
+
PASKIA_BACKEND_URL=http://localhost:4401 uvx cista
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
In Paskia mode:
|
|
69
|
+
- All `/auth/*` requests are proxied to the Paskia backend
|
|
70
|
+
- Users with `cista:login` permission can access files
|
|
71
|
+
- Users with `cista:admin` permission get privileged access (Admin Settings)
|
|
72
|
+
- Public mode works with Paskia: unauthenticated users can browse, while the menu has option to login
|
|
73
|
+
|
|
41
74
|
### Internet Access
|
|
42
75
|
|
|
43
76
|
Most admins find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
|
|
@@ -25,6 +25,28 @@ def create_banner():
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def create_startup_box(*, folder, url, unix=None, dev=False, paskia_url=None):
|
|
29
|
+
"""Create a framed startup box with server information."""
|
|
30
|
+
title = f"Cista {cista.__version__}"
|
|
31
|
+
listen = f"{url} ({unix})" if unix else url
|
|
32
|
+
location = f"{folder} @ {listen}"
|
|
33
|
+
lines = [title, location]
|
|
34
|
+
if paskia_url:
|
|
35
|
+
lines.append(f"Paskia: {paskia_url}")
|
|
36
|
+
if dev:
|
|
37
|
+
lines.append("dev mode")
|
|
38
|
+
|
|
39
|
+
# Calculate width based on content
|
|
40
|
+
inner_width = max(len(line) for line in lines) + 2
|
|
41
|
+
|
|
42
|
+
# Build the box
|
|
43
|
+
box = [f"╭{'─' * inner_width}╮"]
|
|
44
|
+
for line in lines:
|
|
45
|
+
box.append(f"│ {line:<{inner_width - 1}}│")
|
|
46
|
+
box.append(f"╰{'─' * inner_width}╯")
|
|
47
|
+
return "\n".join(box) + "\n"
|
|
48
|
+
|
|
49
|
+
|
|
28
50
|
banner = create_banner()
|
|
29
51
|
|
|
30
52
|
doc = """\
|
|
@@ -83,8 +105,7 @@ def _main():
|
|
|
83
105
|
elif "--version" in sys.argv:
|
|
84
106
|
sys.stdout.write(f"cista {cista.__version__}\n")
|
|
85
107
|
return 0
|
|
86
|
-
|
|
87
|
-
sys.stderr.write(banner)
|
|
108
|
+
# Don't print banner yet for normal startup - we'll print the startup box later
|
|
88
109
|
args = docopt(doc)
|
|
89
110
|
if args["--user"]:
|
|
90
111
|
return _user(args)
|
|
@@ -121,17 +142,23 @@ def _main():
|
|
|
121
142
|
elif not exists:
|
|
122
143
|
settings["listen"] = ":8000"
|
|
123
144
|
operation = config.update_config(settings)
|
|
124
|
-
sys.stderr.write(f"Config {operation}: {config.conffile}\n")
|
|
125
145
|
# Prepare to serve
|
|
126
|
-
|
|
127
|
-
url, _ = serve.parse_listen(config.config.listen)
|
|
146
|
+
url, opts = serve.parse_listen(config.config.listen)
|
|
128
147
|
if not config.config.path.is_dir():
|
|
129
148
|
raise ValueError(f"No such directory: {config.config.path}")
|
|
130
|
-
extra = f" ({unix})" if unix else ""
|
|
131
149
|
dev = args["--dev"]
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
150
|
+
# Check for Paskia SSO
|
|
151
|
+
from cista.sso import PASKIA_BACKEND_URL
|
|
152
|
+
|
|
153
|
+
# Print startup box
|
|
154
|
+
startup_box = create_startup_box(
|
|
155
|
+
folder=config.config.path,
|
|
156
|
+
url=url,
|
|
157
|
+
unix=opts.get("unix"),
|
|
158
|
+
dev=dev,
|
|
159
|
+
paskia_url=PASKIA_BACKEND_URL or None,
|
|
160
|
+
)
|
|
161
|
+
sys.stderr.write(startup_box)
|
|
135
162
|
# Run the server
|
|
136
163
|
serve.run(dev=dev)
|
|
137
164
|
return 0
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This file is automatically generated by hatch build.
|
|
2
|
-
__version__ = '1.4.
|
|
2
|
+
__version__ = '1.4.2'
|
|
@@ -95,14 +95,19 @@ async def control(req, ws):
|
|
|
95
95
|
async def watch(req, ws):
|
|
96
96
|
# Build user info from either built-in auth or SSO
|
|
97
97
|
user_info = None
|
|
98
|
-
if
|
|
99
|
-
# SSO auth (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
if sso.paskia_enabled():
|
|
99
|
+
# SSO auth: call validation to get user info (don't enforce auth in public mode)
|
|
100
|
+
try:
|
|
101
|
+
await sso.validate_sso_request(req)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass # Ignore auth errors, user_info stays None
|
|
104
|
+
if sso_user := getattr(req.ctx, "sso_user", None):
|
|
105
|
+
ctx = sso_user.get("ctx", {})
|
|
106
|
+
perms = ctx.get("permissions", [])
|
|
107
|
+
user_info = {
|
|
108
|
+
"username": ctx.get("user", {}).get("display_name", ""),
|
|
109
|
+
"privileged": "cista:admin" in perms,
|
|
110
|
+
}
|
|
106
111
|
elif req.ctx.user:
|
|
107
112
|
# Built-in auth: use local user database
|
|
108
113
|
user_info = {
|
|
@@ -43,10 +43,13 @@ setproctitle("cista-main")
|
|
|
43
43
|
async def main_start(app):
|
|
44
44
|
config.load_config()
|
|
45
45
|
setproctitle(f"cista {config.config.path.name}")
|
|
46
|
-
|
|
46
|
+
# Small pool for memory-intensive preview generation
|
|
47
|
+
preview_workers = max(2, min(8, cpu_count()))
|
|
47
48
|
app.ctx.threadexec = ThreadPoolExecutor(
|
|
48
|
-
max_workers=
|
|
49
|
+
max_workers=preview_workers, thread_name_prefix="cista-preview"
|
|
49
50
|
)
|
|
51
|
+
# Larger pool for long-running but low-memory zip operations
|
|
52
|
+
app.ctx.zipexec = ThreadPoolExecutor(max_workers=32, thread_name_prefix="cista-zip")
|
|
50
53
|
watching.start(app)
|
|
51
54
|
|
|
52
55
|
|
|
@@ -56,6 +59,7 @@ async def main_stop(app):
|
|
|
56
59
|
quit.set()
|
|
57
60
|
watching.stop(app)
|
|
58
61
|
app.ctx.threadexec.shutdown()
|
|
62
|
+
app.ctx.zipexec.shutdown(cancel_futures=True)
|
|
59
63
|
await sso.close_client()
|
|
60
64
|
logger.debug("Cista worker threads all finished")
|
|
61
65
|
|
|
@@ -257,10 +261,7 @@ def get_files(wanted: set) -> list[tuple[PurePosixPath, Path]]:
|
|
|
257
261
|
@app.get("/zip/<keys>/<zipfile:ext=zip>")
|
|
258
262
|
async def zip_download(req, keys, zipfile, ext):
|
|
259
263
|
"""Download a zip archive of the given keys"""
|
|
260
|
-
|
|
261
|
-
await auth.verify_sso(req)
|
|
262
|
-
else:
|
|
263
|
-
auth.verify(req)
|
|
264
|
+
await auth.verify(req)
|
|
264
265
|
|
|
265
266
|
wanted = set(keys.split("+"))
|
|
266
267
|
files = get_files(wanted)
|
|
@@ -291,27 +292,40 @@ async def zip_download(req, keys, zipfile, ext):
|
|
|
291
292
|
yield chunk
|
|
292
293
|
assert size == 0
|
|
293
294
|
|
|
295
|
+
pending_put = None # Current queue.put future, can be cancelled
|
|
296
|
+
|
|
294
297
|
def worker():
|
|
298
|
+
nonlocal pending_put
|
|
295
299
|
try:
|
|
296
300
|
for chunk in stream_zip(local_files(files)):
|
|
297
|
-
asyncio.run_coroutine_threadsafe(queue.put(chunk), loop)
|
|
301
|
+
future = asyncio.run_coroutine_threadsafe(queue.put(chunk), loop)
|
|
302
|
+
pending_put = future
|
|
303
|
+
future.result() # Blocks until queue has space
|
|
304
|
+
except asyncio.CancelledError:
|
|
305
|
+
logger.info("ZIP download cancelled by client disconnect")
|
|
298
306
|
except Exception:
|
|
299
307
|
logger.exception("Error streaming ZIP")
|
|
300
308
|
raise
|
|
301
309
|
finally:
|
|
310
|
+
pending_put = None
|
|
302
311
|
asyncio.run_coroutine_threadsafe(queue.put(None), loop)
|
|
303
312
|
|
|
304
|
-
# Don't block the event loop: run in a thread
|
|
313
|
+
# Don't block the event loop: run in a thread (use larger zip pool)
|
|
305
314
|
queue = asyncio.Queue(maxsize=1)
|
|
306
315
|
loop = asyncio.get_event_loop()
|
|
307
|
-
thread = loop.run_in_executor(app.ctx.
|
|
316
|
+
thread = loop.run_in_executor(app.ctx.zipexec, worker)
|
|
308
317
|
|
|
309
318
|
# Stream the response
|
|
310
319
|
res = await req.respond(
|
|
311
320
|
content_type="application/zip",
|
|
312
321
|
headers={"cache-control": "no-store"},
|
|
313
322
|
)
|
|
314
|
-
|
|
315
|
-
await
|
|
323
|
+
try:
|
|
324
|
+
while chunk := await queue.get():
|
|
325
|
+
await res.send(chunk)
|
|
326
|
+
finally:
|
|
327
|
+
# Cancel any pending put to unblock and stop the worker
|
|
328
|
+
if pending_put:
|
|
329
|
+
pending_put.cancel()
|
|
316
330
|
|
|
317
331
|
await thread # If it raises, the response will fail download
|
|
@@ -236,39 +236,36 @@ async def verify(request, *, privileged=False):
|
|
|
236
236
|
|
|
237
237
|
For paskia mode (PASKIA_BACKEND_URL set), validates against the SSO backend.
|
|
238
238
|
For built-in mode, checks session-based authentication.
|
|
239
|
-
For public mode (config.public=True),
|
|
240
|
-
|
|
241
|
-
All 401/403 responses include auth.iframe URL for consistent frontend handling
|
|
242
|
-
via the paskia library's showAuthIframe().
|
|
239
|
+
For public mode (config.public=True), skips auth unless privileged is required.
|
|
243
240
|
|
|
244
241
|
Args:
|
|
245
242
|
request: The Sanic request object
|
|
246
|
-
privileged: If True, requires admin privileges
|
|
243
|
+
privileged: If True, requires admin privileges (always enforced even in public mode)
|
|
247
244
|
|
|
248
245
|
Raises:
|
|
249
246
|
Unauthorized: If authentication is required
|
|
250
247
|
Forbidden: If access is denied
|
|
251
248
|
"""
|
|
249
|
+
# Public mode: skip auth unless privileged access is required
|
|
250
|
+
if config.config.public and not privileged:
|
|
251
|
+
return
|
|
252
|
+
|
|
252
253
|
sso = _get_sso()
|
|
253
254
|
if sso.paskia_enabled():
|
|
254
|
-
# SSO validation against auth backend
|
|
255
|
-
# Always check cista:login; privileged flag comes from response perm list
|
|
256
255
|
perm = "cista:admin" if privileged else "cista:login"
|
|
257
256
|
await sso.validate_sso_request(request, perm=perm)
|
|
258
257
|
return
|
|
259
258
|
|
|
260
259
|
user = getattr(request.ctx, "user", None)
|
|
261
260
|
if privileged:
|
|
262
|
-
if user:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
elif config.config.public or user:
|
|
261
|
+
if user and user.privileged:
|
|
262
|
+
return
|
|
263
|
+
raise Forbidden(
|
|
264
|
+
"Access Forbidden: Only for privileged users",
|
|
265
|
+
quiet=True,
|
|
266
|
+
)
|
|
267
|
+
if user:
|
|
270
268
|
return
|
|
271
|
-
# Return iframe URL for paskia library to show login dialog
|
|
272
269
|
raise Unauthorized(
|
|
273
270
|
f"Login required for {request.path}",
|
|
274
271
|
"cookie",
|