cista 1.4.2__py3-none-any.whl → 1.5.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/__main__.py +7 -7
- cista/_version.py +1 -1
- cista/api.py +6 -0
- cista/app.py +7 -32
- cista/frontend-build/assets/{icons-DMD182WZ.js → icons-6j2UZ_HA.js} +1 -1
- cista/frontend-build/assets/index-BHQJhxzY.js +32 -0
- cista/frontend-build/assets/index-De5UYAfC.css +1 -0
- cista/frontend-build/assets/searchWorker-DbTcSX-c.js +1 -0
- cista/frontend-build/index.html +3 -3
- cista/preview.py +2 -2
- cista/protocol.py +28 -0
- cista/serve.py +21 -10
- cista/watching.py +349 -80
- {cista-1.4.2.dist-info → cista-1.5.0.dist-info}/METADATA +3 -3
- cista-1.5.0.dist-info/RECORD +33 -0
- cista/frontend-build/assets/index-C8Gp9T8Z.js +0 -32
- cista/frontend-build/assets/index-zDODUQOB.css +0 -1
- cista/frontend-build/assets/searchWorker-CxfnO8mP.js +0 -1
- cista-1.4.2.dist-info/RECORD +0 -33
- {cista-1.4.2.dist-info → cista-1.5.0.dist-info}/WHEEL +0 -0
- {cista-1.4.2.dist-info → cista-1.5.0.dist-info}/entry_points.txt +0 -0
cista/serve.py
CHANGED
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from fastapi_vue.hostutil import parse_endpoint
|
|
5
6
|
from sanic import Sanic
|
|
6
7
|
|
|
7
8
|
from cista import config, server80
|
|
@@ -44,18 +45,28 @@ def check_cert(certdir, domain):
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def parse_listen(listen):
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
# Domain name (e.g. example.com) -> HTTPS with LetsEncrypt
|
|
49
|
+
if re.fullmatch(r"(\w+(-\w+)*\.)+\w{2,}", listen, re.UNICODE):
|
|
50
|
+
return f"https://{listen}", {"host": listen, "port": 443, "ssl": True}
|
|
51
|
+
|
|
52
|
+
# Use fastapi_vue's parse_endpoint for everything else
|
|
53
|
+
endpoints = parse_endpoint(listen, default_port=8989)
|
|
54
|
+
ep = endpoints[0]
|
|
55
|
+
|
|
56
|
+
if "uds" in ep:
|
|
57
|
+
unix = Path(ep["uds"]).resolve()
|
|
49
58
|
if not unix.parent.exists():
|
|
50
59
|
raise ValueError(
|
|
51
60
|
f"Directory for unix socket does not exist: {unix.parent}/",
|
|
52
61
|
)
|
|
53
62
|
return "http://localhost", {"unix": unix.as_posix()}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
port
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
|
|
64
|
+
host, port = ep["host"], ep["port"]
|
|
65
|
+
# When binding all interfaces, use single_listener=False for Sanic
|
|
66
|
+
if len(endpoints) > 1:
|
|
67
|
+
return f"http://localhost:{port}", {
|
|
68
|
+
"host": host,
|
|
69
|
+
"port": port,
|
|
70
|
+
"single_listener": False,
|
|
71
|
+
}
|
|
72
|
+
return f"http://{host}:{port}", {"host": host, "port": port}
|
cista/watching.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import queue
|
|
2
3
|
import shutil
|
|
3
4
|
import sys
|
|
4
5
|
import threading
|
|
@@ -116,6 +117,28 @@ state = State()
|
|
|
116
117
|
rootpath: Path = None # type: ignore
|
|
117
118
|
quit = threading.Event()
|
|
118
119
|
|
|
120
|
+
# Thread-safe queue for signaling path updates from websockets
|
|
121
|
+
_update_queue: queue.Queue[PurePosixPath] = queue.Queue()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def notify_change(*paths: PurePosixPath | str):
|
|
125
|
+
"""Signal that paths have changed. Called from control/upload websockets."""
|
|
126
|
+
for path in paths:
|
|
127
|
+
if isinstance(path, str):
|
|
128
|
+
path = PurePosixPath(path)
|
|
129
|
+
# Convert absolute paths to relative (strip leading /)
|
|
130
|
+
if path.is_absolute():
|
|
131
|
+
path = (
|
|
132
|
+
PurePosixPath(*path.parts[1:])
|
|
133
|
+
if len(path.parts) > 1
|
|
134
|
+
else PurePosixPath()
|
|
135
|
+
)
|
|
136
|
+
# Skip root paths (empty, '.') to avoid full tree walks
|
|
137
|
+
if not path.parts or path.parts == (".",):
|
|
138
|
+
continue
|
|
139
|
+
_update_queue.put(path)
|
|
140
|
+
|
|
141
|
+
|
|
119
142
|
## Filesystem scanning
|
|
120
143
|
|
|
121
144
|
|
|
@@ -326,127 +349,373 @@ def format_root(root):
|
|
|
326
349
|
|
|
327
350
|
|
|
328
351
|
def broadcast(msg, loop):
|
|
329
|
-
|
|
352
|
+
fut = asyncio.run_coroutine_threadsafe(abroadcast(msg), loop)
|
|
353
|
+
return fut.result()
|
|
330
354
|
|
|
331
355
|
|
|
332
356
|
async def abroadcast(msg):
|
|
357
|
+
client_count = 0
|
|
333
358
|
try:
|
|
334
359
|
for queue in pubsub.values():
|
|
335
360
|
queue.put_nowait(msg)
|
|
361
|
+
client_count += 1
|
|
336
362
|
except Exception:
|
|
337
363
|
# Log because asyncio would silently eat the error
|
|
338
364
|
logger.exception("Broadcast error")
|
|
365
|
+
return client_count
|
|
339
366
|
|
|
340
367
|
|
|
341
368
|
## Watcher thread
|
|
342
369
|
|
|
343
370
|
|
|
344
|
-
|
|
345
|
-
"""
|
|
346
|
-
|
|
371
|
+
class PathIndex:
|
|
372
|
+
"""O(1) path lookup index for the flat FileEntry tree."""
|
|
373
|
+
|
|
374
|
+
def __init__(self, root: list[FileEntry]):
|
|
375
|
+
self.root = root
|
|
376
|
+
self._index: dict[PurePosixPath, tuple[int, int]] = {}
|
|
377
|
+
self._rebuild()
|
|
378
|
+
|
|
379
|
+
def _rebuild(self):
|
|
380
|
+
"""Build path -> (start_idx, count) mapping in single O(n) pass."""
|
|
381
|
+
index: dict[PurePosixPath, tuple[int, int]] = {}
|
|
382
|
+
path_stack: list[tuple[PurePosixPath, int]] = [] # (path, start_idx)
|
|
383
|
+
|
|
384
|
+
for i, entry in enumerate(self.root):
|
|
385
|
+
# Pop completed paths from stack
|
|
386
|
+
while path_stack and entry.level <= len(path_stack[-1][0].parts):
|
|
387
|
+
completed_path, start_idx = path_stack.pop()
|
|
388
|
+
index[completed_path] = (start_idx, i - start_idx)
|
|
389
|
+
|
|
390
|
+
# Build current path
|
|
391
|
+
if entry.level == 0:
|
|
392
|
+
current_path = PurePosixPath()
|
|
393
|
+
else:
|
|
394
|
+
parent = path_stack[-1][0] if path_stack else PurePosixPath()
|
|
395
|
+
current_path = parent / entry.name
|
|
396
|
+
|
|
397
|
+
path_stack.append((current_path, i))
|
|
398
|
+
|
|
399
|
+
# Close remaining paths
|
|
400
|
+
for path, start_idx in path_stack:
|
|
401
|
+
index[path] = (start_idx, len(self.root) - start_idx)
|
|
402
|
+
|
|
403
|
+
self._index = index
|
|
404
|
+
|
|
405
|
+
def get(self, path: PurePosixPath) -> tuple[int | None, list[FileEntry]]:
|
|
406
|
+
"""O(1) lookup: returns (start_idx, entries) or (None, [])."""
|
|
407
|
+
if path not in self._index:
|
|
408
|
+
return None, []
|
|
409
|
+
start, count = self._index[path]
|
|
410
|
+
return start, self.root[start : start + count]
|
|
411
|
+
|
|
412
|
+
def find_insert_pos(self, path: PurePosixPath, isfile: int) -> int:
|
|
413
|
+
"""Find insertion position using index + binary search."""
|
|
414
|
+
if not path.parts:
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
parent = path.parent
|
|
418
|
+
name = path.name
|
|
419
|
+
|
|
420
|
+
# Find parent's range
|
|
421
|
+
if parent == PurePosixPath():
|
|
422
|
+
# Insert at root level - scan root's direct children
|
|
423
|
+
start, count = 0, len(self.root)
|
|
424
|
+
target_level = 1
|
|
425
|
+
elif parent in self._index:
|
|
426
|
+
start, count = self._index[parent]
|
|
427
|
+
start += 1 # Skip parent entry itself
|
|
428
|
+
count -= 1
|
|
429
|
+
target_level = len(parent.parts) + 1
|
|
430
|
+
else:
|
|
431
|
+
# Parent doesn't exist, shouldn't happen
|
|
432
|
+
return len(self.root)
|
|
433
|
+
|
|
434
|
+
# Binary search among direct children at target_level
|
|
435
|
+
# Collect children indices first
|
|
436
|
+
children = []
|
|
437
|
+
i = start
|
|
438
|
+
end = start + count
|
|
439
|
+
while i < end:
|
|
440
|
+
entry = self.root[i]
|
|
441
|
+
if entry.level == target_level:
|
|
442
|
+
children.append(i)
|
|
443
|
+
i += 1
|
|
444
|
+
|
|
445
|
+
if not children:
|
|
446
|
+
return start
|
|
447
|
+
|
|
448
|
+
# Binary search for insertion point
|
|
449
|
+
nsort = sortkey(name)
|
|
450
|
+
lo, hi = 0, len(children)
|
|
451
|
+
while lo < hi:
|
|
452
|
+
mid = (lo + hi) // 2
|
|
453
|
+
idx = children[mid]
|
|
454
|
+
entry = self.root[idx]
|
|
455
|
+
ename = entry.name
|
|
456
|
+
esort = sortkey(ename)
|
|
457
|
+
# Compare: isfile, then sort key, then case-sensitive
|
|
458
|
+
cmp = (
|
|
459
|
+
entry.isfile - isfile
|
|
460
|
+
or (esort > nsort) - (esort < nsort)
|
|
461
|
+
or (ename > name) - (ename < name)
|
|
462
|
+
)
|
|
463
|
+
if cmp < 0:
|
|
464
|
+
lo = mid + 1
|
|
465
|
+
else:
|
|
466
|
+
hi = mid
|
|
467
|
+
|
|
468
|
+
if lo < len(children):
|
|
469
|
+
return children[lo]
|
|
470
|
+
elif children:
|
|
471
|
+
# Insert after last child's subtree
|
|
472
|
+
last_idx = children[-1]
|
|
473
|
+
last_entry = self.root[last_idx]
|
|
474
|
+
if last_entry.isfile:
|
|
475
|
+
return last_idx + 1
|
|
476
|
+
# Find end of last child's subtree
|
|
477
|
+
last_path = parent / last_entry.name
|
|
478
|
+
if last_path in self._index:
|
|
479
|
+
s, c = self._index[last_path]
|
|
480
|
+
return s + c
|
|
481
|
+
return last_idx + 1
|
|
482
|
+
return start
|
|
483
|
+
|
|
484
|
+
def apply_update(
|
|
485
|
+
self, path: PurePosixPath, new_entries: list[FileEntry]
|
|
486
|
+
) -> list[FileEntry]:
|
|
487
|
+
"""Apply an update and return the new root. Rebuilds index."""
|
|
488
|
+
start, old_entries = self.get(path)
|
|
489
|
+
|
|
490
|
+
if old_entries == new_entries:
|
|
491
|
+
return self.root
|
|
492
|
+
|
|
493
|
+
new_root = self.root[:]
|
|
494
|
+
|
|
495
|
+
if start is not None:
|
|
496
|
+
del new_root[start : start + len(old_entries)]
|
|
497
|
+
|
|
498
|
+
if new_entries:
|
|
499
|
+
# Rebuild index on modified list to find insert pos
|
|
500
|
+
self.root = new_root
|
|
501
|
+
self._rebuild()
|
|
502
|
+
insert_pos = self.find_insert_pos(path, new_entries[0].isfile)
|
|
503
|
+
new_root[insert_pos:insert_pos] = new_entries
|
|
504
|
+
|
|
505
|
+
self.root = new_root
|
|
506
|
+
self._rebuild()
|
|
507
|
+
return new_root
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def collapse_paths(paths: set[PurePosixPath]) -> set[PurePosixPath]:
|
|
511
|
+
"""Remove child paths if parent is in set."""
|
|
512
|
+
if not paths:
|
|
513
|
+
return paths
|
|
514
|
+
# Filter out root paths (empty or '.') which would cause full tree walks
|
|
515
|
+
paths = {p for p in paths if p.parts and p.parts != (".",)}
|
|
516
|
+
if not paths:
|
|
517
|
+
return set()
|
|
518
|
+
# Sort by depth (fewest parts first)
|
|
519
|
+
sorted_paths = sorted(paths, key=lambda p: len(p.parts))
|
|
520
|
+
result = set()
|
|
521
|
+
for path in sorted_paths:
|
|
522
|
+
# Check if any ancestor is already in result
|
|
523
|
+
is_child = False
|
|
524
|
+
for i in range(len(path.parts)):
|
|
525
|
+
ancestor = PurePosixPath(*path.parts[:i]) if i > 0 else PurePosixPath()
|
|
526
|
+
if ancestor in result:
|
|
527
|
+
is_child = True
|
|
528
|
+
break
|
|
529
|
+
if not is_child:
|
|
530
|
+
result.add(path)
|
|
531
|
+
return result
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# Debounce settings
|
|
535
|
+
DEBOUNCE_DELAY = 0.01 # Wait 10ms after last event
|
|
536
|
+
DEBOUNCE_MAX = 0.1 # But no more than 100ms total
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def watcher(loop):
|
|
540
|
+
"""Unified watcher thread handling inotify, websocket signals, and periodic scans."""
|
|
541
|
+
use_inotify = sys.platform == "linux"
|
|
542
|
+
inotify_tree = None
|
|
543
|
+
modified_flags = frozenset()
|
|
544
|
+
|
|
545
|
+
if use_inotify:
|
|
546
|
+
import inotify.adapters
|
|
547
|
+
|
|
548
|
+
modified_flags = frozenset(
|
|
549
|
+
(
|
|
550
|
+
"IN_CREATE",
|
|
551
|
+
"IN_DELETE",
|
|
552
|
+
"IN_DELETE_SELF",
|
|
553
|
+
"IN_MODIFY",
|
|
554
|
+
"IN_MOVE_SELF",
|
|
555
|
+
"IN_MOVED_FROM",
|
|
556
|
+
"IN_MOVED_TO",
|
|
557
|
+
)
|
|
558
|
+
)
|
|
347
559
|
|
|
348
|
-
modified_flags = (
|
|
349
|
-
"IN_CREATE",
|
|
350
|
-
"IN_DELETE",
|
|
351
|
-
"IN_DELETE_SELF",
|
|
352
|
-
"IN_MODIFY",
|
|
353
|
-
"IN_MOVE_SELF",
|
|
354
|
-
"IN_MOVED_FROM",
|
|
355
|
-
"IN_MOVED_TO",
|
|
356
|
-
)
|
|
357
560
|
while not quit.is_set():
|
|
358
|
-
|
|
561
|
+
if use_inotify:
|
|
562
|
+
import inotify.adapters
|
|
563
|
+
|
|
564
|
+
inotify_tree = inotify.adapters.InotifyTree(rootpath.as_posix())
|
|
565
|
+
|
|
359
566
|
# Initialize the tree from filesystem
|
|
360
567
|
update_root(loop)
|
|
568
|
+
path_index = PathIndex(state.root[:])
|
|
569
|
+
|
|
361
570
|
trefresh = time.monotonic() + 300.0
|
|
362
571
|
tspace = time.monotonic() + 5.0
|
|
363
|
-
|
|
572
|
+
|
|
573
|
+
# Pending changes: path -> {"ws": count, "inotify": count}
|
|
574
|
+
dirty_paths: dict[PurePosixPath, dict[str, int]] = {}
|
|
575
|
+
first_event_time: float | None = None
|
|
576
|
+
last_event_time: float | None = None
|
|
577
|
+
|
|
578
|
+
def add_dirty(path: PurePosixPath, source: str) -> bool:
|
|
579
|
+
"""Add path to dirty set. Returns True if added, False if redundant."""
|
|
580
|
+
nonlocal first_event_time, last_event_time
|
|
581
|
+
# Check if already covered by an existing dirty path
|
|
582
|
+
for existing in dirty_paths:
|
|
583
|
+
if path == existing or (
|
|
584
|
+
len(path.parts) > len(existing.parts)
|
|
585
|
+
and path.parts[: len(existing.parts)] == existing.parts
|
|
586
|
+
):
|
|
587
|
+
# Count the event even if skipped
|
|
588
|
+
dirty_paths[existing][source] = (
|
|
589
|
+
dirty_paths[existing].get(source, 0) + 1
|
|
590
|
+
)
|
|
591
|
+
return False
|
|
592
|
+
# Remove any paths that would be covered by this new one
|
|
593
|
+
covered = {
|
|
594
|
+
p
|
|
595
|
+
for p in dirty_paths
|
|
596
|
+
if len(p.parts) > len(path.parts)
|
|
597
|
+
and p.parts[: len(path.parts)] == path.parts
|
|
598
|
+
}
|
|
599
|
+
# Aggregate counts from covered paths
|
|
600
|
+
counts: dict[str, int] = {source: 1}
|
|
601
|
+
for p in covered:
|
|
602
|
+
for s, c in dirty_paths[p].items():
|
|
603
|
+
counts[s] = counts.get(s, 0) + c
|
|
604
|
+
del dirty_paths[p]
|
|
605
|
+
dirty_paths[path] = counts
|
|
606
|
+
now = time.monotonic()
|
|
607
|
+
if first_event_time is None:
|
|
608
|
+
first_event_time = now
|
|
609
|
+
last_event_time = now
|
|
610
|
+
return True
|
|
611
|
+
|
|
364
612
|
while not quit.is_set():
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
613
|
+
now = time.monotonic()
|
|
614
|
+
|
|
615
|
+
# Full refresh every 300s
|
|
616
|
+
if now >= trefresh:
|
|
368
617
|
break
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
618
|
+
|
|
619
|
+
# Disk usage update every 5s
|
|
620
|
+
if now >= tspace:
|
|
621
|
+
tspace = now + 5.0
|
|
372
622
|
update_space(loop)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
broadcast(update, loop)
|
|
403
|
-
state.root = rootmod
|
|
404
|
-
except Exception:
|
|
405
|
-
logger.exception(
|
|
406
|
-
"format_update failed; falling back to full rescan"
|
|
407
|
-
)
|
|
408
|
-
# Fallback: full rescan and try diff again; last resort send full root
|
|
623
|
+
|
|
624
|
+
# Check if we should flush pending changes
|
|
625
|
+
should_flush = False
|
|
626
|
+
if dirty_paths:
|
|
627
|
+
time_since_last = now - last_event_time if last_event_time else 0
|
|
628
|
+
time_since_first = now - first_event_time if first_event_time else 0
|
|
629
|
+
if (
|
|
630
|
+
time_since_last >= DEBOUNCE_DELAY
|
|
631
|
+
or time_since_first >= DEBOUNCE_MAX
|
|
632
|
+
):
|
|
633
|
+
should_flush = True
|
|
634
|
+
|
|
635
|
+
if should_flush:
|
|
636
|
+
paths_to_process = dirty_paths.copy()
|
|
637
|
+
dirty_paths.clear()
|
|
638
|
+
first_event_time = None
|
|
639
|
+
last_event_time = None
|
|
640
|
+
|
|
641
|
+
# Collapse paths (remove children if parent present)
|
|
642
|
+
collapsed = collapse_paths(set(paths_to_process.keys()))
|
|
643
|
+
|
|
644
|
+
# Process each collapsed path
|
|
645
|
+
new_root = path_index.root
|
|
646
|
+
for path in collapsed:
|
|
647
|
+
new_entries = walk(path)
|
|
648
|
+
new_root = path_index.apply_update(path, new_entries)
|
|
649
|
+
|
|
650
|
+
# Broadcast if changed
|
|
651
|
+
if new_root != state.root:
|
|
409
652
|
try:
|
|
410
|
-
|
|
653
|
+
update_msg = format_update(state.root, new_root)
|
|
654
|
+
with state.lock:
|
|
655
|
+
broadcast(update_msg, loop)
|
|
656
|
+
state.root = new_root
|
|
657
|
+
except Exception:
|
|
658
|
+
logger.exception("format_update failed; full rescan")
|
|
411
659
|
try:
|
|
412
|
-
|
|
660
|
+
fresh = walk(PurePosixPath())
|
|
661
|
+
path_index = PathIndex(fresh)
|
|
662
|
+
update_msg = format_update(state.root, fresh)
|
|
413
663
|
with state.lock:
|
|
414
|
-
broadcast(
|
|
664
|
+
broadcast(update_msg, loop)
|
|
415
665
|
state.root = fresh
|
|
416
666
|
except Exception:
|
|
417
|
-
logger.exception(
|
|
418
|
-
"Fallback diff failed; sending full root snapshot"
|
|
419
|
-
)
|
|
667
|
+
logger.exception("Fallback failed; sending full root")
|
|
420
668
|
with state.lock:
|
|
421
669
|
broadcast(format_root(fresh), loop)
|
|
422
670
|
state.root = fresh
|
|
423
|
-
except Exception:
|
|
424
|
-
logger.exception(
|
|
425
|
-
"Full rescan failed; dropping this batch of updates"
|
|
426
|
-
)
|
|
427
671
|
|
|
428
|
-
|
|
672
|
+
# Collect events from websocket signals (non-blocking)
|
|
673
|
+
try:
|
|
674
|
+
while True:
|
|
675
|
+
path = _update_queue.get_nowait()
|
|
676
|
+
add_dirty(path, "ws")
|
|
677
|
+
except queue.Empty:
|
|
678
|
+
pass
|
|
679
|
+
|
|
680
|
+
# Collect inotify events if available (short timeout for responsiveness)
|
|
681
|
+
if inotify_tree:
|
|
682
|
+
for event in inotify_tree.event_gen(yield_nones=False, timeout_s=0.05):
|
|
683
|
+
if quit.is_set():
|
|
684
|
+
return
|
|
685
|
+
if not (modified_flags & set(event[1])):
|
|
686
|
+
continue
|
|
687
|
+
|
|
688
|
+
# Extract relative path
|
|
689
|
+
path = PurePosixPath(event[2]) / event[3]
|
|
690
|
+
try:
|
|
691
|
+
rel_path = path.relative_to(rootpath)
|
|
692
|
+
except ValueError:
|
|
693
|
+
continue
|
|
429
694
|
|
|
695
|
+
# Skip dotfiles
|
|
696
|
+
if any(part.startswith(".") for part in rel_path.parts):
|
|
697
|
+
continue
|
|
430
698
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
699
|
+
add_dirty(rel_path, "inotify")
|
|
700
|
+
|
|
701
|
+
# Don't block too long collecting events
|
|
702
|
+
now = time.monotonic()
|
|
703
|
+
if first_event_time and now - first_event_time >= DEBOUNCE_MAX:
|
|
704
|
+
break
|
|
705
|
+
else:
|
|
706
|
+
# No inotify, just sleep briefly for responsiveness
|
|
707
|
+
time.sleep(0.05)
|
|
708
|
+
|
|
709
|
+
if inotify_tree:
|
|
710
|
+
del inotify_tree
|
|
441
711
|
|
|
442
712
|
|
|
443
713
|
def start(app):
|
|
444
714
|
global rootpath
|
|
445
715
|
config.load_config()
|
|
446
716
|
rootpath = config.config.path
|
|
447
|
-
use_inotify = sys.platform == "linux"
|
|
448
717
|
app.ctx.watcher = threading.Thread(
|
|
449
|
-
target=
|
|
718
|
+
target=watcher,
|
|
450
719
|
args=[app.loop],
|
|
451
720
|
# Descriptive name for system monitoring
|
|
452
721
|
name=f"cista-watcher {rootpath}",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cista
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
|
@@ -16,8 +16,8 @@ Requires-Python: >=3.11
|
|
|
16
16
|
Requires-Dist: argon2-cffi>=25.1.0
|
|
17
17
|
Requires-Dist: av>=15.0.0
|
|
18
18
|
Requires-Dist: blake3>=1.0.5
|
|
19
|
-
Requires-Dist: docopt>=0.
|
|
20
|
-
Requires-Dist: fastapi-vue>=0.5.
|
|
19
|
+
Requires-Dist: docopt-ng>=0.9.0
|
|
20
|
+
Requires-Dist: fastapi-vue>=0.5.2
|
|
21
21
|
Requires-Dist: fastapi[standard]>=0.128.0
|
|
22
22
|
Requires-Dist: html5tagger>=1.3.0
|
|
23
23
|
Requires-Dist: httpx>=0.28.0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
cista/__init__.py,sha256=4IT57gQUTOV6q9zFGj9i6E9LmiRz3dIfHK3q4qCeOIc,66
|
|
2
|
+
cista/__main__.py,sha256=_OVF8Js2cOLYiEiGFzU-2tLKxNB7je2sJPtPBzxRAro,6926
|
|
3
|
+
cista/_version.py,sha256=7QEFslgnfsEfWLaZBQ4XRIkrvnxd18xbV0t8PvA28po,77
|
|
4
|
+
cista/api.py,sha256=FAZG1yj3dux5WZ9FyT2DRtDb5oYRVxXSnGuZl18HSWI,5827
|
|
5
|
+
cista/app.py,sha256=5iownX81gZIDjrqurGybjOGAiDE1IHr4R3SGrRkViqY,10205
|
|
6
|
+
cista/auth.py,sha256=-xYYAYhNIivgkchfW0P7-fuJT1fodPpg-c0niVjgf4g,14339
|
|
7
|
+
cista/config.py,sha256=noKbZjpcEq5WG55WEkuKKudBzb5InoGEDtSWeUdVrJM,6183
|
|
8
|
+
cista/droppy.py,sha256=04a7TyZjmgmaLifGyH3WMAbwCAGBGPEl9oFYc423dQA,1304
|
|
9
|
+
cista/fileio.py,sha256=A8OBsv_jN4IgHaZCr8s9t6uqASu3r6qxPCILPKVxXQ0,2939
|
|
10
|
+
cista/preview.py,sha256=VA0zejs997wp6-UZcKt8gzJRu0ACog0UNhHrdcx6od8,9541
|
|
11
|
+
cista/protocol.py,sha256=9nWM-TgvExtR8fpTyUlF0x7-bVZADFEJNlFaPdkPk7U,4321
|
|
12
|
+
cista/serve.py,sha256=XssVa6IEsmrkR83d79V_ZIhnbvqIBkVMv7B833Co1y8,2284
|
|
13
|
+
cista/server80.py,sha256=opjtARyheVXQfJ5wBOMyzGDAtpMHnOpKNoZ1EsIytnI,780
|
|
14
|
+
cista/session.py,sha256=zc-A0iPaepAOLIoO5PTAHy9qn2NZb2zQir3Rm4ZmaK0,1090
|
|
15
|
+
cista/sso.py,sha256=z3ELX7Griv6W_OQuZA7y-u0rjk9ljHCUjrCEV-zEYE0,10410
|
|
16
|
+
cista/watching.py,sha256=HjvV3d4QUVPjsH_86fyewqRPgheczBQXUxmyq4cmqW0,23478
|
|
17
|
+
cista/frontend-build/index.html,sha256=BWYc8PA_YSfLUB3xe1DaIWiI-5aKxL-24fP-mddtroE,725
|
|
18
|
+
cista/frontend-build/robots.txt,sha256=Mx6pCQ2wyfb1l72YQP1bFxgw9uCzuhyyTfqR8Mla7cE,26
|
|
19
|
+
cista/frontend-build/assets/icons-6j2UZ_HA.js,sha256=J-0NXwc1tsV85JMGEE2GCGPFBt8dMX-g1nlYQKIBAEQ,102556
|
|
20
|
+
cista/frontend-build/assets/index-BHQJhxzY.js,sha256=mhPbB7QIYZx-PIHD49yv22ASnodrLp-Xp1LlLbLuaXE,128408
|
|
21
|
+
cista/frontend-build/assets/index-De5UYAfC.css,sha256=NNvkrLY57y7n0WPVFBD_vvqZUenlM9gNVxChjjhbgGw,33698
|
|
22
|
+
cista/frontend-build/assets/logo-ctv8tVwU.svg,sha256=l9HX62cauXDeo8zXU5-xNWhLmm-y0-xsXp_1xzHQX7s,258
|
|
23
|
+
cista/frontend-build/assets/searchWorker-DbTcSX-c.js,sha256=aKcLgV0IFvq6yarxebeUVash4rQlNuYiKcR4MyB-lp0,1903
|
|
24
|
+
cista/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
cista/util/apphelpers.py,sha256=sQ3oQGa3XlgPIIKeYDgk6wzmtml-O3GZH7g3lsePUXY,2603
|
|
26
|
+
cista/util/asynclink.py,sha256=3jl57o6f2AQGYDKV4hJFHSJUuJaKy3-dBctxNut8_9E,3122
|
|
27
|
+
cista/util/filename.py,sha256=_0qO7mPudritrSnlHQTAh03KiunYrt3I9vat5ZVXXEg,581
|
|
28
|
+
cista/util/lrucache.py,sha256=Gpzx7FNtN1Xkdlgipa1oGebnWolMJVZfruW3KKlX7Ms,2070
|
|
29
|
+
cista/util/pwgen.py,sha256=yapMYCAzOGqIm51LsQoBbkXAQQufAhbX0hOI1fPQ8nQ,6276
|
|
30
|
+
cista-1.5.0.dist-info/METADATA,sha256=DWoS7PlMPVxGbd0QV_X0IYKMj0dtv6Vsw0JnqtwzN-Y,8268
|
|
31
|
+
cista-1.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
cista-1.5.0.dist-info/entry_points.txt,sha256=_XK4tjdkpyX8yUrVMHboZ5EJ9Q1bNqhuIkKoA06WiKw,46
|
|
33
|
+
cista-1.5.0.dist-info/RECORD,,
|