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.
Files changed (35) hide show
  1. {cista-1.4.0 → cista-1.4.2}/PKG-INFO +34 -1
  2. {cista-1.4.0 → cista-1.4.2}/README.md +33 -0
  3. {cista-1.4.0 → cista-1.4.2}/cista/__main__.py +36 -9
  4. {cista-1.4.0 → cista-1.4.2}/cista/_version.py +1 -1
  5. {cista-1.4.0 → cista-1.4.2}/cista/api.py +13 -8
  6. {cista-1.4.0 → cista-1.4.2}/cista/app.py +25 -11
  7. {cista-1.4.0 → cista-1.4.2}/cista/auth.py +13 -16
  8. cista-1.4.2/cista/frontend-build/assets/index-C8Gp9T8Z.js +32 -0
  9. cista-1.4.2/cista/frontend-build/assets/index-zDODUQOB.css +1 -0
  10. cista-1.4.2/cista/frontend-build/assets/searchWorker-CxfnO8mP.js +1 -0
  11. {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/index.html +2 -2
  12. cista-1.4.0/cista/frontend-build/assets/index-D4WrmH4f.js +0 -32
  13. cista-1.4.0/cista/frontend-build/assets/index-DQ2iYrs-.css +0 -1
  14. {cista-1.4.0 → cista-1.4.2}/.gitignore +0 -0
  15. {cista-1.4.0 → cista-1.4.2}/cista/__init__.py +0 -0
  16. {cista-1.4.0 → cista-1.4.2}/cista/config.py +0 -0
  17. {cista-1.4.0 → cista-1.4.2}/cista/droppy.py +0 -0
  18. {cista-1.4.0 → cista-1.4.2}/cista/fileio.py +0 -0
  19. {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/assets/icons-DMD182WZ.js +0 -0
  20. {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/assets/logo-ctv8tVwU.svg +0 -0
  21. {cista-1.4.0 → cista-1.4.2}/cista/frontend-build/robots.txt +0 -0
  22. {cista-1.4.0 → cista-1.4.2}/cista/preview.py +0 -0
  23. {cista-1.4.0 → cista-1.4.2}/cista/protocol.py +0 -0
  24. {cista-1.4.0 → cista-1.4.2}/cista/serve.py +0 -0
  25. {cista-1.4.0 → cista-1.4.2}/cista/server80.py +0 -0
  26. {cista-1.4.0 → cista-1.4.2}/cista/session.py +0 -0
  27. {cista-1.4.0 → cista-1.4.2}/cista/sso.py +0 -0
  28. {cista-1.4.0 → cista-1.4.2}/cista/util/__init__.py +0 -0
  29. {cista-1.4.0 → cista-1.4.2}/cista/util/apphelpers.py +0 -0
  30. {cista-1.4.0 → cista-1.4.2}/cista/util/asynclink.py +0 -0
  31. {cista-1.4.0 → cista-1.4.2}/cista/util/filename.py +0 -0
  32. {cista-1.4.0 → cista-1.4.2}/cista/util/lrucache.py +0 -0
  33. {cista-1.4.0 → cista-1.4.2}/cista/util/pwgen.py +0 -0
  34. {cista-1.4.0 → cista-1.4.2}/cista/watching.py +0 -0
  35. {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.0
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
- else:
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
- unix = None
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
- if dev:
133
- extra += " (dev mode)"
134
- sys.stderr.write(f"Serving {config.config.path} at {url}{extra}\n")
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.0'
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 sso_user := getattr(req.ctx, "sso_user", None):
99
- # SSO auth (paskia mode): extract from validation response
100
- ctx = sso_user.get("ctx", {})
101
- perms = ctx.get("permissions", [])
102
- user_info = {
103
- "username": ctx.get("user", {}).get("display_name", ""),
104
- "privileged": "cista:admin" in perms,
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
- workers = max(2, min(8, cpu_count()))
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=workers, thread_name_prefix="cista-ioworker"
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
- if config.config.authentication == "paskia":
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).result()
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.threadexec, worker)
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
- while chunk := await queue.get():
315
- await res.send(chunk)
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), allows all requests.
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
- if user.privileged:
264
- return
265
- raise Forbidden(
266
- "Access Forbidden: Only for privileged users",
267
- quiet=True,
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",