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.
Files changed (76) hide show
  1. cista/_version.py +1 -1
  2. cista/api.py +16 -5
  3. cista/app.py +40 -27
  4. cista/auth.py +33 -1
  5. cista/preview.py +114 -0
  6. cista/protocol.py +5 -2
  7. cista/serve.py +1 -1
  8. cista/util/apphelpers.py +3 -3
  9. cista/watching.py +271 -210
  10. cista/wwwroot/assets/{add-file-78f95102.js → add-file-e19e18ff.js} +1 -1
  11. cista/wwwroot/assets/{add-folder-ef91bb0e.js → add-folder-cf930a27.js} +1 -1
  12. cista/wwwroot/assets/{arrow-c279db10.js → arrow-f09bf4bc.js} +1 -1
  13. cista/wwwroot/assets/{arrows-h-0090937e.js → arrows-h-74714fc1.js} +1 -1
  14. cista/wwwroot/assets/{arrows-v-05d0dc3e.js → arrows-v-900e3e20.js} +1 -1
  15. cista/wwwroot/assets/{check-736f9156.js → check-5d5030f9.js} +1 -1
  16. cista/wwwroot/assets/{code-f442e1ae.js → code-08d0327a.js} +1 -1
  17. cista/wwwroot/assets/{copy-f9304d76.js → copy-d4e131a3.js} +1 -1
  18. cista/wwwroot/assets/{create-file-5cfbca5b.js → create-file-7ee36655.js} +1 -1
  19. cista/wwwroot/assets/{create-folder-f3416e7f.js → create-folder-4baad31c.js} +1 -1
  20. cista/wwwroot/assets/{cross-34310f33.js → cross-5dc70d0a.js} +1 -1
  21. cista/wwwroot/assets/{disk-9de14290.js → disk-1c8c6bb2.js} +1 -1
  22. cista/wwwroot/assets/{download-40f6e8d9.js → download-4c58df40.js} +1 -1
  23. cista/wwwroot/assets/{exclamation-67d2f487.js → exclamation-b23fc5a1.js} +1 -1
  24. cista/wwwroot/assets/{eye-1a9cae12.js → eye-fc97097a.js} +1 -1
  25. cista/wwwroot/assets/{find-f49bedc3.js → find-9f5d36c7.js} +1 -1
  26. cista/wwwroot/assets/{fullscreen-6e83f85d.js → fullscreen-72bac89d.js} +1 -1
  27. cista/wwwroot/assets/{github-33da7a52.js → github-8781f34a.js} +1 -1
  28. cista/wwwroot/assets/index-5ab4ce9a.css +1 -0
  29. cista/wwwroot/assets/index-c828fba8.js +13 -0
  30. cista/wwwroot/assets/{info-9a003b6c.js → info-81c1e2fa.js} +1 -1
  31. cista/wwwroot/assets/{link-b5bf40a7.js → link-ddc2f9ba.js} +1 -1
  32. cista/wwwroot/assets/{logo-adde2ca4.js → logo-10d7b218.js} +1 -1
  33. cista/wwwroot/assets/{loop-762ab430.js → loop-a579040b.js} +1 -1
  34. cista/wwwroot/assets/{menu-633b2a61.js → menu-cb4bdef2.js} +1 -1
  35. cista/wwwroot/assets/{next-d9d1d510.js → next-bb1c5152.js} +1 -1
  36. cista/wwwroot/assets/{open-91351e45.js → open-f8e4da33.js} +1 -1
  37. cista/wwwroot/assets/{paste-d3f337c1.js → paste-0bef6dfd.js} +1 -1
  38. cista/wwwroot/assets/{pause-8f82e536.js → pause-27898a74.js} +1 -1
  39. cista/wwwroot/assets/{pencil-0b434534.js → pencil-19d33c49.js} +1 -1
  40. cista/wwwroot/assets/{play-e0e51167.js → play-fe6706ce.js} +1 -1
  41. cista/wwwroot/assets/{plus-e2a2ec0f.js → plus-ab9d4dbd.js} +1 -1
  42. cista/wwwroot/assets/{previous-cd61ebe6.js → previous-5be4e762.js} +1 -1
  43. cista/wwwroot/assets/{reload-a9c668b2.js → reload-84455f3f.js} +1 -1
  44. cista/wwwroot/assets/{rename-bd15329b.js → rename-88ab1b4d.js} +1 -1
  45. cista/wwwroot/assets/{scissors-dcbf78c0.js → scissors-1266cf56.js} +1 -1
  46. cista/wwwroot/assets/{shuffle-74e9ea1c.js → shuffle-0412a143.js} +1 -1
  47. cista/wwwroot/assets/{signin-bbf26a1b.js → signin-dcc16c88.js} +1 -1
  48. cista/wwwroot/assets/{signout-caa34d68.js → signout-935b6b65.js} +1 -1
  49. cista/wwwroot/assets/{skip-423d5cf0.js → skip-adeae5b8.js} +1 -1
  50. cista/wwwroot/assets/{spinner-b299e14e.js → spinner-7c5e1e66.js} +1 -1
  51. cista/wwwroot/assets/{stop-91578a62.js → stop-6ec4fac4.js} +1 -1
  52. cista/wwwroot/assets/{trash-3b7b72a3.js → trash-218fd3df.js} +1 -1
  53. cista/wwwroot/assets/{triangle-724a2314.js → triangle-40a425a9.js} +1 -1
  54. cista/wwwroot/assets/{unfullscreen-29f4977c.js → unfullscreen-3d51fed5.js} +1 -1
  55. cista/wwwroot/assets/{up-arrow-ceb58d59.js → up-arrow-af385124.js} +1 -1
  56. cista/wwwroot/assets/{upload-cloud-936fb8b2.js → upload-cloud-68d0fe9f.js} +1 -1
  57. cista/wwwroot/assets/{user-cog-887c6f3f.js → user-cog-bca0b085.js} +1 -1
  58. cista/wwwroot/assets/{user-ab4ed9ac.js → user-dd4fef53.js} +1 -1
  59. cista/wwwroot/assets/{volume-high-74a17568.js → volume-high-0f8f0e3d.js} +1 -1
  60. cista/wwwroot/assets/{volume-low-f7170d5f.js → volume-low-b1bd663e.js} +1 -1
  61. cista/wwwroot/assets/{volume-medium-7b16c1db.js → volume-medium-cd88f329.js} +1 -1
  62. cista/wwwroot/assets/{volume-mute-0c7078a1.js → volume-mute-458a2f48.js} +1 -1
  63. cista/wwwroot/assets/{window-f7f79ada.js → window-b63b3c5a.js} +1 -1
  64. cista/wwwroot/assets/{window-cross-e3a75b33.js → window-cross-b0bd1b60.js} +1 -1
  65. cista/wwwroot/assets/{wordwrap-3bcce83c.js → wordwrap-26b55346.js} +1 -1
  66. cista/wwwroot/assets/{zoomin-bd27188c.js → zoomin-cb10ed9f.js} +1 -1
  67. cista/wwwroot/assets/{zoomout-31844707.js → zoomout-90bcc471.js} +1 -1
  68. cista/wwwroot/index.html +3 -4
  69. {cista-0.5.0.dist-info → cista-0.7.0.dist-info}/METADATA +15 -10
  70. cista-0.7.0.dist-info/RECORD +86 -0
  71. cista/wwwroot/assets/cog-fcdd928d.js +0 -1
  72. cista/wwwroot/assets/index-6716899e.js +0 -11
  73. cista/wwwroot/assets/index-bcee9add.css +0 -1
  74. cista-0.5.0.dist-info/RECORD +0 -86
  75. {cista-0.5.0.dist-info → cista-0.7.0.dist-info}/WHEEL +0 -0
  76. {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 logging
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._listing: list[FileEntry] = []
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
- def _slice(self, idx: PurePosixPath | tuple[PurePosixPath, int]):
49
- relpath, relfile = idx if isinstance(idx, tuple) else (idx, 0)
50
- begin, end = 0, len(self._listing)
51
- level = 0
52
- isfile = 0
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
- begin += 1
59
- for part in relpath.parts:
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
- found = False
62
-
63
- while begin < end:
64
- entry = self._listing[begin]
65
-
66
- if entry.level < level:
67
- break
68
-
69
- if entry.level == level:
70
- if entry.name == part:
71
- found = True
72
- if level == len(relpath.parts):
73
- isfile = relfile
74
- else:
75
- begin += 1
76
- break
77
- cmp = entry.isfile - isfile or sortkey(entry.name) > sortkey(part)
78
- if cmp > 0:
79
- break
80
-
81
- begin += 1
82
-
83
- if not found:
84
- return slice(begin, begin)
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 = False
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
- while not quit:
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
- time.sleep(2.0)
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
- s = f.stat()
226
- li.append((int(not stat.S_ISDIR(s.st_mode)), f.name, s))
227
- for [isfile, name, s] in humansorted(li):
228
- if quit:
229
- raise SystemExit("quit")
230
- subtree = _walk(rel / name, isfile, s)
231
- child = subtree[0]
232
- entry.mtime = max(entry.mtime, child.mtime)
233
- entry.size += child.size
234
- ret.extend(subtree)
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
- print("OS error walking path", path, e)
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 update(relpath: PurePosixPath, loop):
243
- """Called by inotify updates, check the filesystem and broadcast any changes."""
244
- if rootpath is None or relpath is None:
245
- print("ERROR", rootpath, relpath)
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
- with state.lock:
248
- old = state[relpath]
249
- if old == new:
250
- return
251
- old = state.root
252
- if new:
253
- state[relpath, new[0].isfile] = new
254
- else:
255
- del state[relpath]
256
- broadcast(format_update(old, state.root), loop)
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
- rest = new[nidx:]
276
- while oidx < len(old) and old[oidx] not in rest:
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
- rest = old[oidx:]
285
- while nidx < len(new) and new[nidx] not in rest:
286
- insert_items.append(new[nidx])
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
- update.append(UpdIns(insert_items))
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 oidx < len(old):
294
- update.append(UpdDel(len(old) - oidx))
295
- elif nidx < len(new):
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
- logging.exception("Broadcast error")
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=watcher_thread if use_inotify else watcher_thread_poll,
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
- global quit
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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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
+ 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-6716899e.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};
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};