Compare commits
No commits in common. "5bdeb180b5b145ab38ff0f7a7cd4781d22f0824b" and "2978e0c9681ab1f3088cc8f37ce644f5c6d82634" have entirely different histories.
5bdeb180b5
...
2978e0c968
@ -120,9 +120,6 @@ class FileEntry(msgspec.Struct, array_like=True):
|
|||||||
size: int
|
size: int
|
||||||
isfile: int
|
isfile: int
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.key or "FileEntry()"
|
|
||||||
|
|
||||||
|
|
||||||
class Update(msgspec.Struct, array_like=True):
|
class Update(msgspec.Struct, array_like=True):
|
||||||
...
|
...
|
||||||
@ -140,10 +137,6 @@ class UpdIns(Update, tag="i"):
|
|||||||
items: list[FileEntry]
|
items: list[FileEntry]
|
||||||
|
|
||||||
|
|
||||||
class UpdateMessage(msgspec.Struct):
|
|
||||||
update: list[UpdKeep | UpdDel | UpdIns]
|
|
||||||
|
|
||||||
|
|
||||||
class Space(msgspec.Struct):
|
class Space(msgspec.Struct):
|
||||||
disk: int
|
disk: int
|
||||||
free: int
|
free: int
|
||||||
|
@ -50,42 +50,37 @@ class State:
|
|||||||
begin, end = 0, len(self._listing)
|
begin, end = 0, len(self._listing)
|
||||||
level = 0
|
level = 0
|
||||||
isfile = 0
|
isfile = 0
|
||||||
|
while level < len(relpath.parts):
|
||||||
# Special case for root
|
# Enter a subdirectory
|
||||||
if not relpath.parts:
|
|
||||||
return slice(begin, end)
|
|
||||||
|
|
||||||
begin += 1
|
|
||||||
for part in relpath.parts:
|
|
||||||
level += 1
|
level += 1
|
||||||
found = False
|
begin += 1
|
||||||
|
|
||||||
while begin < end:
|
|
||||||
entry = self._listing[begin]
|
|
||||||
|
|
||||||
if entry.level < level:
|
|
||||||
break
|
|
||||||
|
|
||||||
if entry.level == level:
|
|
||||||
if entry.name == part:
|
|
||||||
found = True
|
|
||||||
if level == len(relpath.parts):
|
if level == len(relpath.parts):
|
||||||
isfile = relfile
|
isfile = relfile
|
||||||
else:
|
name = relpath.parts[level - 1]
|
||||||
|
namesort = sortkey(name)
|
||||||
|
r = self._listing[begin]
|
||||||
|
assert r.level == level
|
||||||
|
# Iterate over items at this level
|
||||||
|
while (
|
||||||
|
begin < end
|
||||||
|
and r.name != name
|
||||||
|
and r.isfile <= isfile
|
||||||
|
and sortkey(r.name) < namesort
|
||||||
|
):
|
||||||
|
# Skip contents
|
||||||
begin += 1
|
begin += 1
|
||||||
break
|
while begin < end and self._listing[begin].level > level:
|
||||||
cmp = entry.isfile - isfile or sortkey(entry.name) > sortkey(part)
|
|
||||||
if cmp > 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
begin += 1
|
begin += 1
|
||||||
|
# Not found?
|
||||||
if not found:
|
if begin == end or self._listing[begin].level < level:
|
||||||
return slice(begin, begin)
|
return slice(begin, begin)
|
||||||
|
r = self._listing[begin]
|
||||||
# Found the starting point, now find the end of the slice
|
# Not found?
|
||||||
for end in range(begin + 1, len(self._listing) + 1):
|
if begin == end or r.name != name:
|
||||||
if end == len(self._listing) or self._listing[end].level <= level:
|
return slice(begin, begin)
|
||||||
|
# Found an item, now find its end
|
||||||
|
for end in range(begin + 1, len(self._listing)):
|
||||||
|
if self._listing[end].level <= level:
|
||||||
break
|
break
|
||||||
return slice(begin, end)
|
return slice(begin, end)
|
||||||
|
|
||||||
@ -153,12 +148,11 @@ def watcher_thread(loop):
|
|||||||
rootpath = config.config.path
|
rootpath = config.config.path
|
||||||
i = inotify.adapters.InotifyTree(rootpath.as_posix())
|
i = inotify.adapters.InotifyTree(rootpath.as_posix())
|
||||||
# Initialize the tree from filesystem
|
# Initialize the tree from filesystem
|
||||||
new = walk()
|
old, new = state.root, walk()
|
||||||
with state.lock:
|
|
||||||
old = state.root
|
|
||||||
if old != new:
|
if old != new:
|
||||||
|
with state.lock:
|
||||||
state.root = new
|
state.root = new
|
||||||
broadcast(format_update(old, new), loop)
|
broadcast(format_root(new), loop)
|
||||||
|
|
||||||
# The watching is not entirely reliable, so do a full refresh every minute
|
# The watching is not entirely reliable, so do a full refresh every minute
|
||||||
refreshdl = time.monotonic() + 60.0
|
refreshdl = time.monotonic() + 60.0
|
||||||
@ -196,10 +190,10 @@ def watcher_thread_poll(loop):
|
|||||||
|
|
||||||
while not quit:
|
while not quit:
|
||||||
rootpath = config.config.path
|
rootpath = config.config.path
|
||||||
new = walk()
|
|
||||||
with state.lock:
|
|
||||||
old = state.root
|
old = state.root
|
||||||
|
new = walk()
|
||||||
if old != new:
|
if old != new:
|
||||||
|
with state.lock:
|
||||||
state.root = new
|
state.root = new
|
||||||
broadcast(format_update(old, new), loop)
|
broadcast(format_update(old, new), loop)
|
||||||
|
|
||||||
@ -289,11 +283,13 @@ def format_update(old, new):
|
|||||||
|
|
||||||
del_count = 0
|
del_count = 0
|
||||||
rest = new[nidx:]
|
rest = new[nidx:]
|
||||||
while oidx < len(old) and old[oidx] not in rest:
|
while old[oidx] not in rest:
|
||||||
del_count += 1
|
del_count += 1
|
||||||
oidx += 1
|
oidx += 1
|
||||||
|
|
||||||
if del_count:
|
if del_count:
|
||||||
update.append(UpdDel(del_count))
|
update.append(UpdDel(del_count))
|
||||||
|
oidx += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
insert_items = []
|
insert_items = []
|
||||||
@ -337,9 +333,8 @@ async def abroadcast(msg):
|
|||||||
|
|
||||||
async def start(app, loop):
|
async def start(app, loop):
|
||||||
config.load_config()
|
config.load_config()
|
||||||
use_inotify = False and sys.platform == "linux"
|
|
||||||
app.ctx.watcher = threading.Thread(
|
app.ctx.watcher = threading.Thread(
|
||||||
target=watcher_thread if use_inotify else watcher_thread_poll,
|
target=watcher_thread if sys.platform == "linux" else watcher_thread_poll,
|
||||||
args=[loop],
|
args=[loop],
|
||||||
)
|
)
|
||||||
app.ctx.watcher.start()
|
app.ctx.watcher.start()
|
||||||
|
@ -135,7 +135,7 @@ function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
|||||||
else console.log("Unknown update action", action, arg)
|
else console.log("Unknown update action", action, arg)
|
||||||
}
|
}
|
||||||
if (oidx != tree.length)
|
if (oidx != tree.length)
|
||||||
throw Error(`Tree update out of sync, number of entries mismatch: got ${oidx}, expected ${tree.length}, new tree ${newtree.length}`)
|
throw Error(`Tree update out of sync, number of entries mismatch: got ${oidx}, expected ${tree.length}`)
|
||||||
store.updateRoot(newtree)
|
store.updateRoot(newtree)
|
||||||
tree = newtree
|
tree = newtree
|
||||||
saveSession()
|
saveSession()
|
||||||
|
@ -4,6 +4,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { collator } from '@/utils'
|
import { collator } from '@/utils'
|
||||||
import { logoutUser } from '@/repositories/User'
|
import { logoutUser } from '@/repositories/User'
|
||||||
import { watchConnect } from '@/repositories/WS'
|
import { watchConnect } from '@/repositories/WS'
|
||||||
|
import { format } from 'path'
|
||||||
|
|
||||||
type FileData = { id: string; mtime: number; size: number; dir: DirectoryData }
|
type FileData = { id: string; mtime: number; size: number; dir: DirectoryData }
|
||||||
type DirectoryData = {
|
type DirectoryData = {
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
from pathlib import PurePosixPath
|
|
||||||
|
|
||||||
import msgspec
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from cista.protocol import FileEntry, UpdateMessage, UpdDel, UpdIns, UpdKeep
|
|
||||||
from cista.watching import State, format_update
|
|
||||||
|
|
||||||
|
|
||||||
def decode(data: str):
|
|
||||||
return msgspec.json.decode(data, type=UpdateMessage).update
|
|
||||||
|
|
||||||
|
|
||||||
# Helper function to create a list of FileEntry objects
|
|
||||||
def f(count, start=0):
|
|
||||||
return [FileEntry(i, str(i), str(i), 0, 0, 0) for i in range(start, start + count)]
|
|
||||||
|
|
||||||
|
|
||||||
def test_identical_lists():
|
|
||||||
old_list = f(3)
|
|
||||||
new_list = old_list.copy()
|
|
||||||
expected = [UpdKeep(3)]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_completely_different_lists():
|
|
||||||
old_list = f(3)
|
|
||||||
new_list = f(3, 3) # Different entries
|
|
||||||
expected = [UpdDel(3), UpdIns(new_list)]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_insertions():
|
|
||||||
old_list = f(3)
|
|
||||||
new_list = old_list[:2] + f(1, 10) + old_list[2:]
|
|
||||||
expected = [UpdKeep(2), UpdIns(f(1, 10)), UpdKeep(1)]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_deletions():
|
|
||||||
old_list = f(3)
|
|
||||||
new_list = [old_list[0], old_list[2]]
|
|
||||||
expected = [UpdKeep(1), UpdDel(1), UpdKeep(1)]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_mixed_operations():
|
|
||||||
old_list = f(4)
|
|
||||||
new_list = [old_list[0], old_list[2], *f(1, 10)]
|
|
||||||
expected = [UpdKeep(1), UpdDel(1), UpdKeep(1), UpdDel(1), UpdIns(f(1, 10))]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_old_list():
|
|
||||||
old_list = []
|
|
||||||
new_list = f(3)
|
|
||||||
expected = [UpdIns(new_list)]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_new_list():
|
|
||||||
old_list = f(3)
|
|
||||||
new_list = []
|
|
||||||
expected = [UpdDel(3)]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_longer_lists():
|
|
||||||
old_list = f(6)
|
|
||||||
new_list = f(1, 6) + old_list[1:3] + old_list[4:5] + f(2, 7)
|
|
||||||
expected = [
|
|
||||||
UpdDel(1),
|
|
||||||
UpdIns(f(1, 6)),
|
|
||||||
UpdKeep(2),
|
|
||||||
UpdDel(1),
|
|
||||||
UpdKeep(1),
|
|
||||||
UpdDel(1),
|
|
||||||
UpdIns(f(2, 7)),
|
|
||||||
]
|
|
||||||
assert decode(format_update(old_list, new_list)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def sortkey(name):
|
|
||||||
# Define the sorting key for names here
|
|
||||||
return name.lower()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def state():
|
|
||||||
entries = [
|
|
||||||
FileEntry(0, "", "root", 0, 0, 0),
|
|
||||||
FileEntry(1, "bar", "bar", 0, 0, 0),
|
|
||||||
FileEntry(2, "baz", "bar/baz", 0, 0, 0),
|
|
||||||
FileEntry(1, "foo", "foo", 0, 0, 0),
|
|
||||||
FileEntry(1, "xxx", "xxx", 0, 0, 0),
|
|
||||||
FileEntry(2, "yyy", "xxx/yyy", 0, 0, 1),
|
|
||||||
]
|
|
||||||
s = State()
|
|
||||||
s._listing = entries
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def test_existing_directory(state):
|
|
||||||
path = PurePosixPath("bar")
|
|
||||||
expected_slice = slice(1, 3) # Includes 'bar' and 'baz'
|
|
||||||
assert state._slice(path) == expected_slice
|
|
||||||
|
|
||||||
|
|
||||||
def test_existing_file(state):
|
|
||||||
path = PurePosixPath("xxx/yyy")
|
|
||||||
expected_slice = slice(5, 6) # Only includes 'yyy'
|
|
||||||
assert state._slice(path) == expected_slice
|
|
||||||
|
|
||||||
|
|
||||||
def test_nonexistent_directory(state):
|
|
||||||
path = PurePosixPath("zzz")
|
|
||||||
expected_slice = slice(6, 6) # 'zzz' would be inserted at end
|
|
||||||
assert state._slice(path) == expected_slice
|
|
||||||
|
|
||||||
|
|
||||||
def test_nonexistent_file(state):
|
|
||||||
path = (PurePosixPath("bar/mmm"), 1)
|
|
||||||
expected_slice = slice(3, 3) # A file would be inserted after 'baz' under 'bar'
|
|
||||||
assert state._slice(path) == expected_slice
|
|
||||||
|
|
||||||
|
|
||||||
def test_root_directory(state):
|
|
||||||
path = PurePosixPath()
|
|
||||||
expected_slice = slice(0, 6) # Entire tree
|
|
||||||
assert state._slice(path) == expected_slice
|
|
||||||
|
|
||||||
|
|
||||||
def test_directory_with_subdirs_and_files(state):
|
|
||||||
path = PurePosixPath("xxx")
|
|
||||||
expected_slice = slice(4, 6) # Includes 'xxx' and 'yyy'
|
|
||||||
assert state._slice(path) == expected_slice
|
|
Loading…
x
Reference in New Issue
Block a user