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/watching.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import shutil
|
|
3
|
-
import stat
|
|
4
3
|
import sys
|
|
5
4
|
import threading
|
|
6
5
|
import time
|
|
6
|
+
from contextlib import suppress
|
|
7
7
|
from os import stat_result
|
|
8
8
|
from pathlib import Path, PurePosixPath
|
|
9
|
+
from stat import S_ISDIR, S_ISREG
|
|
9
10
|
|
|
10
11
|
import msgspec
|
|
11
12
|
from natsort import humansorted, natsort_keygen, ns
|
|
12
|
-
from sanic.log import
|
|
13
|
+
from sanic.log import logger
|
|
13
14
|
|
|
14
15
|
from cista import config
|
|
15
16
|
from cista.fileio import fuid
|
|
@@ -23,7 +24,7 @@ class State:
|
|
|
23
24
|
def __init__(self):
|
|
24
25
|
self.lock = threading.RLock()
|
|
25
26
|
self._space = Space(0, 0, 0, 0)
|
|
26
|
-
self.
|
|
27
|
+
self.root: list[FileEntry] = []
|
|
27
28
|
|
|
28
29
|
@property
|
|
29
30
|
def space(self):
|
|
@@ -35,264 +36,236 @@ class State:
|
|
|
35
36
|
with self.lock:
|
|
36
37
|
self._space = space
|
|
37
38
|
|
|
38
|
-
@property
|
|
39
|
-
def root(self) -> list[FileEntry]:
|
|
40
|
-
with self.lock:
|
|
41
|
-
return self._listing[:]
|
|
42
|
-
|
|
43
|
-
@root.setter
|
|
44
|
-
def root(self, listing: list[FileEntry]):
|
|
45
|
-
with self.lock:
|
|
46
|
-
self._listing = listing
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
level
|
|
52
|
-
|
|
40
|
+
def treeiter(rootmod):
|
|
41
|
+
relpath = PurePosixPath()
|
|
42
|
+
for i, entry in enumerate(rootmod):
|
|
43
|
+
if entry.level > 0:
|
|
44
|
+
relpath = PurePosixPath(*relpath.parts[: entry.level - 1]) / entry.name
|
|
45
|
+
yield i, relpath, entry
|
|
53
46
|
|
|
54
|
-
# Special case for root
|
|
55
|
-
if not relpath.parts:
|
|
56
|
-
return slice(begin, end)
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
def treeget(rootmod: list[FileEntry], path: PurePosixPath):
|
|
49
|
+
begin = None
|
|
50
|
+
ret = []
|
|
51
|
+
for i, relpath, entry in treeiter(rootmod):
|
|
52
|
+
if begin is None:
|
|
53
|
+
if relpath == path:
|
|
54
|
+
begin = i
|
|
55
|
+
ret.append(entry)
|
|
56
|
+
continue
|
|
57
|
+
if entry.level <= len(path.parts):
|
|
58
|
+
break
|
|
59
|
+
ret.append(entry)
|
|
60
|
+
return begin, ret
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def treeinspos(rootmod: list[FileEntry], relpath: PurePosixPath, relfile: int):
|
|
64
|
+
# Find the first entry greater than the new one
|
|
65
|
+
# precondition: the new entry doesn't exist
|
|
66
|
+
isfile = 0
|
|
67
|
+
level = 0
|
|
68
|
+
i = 0
|
|
69
|
+
for i, rel, entry in treeiter(rootmod):
|
|
70
|
+
if entry.level > level:
|
|
71
|
+
# We haven't found item at level, skip subdirectories
|
|
72
|
+
continue
|
|
73
|
+
if entry.level < level:
|
|
74
|
+
# We have passed the level, so the new item is the first
|
|
75
|
+
return i
|
|
76
|
+
if level == 0:
|
|
77
|
+
# root
|
|
60
78
|
level += 1
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Found the starting point, now find the end of the slice
|
|
87
|
-
for end in range(begin + 1, len(self._listing) + 1):
|
|
88
|
-
if end == len(self._listing) or self._listing[end].level <= level:
|
|
89
|
-
break
|
|
90
|
-
return slice(begin, end)
|
|
91
|
-
|
|
92
|
-
def __getitem__(self, index: PurePosixPath | tuple[PurePosixPath, int]):
|
|
93
|
-
with self.lock:
|
|
94
|
-
return self._listing[self._slice(index)]
|
|
95
|
-
|
|
96
|
-
def __setitem__(
|
|
97
|
-
self, index: tuple[PurePosixPath, int], value: list[FileEntry]
|
|
98
|
-
) -> None:
|
|
99
|
-
rel, isfile = index
|
|
100
|
-
with self.lock:
|
|
101
|
-
if rel.parts:
|
|
102
|
-
parent = self._slice(rel.parent)
|
|
103
|
-
if parent.start == parent.stop:
|
|
104
|
-
raise ValueError(
|
|
105
|
-
f"Parent folder {rel.as_posix()} is missing for {rel.name}"
|
|
106
|
-
)
|
|
107
|
-
self._listing[self._slice(index)] = value
|
|
108
|
-
|
|
109
|
-
def __delitem__(self, relpath: PurePosixPath):
|
|
110
|
-
with self.lock:
|
|
111
|
-
del self._listing[self._slice(relpath)]
|
|
79
|
+
continue
|
|
80
|
+
ename = rel.parts[level - 1]
|
|
81
|
+
name = relpath.parts[level - 1]
|
|
82
|
+
esort = sortkey(ename)
|
|
83
|
+
nsort = sortkey(name)
|
|
84
|
+
# Non-leaf are always folders, only use relfile at leaf
|
|
85
|
+
isfile = relfile if len(relpath.parts) == level else 0
|
|
86
|
+
# First compare by isfile, then by sorting order and if that too matches then case sensitive
|
|
87
|
+
cmp = (
|
|
88
|
+
entry.isfile - isfile
|
|
89
|
+
or (esort > nsort) - (esort < nsort)
|
|
90
|
+
or (ename > name) - (ename < name)
|
|
91
|
+
)
|
|
92
|
+
if cmp > 0:
|
|
93
|
+
return i
|
|
94
|
+
if cmp < 0:
|
|
95
|
+
continue
|
|
96
|
+
level += 1
|
|
97
|
+
if level > len(relpath.parts):
|
|
98
|
+
print("ERROR: insertpos", relpath, i, entry.name, entry.level, level)
|
|
99
|
+
break
|
|
100
|
+
else:
|
|
101
|
+
i += 1
|
|
102
|
+
return i
|
|
112
103
|
|
|
113
104
|
|
|
114
105
|
state = State()
|
|
115
106
|
rootpath: Path = None # type: ignore
|
|
116
|
-
quit =
|
|
117
|
-
modified_flags = (
|
|
118
|
-
"IN_CREATE",
|
|
119
|
-
"IN_DELETE",
|
|
120
|
-
"IN_DELETE_SELF",
|
|
121
|
-
"IN_MODIFY",
|
|
122
|
-
"IN_MOVE_SELF",
|
|
123
|
-
"IN_MOVED_FROM",
|
|
124
|
-
"IN_MOVED_TO",
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def watcher_thread(loop):
|
|
129
|
-
global rootpath
|
|
130
|
-
import inotify.adapters
|
|
131
|
-
|
|
132
|
-
while not quit:
|
|
133
|
-
rootpath = config.config.path
|
|
134
|
-
i = inotify.adapters.InotifyTree(rootpath.as_posix())
|
|
135
|
-
# Initialize the tree from filesystem
|
|
136
|
-
new = walk()
|
|
137
|
-
with state.lock:
|
|
138
|
-
old = state.root
|
|
139
|
-
if old != new:
|
|
140
|
-
state.root = new
|
|
141
|
-
broadcast(format_update(old, new), loop)
|
|
142
|
-
|
|
143
|
-
# The watching is not entirely reliable, so do a full refresh every 30 seconds
|
|
144
|
-
refreshdl = time.monotonic() + 30.0
|
|
145
|
-
|
|
146
|
-
for event in i.event_gen():
|
|
147
|
-
if quit:
|
|
148
|
-
return
|
|
149
|
-
# Disk usage update
|
|
150
|
-
du = shutil.disk_usage(rootpath)
|
|
151
|
-
space = Space(*du, storage=state.root[0].size)
|
|
152
|
-
if space != state.space:
|
|
153
|
-
state.space = space
|
|
154
|
-
broadcast(format_space(space), loop)
|
|
155
|
-
break
|
|
156
|
-
# Do a full refresh?
|
|
157
|
-
if time.monotonic() > refreshdl:
|
|
158
|
-
break
|
|
159
|
-
if event is None:
|
|
160
|
-
continue
|
|
161
|
-
_, flags, path, filename = event
|
|
162
|
-
if not any(f in modified_flags for f in flags):
|
|
163
|
-
continue
|
|
164
|
-
# Update modified path
|
|
165
|
-
path = PurePosixPath(path) / filename
|
|
166
|
-
try:
|
|
167
|
-
update(path.relative_to(rootpath), loop)
|
|
168
|
-
except Exception as e:
|
|
169
|
-
print("Watching error", e, path, rootpath)
|
|
170
|
-
raise
|
|
171
|
-
i = None # Free the inotify object
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def watcher_thread_poll(loop):
|
|
175
|
-
global rootpath
|
|
107
|
+
quit = threading.Event()
|
|
176
108
|
|
|
177
|
-
|
|
178
|
-
rootpath = config.config.path
|
|
179
|
-
new = walk()
|
|
180
|
-
with state.lock:
|
|
181
|
-
old = state.root
|
|
182
|
-
if old != new:
|
|
183
|
-
state.root = new
|
|
184
|
-
broadcast(format_update(old, new), loop)
|
|
109
|
+
## Filesystem scanning
|
|
185
110
|
|
|
186
|
-
# Disk usage update
|
|
187
|
-
du = shutil.disk_usage(rootpath)
|
|
188
|
-
space = Space(*du, storage=state.root[0].size)
|
|
189
|
-
if space != state.space:
|
|
190
|
-
state.space = space
|
|
191
|
-
broadcast(format_space(space), loop)
|
|
192
111
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def walk(rel=PurePosixPath()) -> list[FileEntry]: # noqa: B008
|
|
197
|
-
path = rootpath / rel
|
|
198
|
-
try:
|
|
199
|
-
st = path.stat()
|
|
200
|
-
except OSError:
|
|
201
|
-
return []
|
|
202
|
-
return _walk(rel, int(not stat.S_ISDIR(st.st_mode)), st)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _walk(rel: PurePosixPath, isfile: int, st: stat_result) -> list[FileEntry]:
|
|
206
|
-
entry = FileEntry(
|
|
207
|
-
level=len(rel.parts),
|
|
208
|
-
name=rel.name,
|
|
209
|
-
key=fuid(st),
|
|
210
|
-
mtime=int(st.st_mtime),
|
|
211
|
-
size=st.st_size if isfile else 0,
|
|
212
|
-
isfile=isfile,
|
|
213
|
-
)
|
|
214
|
-
if isfile:
|
|
215
|
-
return [entry]
|
|
216
|
-
ret = [entry]
|
|
112
|
+
def walk(rel: PurePosixPath, stat: stat_result | None = None) -> list[FileEntry]:
|
|
217
113
|
path = rootpath / rel
|
|
114
|
+
ret = []
|
|
218
115
|
try:
|
|
116
|
+
st = stat or path.stat()
|
|
117
|
+
isfile = int(not S_ISDIR(st.st_mode))
|
|
118
|
+
entry = FileEntry(
|
|
119
|
+
level=len(rel.parts),
|
|
120
|
+
name=rel.name,
|
|
121
|
+
key=fuid(st),
|
|
122
|
+
mtime=int(st.st_mtime),
|
|
123
|
+
size=st.st_size if isfile else 0,
|
|
124
|
+
isfile=isfile,
|
|
125
|
+
)
|
|
126
|
+
if isfile:
|
|
127
|
+
return [entry]
|
|
128
|
+
# Walk all entries of the directory
|
|
129
|
+
ret: list[FileEntry] = [...] # type: ignore
|
|
219
130
|
li = []
|
|
220
131
|
for f in path.iterdir():
|
|
221
|
-
if quit:
|
|
132
|
+
if quit.is_set():
|
|
222
133
|
raise SystemExit("quit")
|
|
223
134
|
if f.name.startswith("."):
|
|
224
135
|
continue # No dotfiles
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
136
|
+
with suppress(FileNotFoundError):
|
|
137
|
+
s = f.lstat()
|
|
138
|
+
isfile = S_ISREG(s.st_mode)
|
|
139
|
+
isdir = S_ISDIR(s.st_mode)
|
|
140
|
+
if not isfile and not isdir:
|
|
141
|
+
continue
|
|
142
|
+
li.append((int(isfile), f.name, s))
|
|
143
|
+
# Build the tree as a list of FileEntries
|
|
144
|
+
for [_, name, s] in humansorted(li):
|
|
145
|
+
sub = walk(rel / name, stat=s)
|
|
146
|
+
child = sub[0]
|
|
147
|
+
entry = FileEntry(
|
|
148
|
+
level=entry.level,
|
|
149
|
+
name=entry.name,
|
|
150
|
+
key=entry.key,
|
|
151
|
+
size=entry.size + child.size,
|
|
152
|
+
mtime=max(entry.mtime, child.mtime),
|
|
153
|
+
isfile=entry.isfile,
|
|
154
|
+
)
|
|
155
|
+
ret.extend(sub)
|
|
235
156
|
except FileNotFoundError:
|
|
236
157
|
pass # Things may be rapidly in motion
|
|
237
158
|
except OSError as e:
|
|
238
|
-
|
|
159
|
+
if e.errno == 13: # Permission denied
|
|
160
|
+
pass
|
|
161
|
+
logger.error(f"Watching {path=}: {e!r}")
|
|
162
|
+
if ret:
|
|
163
|
+
ret[0] = entry
|
|
239
164
|
return ret
|
|
240
165
|
|
|
241
166
|
|
|
242
|
-
def
|
|
243
|
-
"""
|
|
244
|
-
|
|
245
|
-
|
|
167
|
+
def update_root(loop):
|
|
168
|
+
"""Full filesystem scan"""
|
|
169
|
+
old = state.root
|
|
170
|
+
new = walk(PurePosixPath())
|
|
171
|
+
if old != new:
|
|
172
|
+
update = format_update(old, new)
|
|
173
|
+
with state.lock:
|
|
174
|
+
broadcast(update, loop)
|
|
175
|
+
state.root = new
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def update_path(rootmod: list[FileEntry], relpath: PurePosixPath, loop):
|
|
179
|
+
"""Called on FS updates, check the filesystem and broadcast any changes."""
|
|
246
180
|
new = walk(relpath)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
181
|
+
obegin, old = treeget(rootmod, relpath)
|
|
182
|
+
if old == new:
|
|
183
|
+
logger.debug(
|
|
184
|
+
f"Watch: Event without changes needed {relpath}"
|
|
185
|
+
if old
|
|
186
|
+
else f"Watch: Event with old and new missing: {relpath}"
|
|
187
|
+
)
|
|
188
|
+
return
|
|
189
|
+
if obegin is not None:
|
|
190
|
+
del rootmod[obegin : obegin + len(old)]
|
|
191
|
+
if new:
|
|
192
|
+
logger.debug(f"Watch: Update {relpath}" if old else f"Watch: Created {relpath}")
|
|
193
|
+
i = treeinspos(rootmod, relpath, new[0].isfile)
|
|
194
|
+
rootmod[i:i] = new
|
|
195
|
+
else:
|
|
196
|
+
logger.debug(f"Watch: Removed {relpath}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def update_space(loop):
|
|
200
|
+
"""Called periodically to update the disk usage."""
|
|
201
|
+
du = shutil.disk_usage(rootpath)
|
|
202
|
+
space = Space(*du, storage=state.root[0].size)
|
|
203
|
+
# Update only on difference above 1 MB
|
|
204
|
+
tol = 10**6
|
|
205
|
+
old = msgspec.structs.astuple(state.space)
|
|
206
|
+
new = msgspec.structs.astuple(space)
|
|
207
|
+
if any(abs(o - n) > tol for o, n in zip(old, new, strict=True)):
|
|
208
|
+
state.space = space
|
|
209
|
+
broadcast(format_space(space), loop)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## Messaging
|
|
257
213
|
|
|
258
214
|
|
|
259
215
|
def format_update(old, new):
|
|
260
216
|
# Make keep/del/insert diff until one of the lists ends
|
|
261
217
|
oidx, nidx = 0, 0
|
|
218
|
+
oremain, nremain = set(old), set(new)
|
|
262
219
|
update = []
|
|
263
220
|
keep_count = 0
|
|
264
221
|
while oidx < len(old) and nidx < len(new):
|
|
222
|
+
modified = False
|
|
223
|
+
# Matching entries are kept
|
|
265
224
|
if old[oidx] == new[nidx]:
|
|
225
|
+
entry = old[oidx]
|
|
226
|
+
oremain.remove(entry)
|
|
227
|
+
nremain.remove(entry)
|
|
266
228
|
keep_count += 1
|
|
267
229
|
oidx += 1
|
|
268
230
|
nidx += 1
|
|
269
231
|
continue
|
|
270
232
|
if keep_count > 0:
|
|
233
|
+
modified = True
|
|
271
234
|
update.append(UpdKeep(keep_count))
|
|
272
235
|
keep_count = 0
|
|
273
236
|
|
|
237
|
+
# Items only in old are deleted
|
|
274
238
|
del_count = 0
|
|
275
|
-
|
|
276
|
-
|
|
239
|
+
while oidx < len(old) and old[oidx] not in nremain:
|
|
240
|
+
oremain.remove(old[oidx])
|
|
277
241
|
del_count += 1
|
|
278
242
|
oidx += 1
|
|
279
243
|
if del_count:
|
|
280
244
|
update.append(UpdDel(del_count))
|
|
281
245
|
continue
|
|
282
246
|
|
|
247
|
+
# Items only in new are inserted
|
|
283
248
|
insert_items = []
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
249
|
+
while nidx < len(new) and new[nidx] not in oremain:
|
|
250
|
+
entry = new[nidx]
|
|
251
|
+
nremain.remove(entry)
|
|
252
|
+
insert_items.append(entry)
|
|
287
253
|
nidx += 1
|
|
288
|
-
|
|
254
|
+
if insert_items:
|
|
255
|
+
modified = True
|
|
256
|
+
update.append(UpdIns(insert_items))
|
|
257
|
+
|
|
258
|
+
if not modified:
|
|
259
|
+
raise Exception(
|
|
260
|
+
f"Infinite loop in diff {nidx=} {oidx=} {len(old)=} {len(new)=}"
|
|
261
|
+
)
|
|
289
262
|
|
|
290
263
|
# Diff any remaining
|
|
291
264
|
if keep_count > 0:
|
|
292
265
|
update.append(UpdKeep(keep_count))
|
|
293
|
-
if
|
|
294
|
-
update.append(UpdDel(len(
|
|
295
|
-
elif
|
|
266
|
+
if oremain:
|
|
267
|
+
update.append(UpdDel(len(oremain)))
|
|
268
|
+
elif nremain:
|
|
296
269
|
update.append(UpdIns(new[nidx:]))
|
|
297
270
|
|
|
298
271
|
return msgspec.json.encode({"update": update}).decode()
|
|
@@ -316,20 +289,108 @@ async def abroadcast(msg):
|
|
|
316
289
|
queue.put_nowait(msg)
|
|
317
290
|
except Exception:
|
|
318
291
|
# Log because asyncio would silently eat the error
|
|
319
|
-
|
|
292
|
+
logger.exception("Broadcast error")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
## Watcher thread
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def watcher_inotify(loop):
|
|
299
|
+
"""Inotify watcher thread (Linux only)"""
|
|
300
|
+
import inotify.adapters
|
|
301
|
+
|
|
302
|
+
modified_flags = (
|
|
303
|
+
"IN_CREATE",
|
|
304
|
+
"IN_DELETE",
|
|
305
|
+
"IN_DELETE_SELF",
|
|
306
|
+
"IN_MODIFY",
|
|
307
|
+
"IN_MOVE_SELF",
|
|
308
|
+
"IN_MOVED_FROM",
|
|
309
|
+
"IN_MOVED_TO",
|
|
310
|
+
)
|
|
311
|
+
while not quit.is_set():
|
|
312
|
+
i = inotify.adapters.InotifyTree(rootpath.as_posix())
|
|
313
|
+
# Initialize the tree from filesystem
|
|
314
|
+
t0 = time.perf_counter()
|
|
315
|
+
update_root(loop)
|
|
316
|
+
t1 = time.perf_counter()
|
|
317
|
+
logger.debug(f"Root update took {t1 - t0:.1f}s")
|
|
318
|
+
trefresh = time.monotonic() + 300.0
|
|
319
|
+
tspace = time.monotonic() + 5.0
|
|
320
|
+
# Watch for changes (frequent wakeups needed for quiting)
|
|
321
|
+
while not quit.is_set():
|
|
322
|
+
t = time.monotonic()
|
|
323
|
+
# The watching is not entirely reliable, so do a full refresh every 30 seconds
|
|
324
|
+
if t >= trefresh:
|
|
325
|
+
break
|
|
326
|
+
# Disk usage update
|
|
327
|
+
if t >= tspace:
|
|
328
|
+
tspace = time.monotonic() + 5.0
|
|
329
|
+
update_space(loop)
|
|
330
|
+
# Inotify events, update the tree
|
|
331
|
+
dirty = False
|
|
332
|
+
rootmod = state.root[:]
|
|
333
|
+
for event in i.event_gen(yield_nones=False, timeout_s=0.1):
|
|
334
|
+
assert event
|
|
335
|
+
if quit.is_set():
|
|
336
|
+
return
|
|
337
|
+
interesting = any(f in modified_flags for f in event[1])
|
|
338
|
+
if event[2] == rootpath.as_posix() and event[3] == "zzz":
|
|
339
|
+
logger.debug(f"Watch: {interesting=} {event=}")
|
|
340
|
+
if interesting:
|
|
341
|
+
# Update modified path
|
|
342
|
+
t0 = time.perf_counter()
|
|
343
|
+
path = PurePosixPath(event[2]) / event[3]
|
|
344
|
+
update_path(rootmod, path.relative_to(rootpath), loop)
|
|
345
|
+
t1 = time.perf_counter()
|
|
346
|
+
logger.debug(f"Watch: Update {event[3]} took {t1 - t0:.1f}s")
|
|
347
|
+
if not dirty:
|
|
348
|
+
t = time.monotonic()
|
|
349
|
+
dirty = True
|
|
350
|
+
# Wait a maximum of 0.5s to push the updates
|
|
351
|
+
if dirty and time.monotonic() >= t + 0.5:
|
|
352
|
+
break
|
|
353
|
+
if dirty and state.root != rootmod:
|
|
354
|
+
t0 = time.perf_counter()
|
|
355
|
+
update = format_update(state.root, rootmod)
|
|
356
|
+
t1 = time.perf_counter()
|
|
357
|
+
with state.lock:
|
|
358
|
+
broadcast(update, loop)
|
|
359
|
+
state.root = rootmod
|
|
360
|
+
t2 = time.perf_counter()
|
|
361
|
+
logger.debug(
|
|
362
|
+
f"Format update took {t1 - t0:.1f}s, broadcast {t2 - t1:.1f}s"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
del i # Free the inotify object
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def watcher_poll(loop):
|
|
369
|
+
"""Polling version of the watcher thread."""
|
|
370
|
+
while not quit.is_set():
|
|
371
|
+
t0 = time.perf_counter()
|
|
372
|
+
update_root(loop)
|
|
373
|
+
update_space(loop)
|
|
374
|
+
dur = time.perf_counter() - t0
|
|
375
|
+
if dur > 1.0:
|
|
376
|
+
logger.debug(f"Reading the full file list took {dur:.1f}s")
|
|
377
|
+
quit.wait(0.1 + 8 * dur)
|
|
320
378
|
|
|
321
379
|
|
|
322
380
|
async def start(app, loop):
|
|
381
|
+
global rootpath
|
|
323
382
|
config.load_config()
|
|
383
|
+
rootpath = config.config.path
|
|
324
384
|
use_inotify = sys.platform == "linux"
|
|
325
385
|
app.ctx.watcher = threading.Thread(
|
|
326
|
-
target=
|
|
386
|
+
target=watcher_inotify if use_inotify else watcher_poll,
|
|
327
387
|
args=[loop],
|
|
388
|
+
# Descriptive name for system monitoring
|
|
389
|
+
name=f"cista-watcher {rootpath}",
|
|
328
390
|
)
|
|
329
391
|
app.ctx.watcher.start()
|
|
330
392
|
|
|
331
393
|
|
|
332
394
|
async def stop(app, loop):
|
|
333
|
-
|
|
334
|
-
quit = True
|
|
395
|
+
quit.set()
|
|
335
396
|
app.ctx.watcher.join()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as t,a as o}from"./index-
|
|
1
|
+
import{o as e,c as t,a as o}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 28 28"},c=o("path",{d:"M19.2 2.6H6.1V29h19.8V9.3l-6.7-6.7zM18.5 16v7.1h-5.3V16H8.7l7.1-7.1L23 16h-4.5z"},null,-1),n=[c];function a(r,d){return e(),t("svg",s,[...n])}const h={render:a};export{h as default,a as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as o,a as t}from"./index-
|
|
1
|
+
import{o as e,c as o,a as t}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"},c=t("path",{d:"M29 6H16l-1-2H4L2 8h28zM0 10l2 20h28l2-20H0zm18.3 9.5V27h-5.6v-7.5H8l7.5-7.5 7.5 7.5h-4.7z"},null,-1),n=[c];function r(a,l){return e(),o("svg",s,[...n])}const h={render:r};export{h as default,r as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as t,a as o}from"./index-
|
|
1
|
+
import{o as e,c as t,a as o}from"./index-c828fba8.js";const c={xmlns:"http://www.w3.org/2000/svg",width:"640",height:"640",viewBox:"0 -32 640 640"},s=o("path",{d:"M495.46 365.98c-13.03-13.37-150.24-144.06-150.24-144.06A35.16 35.16 0 0 0 320 211.2a35.06 35.06 0 0 0-25.22 10.72s-137.2 130.7-150.27 144.06c-13 13.38-13.9 37.44 0 51.72 14 14.24 33.4 15.4 50.48 0L320 297.8l125.02 119.9c17.1 15.4 36.55 14.24 50.44 0 13.95-14.3 13.08-38.37 0-51.72z"},null,-1),r=[s];function a(n,d){return e(),t("svg",c,[...r])}const i={render:a};export{i as default,a as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as o,a as t}from"./index-
|
|
1
|
+
import{o as e,c as o,a as t}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"-6 -2 44 36"},r=t("path",{d:"M12 18H6v4l-6-6 6-6v4h6zm8-4h6v-4l6 6-6 6v-4h-6z"},null,-1),c=[r];function n(a,h){return e(),o("svg",s,[...c])}const d={render:n};export{d as default,n as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as o,a as t}from"./index-
|
|
1
|
+
import{o as e,c as o,a as t}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -6 16 44"},r=t("path",{d:"M8 20v6h4l-6 6-6-6h4v-6zm-4-8V6H0l6-6 6 6H8v6z"},null,-1),c=[r];function n(a,l){return e(),o("svg",s,[...c])}const h={render:n};export{h as default,n as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as t,a as o}from"./index-
|
|
1
|
+
import{o as e,c as t,a as o}from"./index-c828fba8.js";const c={xmlns:"http://www.w3.org/2000/svg",width:"512",height:"512",viewBox:"-48 0 512 512"},s=o("path",{d:"M320 96 128 288l-64-64-64 64 128 128 256-256-64-64z"},null,-1),n=[s];function r(a,h){return e(),t("svg",c,[...n])}const i={render:r};export{i as default,r as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as t,a as o}from"./index-
|
|
1
|
+
import{o as e,c as t,a as o}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",width:"512",height:"512",viewBox:"-24 8 512 512"},c=o("path",{d:"m304 96-48 48 112 112-112 112 48 48 144-160L304 96zm-160 0L0 256l144 160 48-48L80 256l112-112-48-48z"},null,-1),n=[c];function r(a,d){return e(),t("svg",s,[...n])}const l={render:r};export{l as default,r as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as o,a as t}from"./index-
|
|
1
|
+
import{o as e,c as o,a as t}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 36 36"},c=t("path",{d:"M26 8h-6V6l-6-6H0v24h12v8h20V14l-6-6zm0 2.83L29.17 14H26v-3.17zm-12-8L17.17 6H14V2.83zM2 2h10v6h6v14H2V2zm28 28H14v-6h6V10h4v6h6v14z"},null,-1),h=[c];function n(r,a){return e(),o("svg",s,[...h])}const l={render:n};export{l as default,n as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c,a as t}from"./index-
|
|
1
|
+
import{o as e,c,a as t}from"./index-c828fba8.js";const o={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"},s=t("path",{d:"M19.2 2.6H6.1V29h19.8V9.3l-6.7-6.7zm3 15c0 .2-.2.4-.4.4h-4.4v4.4c0 .2-.2.4-.4.4h-2.4c-.2 0-.4-.2-.4-.4V18H9.9c-.2 0-.4-.2-.4-.4v-2.4c0-.2.2-.4.4-.4h4.4v-4.4c0-.2.2-.4.4-.4H17c.2 0 .4.2.4.4v4.4h4.4c.2 0 .4.2.4.4v2.4z"},null,-1),n=[s];function r(a,h){return e(),c("svg",o,[...n])}const d={render:r};export{d as default,r as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c,a as t}from"./index-
|
|
1
|
+
import{o as e,c,a as t}from"./index-c828fba8.js";const o={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"},s=t("path",{d:"M29 6H16l-1-2H4L2 8h28zM0 10l2 20h28l2-20H0zm22.8 11.2c0 .3-.2.5-.5.5h-5.2v5.2c0 .3-.2.5-.5.5h-2.8c-.3 0-.5-.2-.5-.5v-5.2H8.1c-.3 0-.5-.2-.5-.5v-2.8c0-.3.2-.5.5-.5h5.2v-5.2c0-.3.2-.5.5-.5h2.8c.3 0 .5.2.5.5v5.2h5.2c.3 0 .5.2.5.5v2.8z"},null,-1),r=[s];function h(n,a){return e(),c("svg",o,[...r])}const d={render:h};export{d as default,h as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as o,a as t}from"./index-
|
|
1
|
+
import{o as e,c as o,a as t}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"},c=t("path",{d:"M25.3 8.56 17.88 16l7.44 7.44-1.86 1.87L16 17.9l-7.44 7.4-1.86-1.85L14.12 16 6.68 8.56 8.55 6.7 16 14.12l7.44-7.44z"},null,-1),n=[c];function r(a,l){return e(),o("svg",s,[...n])}const _={render:r};export{_ as default,r as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as t,a as o}from"./index-
|
|
1
|
+
import{o as e,c as t,a as o}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"},a=o("path",{d:"M24.27 3.2H6.4a3.2 3.2 0 0 0-3.2 3.2v19.2a3.2 3.2 0 0 0 3.2 3.2h19.2a3.2 3.2 0 0 0 3.2-3.2V8.2l-4.53-5zm-1.87 9.6c0 .88-.72 1.6-1.6 1.6h-9.6a1.6 1.6 0 0 1-1.6-1.6v-8h12.8v8zm-1.6-6.4h-3.2v6.4h3.2V6.4z"},null,-1),c=[a];function n(r,h){return e(),t("svg",s,[...c])}const l={render:n};export{l as default,n as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as c,c as s,a as e}from"./index-
|
|
1
|
+
import{o as c,c as s,a as e}from"./index-c828fba8.js";const o={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 30 30"},t=e("path",{d:"M23 25.9c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3c-.3 0-.6.1-.8.3-.2.2-.3.5-.3.8s.1.6.3.8c.2.2.5.3.8.3.3 0 .6-.1.8-.3.2-.3.3-.5.3-.8zm4.6 0c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3c-.3 0-.6.1-.8.3-.2.2-.3.5-.3.8s.1.6.3.8c.2.2.5.3.8.3.3 0 .6-.1.8-.3.2-.3.3-.5.3-.8zm2.3-4v5.7c0 .5-.2.9-.5 1.2-.3.3-.7.5-1.2.5H1.9c-.5 0-.9-.2-1.2-.5s-.5-.7-.5-1.2v-5.7c0-.5.2-.9.5-1.2.3-.3.7-.5 1.2-.5h8.3l2.4 2.4c.7.7 1.5 1 2.4 1 .9 0 1.7-.3 2.4-1l2.4-2.4h8.3c.5 0 .9.2 1.2.5.4.3.6.7.6 1.2zm-5.8-10.2c.2.5.1.9-.3 1.3l-8 8c-.2.2-.5.3-.8.3-.3 0-.6-.1-.8-.3l-8-8c-.4-.3-.5-.8-.3-1.3S6.5 11 7 11h4.6V3c0-.3.1-.6.3-.8s.5-.3.8-.3h4.6c.3 0 .6.1.8.3s.3.5.3.8v8H23c.5 0 .8.2 1.1.7z"},null,-1),n=[t];function a(l,r){return c(),s("svg",o,[...n])}const h={render:a};export{h as default,a as render};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,c as t,a as o}from"./index-
|
|
1
|
+
import{o as e,c as t,a as o}from"./index-c828fba8.js";const s={xmlns:"http://www.w3.org/2000/svg",width:"448",height:"448",viewBox:"-136 0 448 448"},c=o("path",{d:"M128 312v56q0 6.5-4.75 11.25T112 384H48q-6.5 0-11.25-4.75T32 368v-56q0-6.5 4.75-11.25T48 296h64q6.5 0 11.25 4.75T128 312zm7.5-264-7 192q-.25 6.5-5.13 11.25T112 256H48q-6.5 0-11.38-4.75T31.5 240l-7-192q-.25-6.5 4.38-11.25T40 32h80q6.5 0 11.13 4.75T135.5 48z"},null,-1),n=[c];function a(r,h){return e(),t("svg",s,[...n])}const i={render:a};export{i as default,a as render};
|