Compare commits

..

No commits in common. "4c51029c9fedef275b5fa0334efb9e05bc08bb7c" and "54d6ea6332e8fe09e5e73973ef3c492dea9d7fb2" have entirely different histories.

13 changed files with 115 additions and 200 deletions

View File

@ -3,39 +3,46 @@
:root { :root {
--primary-color: #000; --primary-color: #000;
--primary-background: #ddd; --primary-background: #ddd;
--header-background: var(--soft-color); --header-background: #246;
--header-color: #ccc; --header-color: #ccc;
--input-background: #fff;
--input-color: #000;
--primary-color: #000; --primary-color: #000;
--soft-color: #146;
--accent-color: #f80; --accent-color: #f80;
--transition-time: 0.2s; --transition-time: 0.2s;
/* The following are overridden by responsive layouts */ /* The following are overridden by responsive layouts */
--root-font-size: 1rem; --root-font-size: 1rem;
--header-font-size: 1rem; --header-font-size: 1rem;
--header-height: calc(6.5 * var(--header-font-size)); --header-height: calc(8 * var(--header-font-size));
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--primary-color: #ddd; --primary-color: #ddd;
--primary-background: var(--soft-color); --primary-background: #003;
--header-background: #000; --header-background: #000;
--header-color: #ccc; --header-color: #ccc;
--input-background: var(--soft-color); }
--input-color: #ddd;
}
} }
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.size, .size,
.modified, .modified {
.summary {
display: none; display: none;
} }
} }
@media screen and (min-width: 1000px) { @media screen and (orientation: landscape) and (min-width: 1200px) {
/* Breadcrumbs and buttons side by side */
header {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: end;
}
header .breadcrumb {
font-size: 1.7em;
flex-shrink: 10;
}
}
@media screen and (min-width: 800px) and (--webkit-min-device-pixel-ratio: 2) {
:root { :root {
--root-font-size: calc(8px + 8 * 100vw / 1000); --root-font-size: calc(16 * 100vw / 800);
} }
header .buttons:has(input[type='search']) > div { header .buttons:has(input[type='search']) > div {
display: none; display: none;
@ -44,51 +51,20 @@
display: inherit; display: inherit;
} }
} }
@media screen and (min-width: 2000px) { @media screen and (min-width: 1600px) and (--webkit-min-device-pixel-ratio: 3) {
:root { :root {
--root-font-size: 1.5rem; --root-font-size: 2rem;
} }
} }
/* Low (landscape) screens: smaller header */
@media screen and (max-height: 600px) { @media screen and (max-height: 600px) {
:root { :root {
--header-font-size: calc(10px + 10 * 100vh / 600); /* 20px at 600px height */ --header-font-size: calc(16 * 100vh / 600); /* 16px (1rem nominal) at 600px height */
--root-font-size: 0.8rem;
}
header .breadcrumb > * {
padding-top: calc(8 + 8 * 100vh / 600) !important;
padding-bottom: calc(8 + 8 * 100vh / 600) !important;
} }
} }
@media screen and (max-height: 300px) { @media screen and (max-height: 300px) {
:root { :root {
--header-font-size: 15px; /* Don't go smaller than this, no benefit */ --header-font-size: 0.5rem; /* Don't go smaller than this, no benefit */
--header-height: calc(1.75 * 16px); --header-height: calc(1.75 * 16px);
--root-font-size: 0.6rem;
}
header .breadcrumb > * {
padding-top: 14px !important;
padding-bottom: 14px !important;
}
}
@media screen and (orientation: landscape) and (min-width: 700px) {
/* Breadcrumbs and buttons side by side */
:root {
--header-font-size: calc(8px + 8 * 100vh / 600); /* 16px (1rem nominal) at 600px height */
}
header {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: end;
}
header .breadcrumb {
flex-shrink: 1;
}
header .breadcrumb > * {
flex-shrink: 1;
padding-top: 1rem !important;
padding-bottom: 1rem !important;
} }
} }
@media print { @media print {
@ -143,7 +119,6 @@
} }
} }
html { html {
font-size: var(--root-font-size);
overflow: hidden; overflow: hidden;
} }
/* Hide scrollbar for all browsers */ /* Hide scrollbar for all browsers */
@ -223,7 +198,7 @@ main {
overflow-y: scroll; overflow-y: scroll;
} }
.spacer { flex-grow: 1 } .spacer { flex-grow: 1 }
.smallgap { flex-shrink: 1; width: 2em } .smallgap { margin-left: 2em }
[data-tooltip]:hover:after { [data-tooltip]:hover:after {
z-index: 101; z-index: 101;
@ -260,9 +235,3 @@ main {
opacity: 0; opacity: 0;
} }
} }
.error-message {
padding: .5em;
font-weight: bold;
background: var(--accent-color);
color: #000;
}

View File

@ -129,8 +129,8 @@
</td> </td>
</tr> </tr>
</template> </template>
<tr class="summary" v-if="props.documents.length > 1"> <tr class="summary">
<td colspan="3" class="right">{{props.documents.length}} items</td> <td colspan="3" class="right">{{props.documents.length}} items shown:</td>
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td> <td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td>
<td class="menu"></td> <td class="menu"></td>
</tr> </tr>
@ -140,7 +140,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watchEffect } from 'vue' import { ref, computed, watchEffect, onBeforeUpdate } from 'vue'
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import type { Document } from '@/repositories/Document' import type { Document } from '@/repositories/Document'
import FileRenameInput from './FileRenameInput.vue' import FileRenameInput from './FileRenameInput.vue'
@ -442,7 +442,6 @@ table td {
} }
} }
thead tr { thead tr {
font-size: var(--header-font-size);
background: linear-gradient(to bottom, #eee, #fff 30%, #ddd); background: linear-gradient(to bottom, #eee, #fff 30%, #ddd);
color: #000; color: #000;
box-shadow: 0 0 .2rem black; box-shadow: 0 0 .2rem black;
@ -511,7 +510,4 @@ tbody .selection input {
.loc { .loc {
color: #888; color: #888;
} }
.summary {
color: #888;
}
</style> </style>

View File

@ -1,10 +1,6 @@
<template> <template>
<nav class="headermain"> <nav class="headermain">
<div class="buttons"> <div class="buttons">
<template v-if="documentStore.error">
<div class="error-message" @click="documentStore.error = ''">{{ documentStore.error }}</div>
<div class="smallgap"></div>
</template>
<UploadButton /> <UploadButton />
<SvgButton <SvgButton
name="create-folder" name="create-folder"
@ -27,6 +23,17 @@
<SvgButton name="cog" @click="settingsMenu" /> <SvgButton name="cog" @click="settingsMenu" />
</div> </div>
</nav> </nav>
<context-menu v-model:show="showMenu">
<context-menu-item label="Simple item" @click="onMenuClick(1)" />
<context-menu-sperator /><!--use this to add sperator-->
<context-menu-group label="Menu with child">
<context-menu-item label="Item1" @click="onMenuClick(2)" />
<context-menu-item label="Item2" @click="onMenuClick(3)" />
<context-menu-group label="Child with v-for 50">
<context-menu-item v-for="index of 50" :key="index" :label="'Item3-'+index" @click="onLoopMenuClick(index)" />
</context-menu-group>
</context-menu-group>
</context-menu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -57,16 +64,12 @@ const toggleSearchInput = () => {
const settingsMenu = (e: Event) => { const settingsMenu = (e: Event) => {
// show the context menu // show the context menu
const items = []
if (documentStore.user.isLoggedIn) {
items.push({ label: `Logout ${documentStore.user.username ?? ''}`, onClick: () => documentStore.logout() })
} else {
items.push({ label: 'Login', onClick: () => documentStore.loginDialog() })
}
ContextMenu.showContextMenu({ ContextMenu.showContextMenu({
// @ts-ignore // @ts-ignore
x: e.target.getBoundingClientRect().right, y: e.target.getBoundingClientRect().bottom, x: e.target.getBoundingClientRect().right, y: e.target.getBoundingClientRect().bottom,
items, items: [
{ label: "Logout", onClick: () => { documentStore.logout() } },
]
}) })
} }
@ -88,8 +91,8 @@ defineExpose({
flex-shrink: 1; flex-shrink: 1;
} }
input[type='search'] { input[type='search'] {
background: var(--input-background); background: var(--primary-background);
color: var(--input-color); color: var(--primary-color);
border: 0; border: 0;
border-radius: 0.1em; border-radius: 0.1em;
padding: 0.5em; padding: 0.5em;

View File

@ -146,7 +146,7 @@ const download = async () => {
<style> <style>
.select-text { .select-text {
color: var(--accent-color); color: var(--accent-color);
white-space: nowrap; text-wrap: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<ModalDialog v-if="store.user.isOpenLoginModal" title="Authentication required" @blur="store.user.isOpenLoginModal = false"> <ModalDialog v-if="store.user.isOpenLoginModal" title="Authentication required">
<form @submit.prevent="login"> <form @submit.prevent="login">
<div class="login-container"> <div class="login-container">
<label for="username">Username:</label> <label for="username">Username:</label>
@ -7,8 +7,6 @@
id="username" id="username"
name="username" name="username"
autocomplete="username" autocomplete="username"
spellcheck="false"
autocorrect="off"
required required
v-model="loginForm.username" v-model="loginForm.username"
/> />
@ -18,14 +16,12 @@
name="password" name="password"
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
spellcheck="false"
autocorrect="off"
required required
v-model="loginForm.password" v-model="loginForm.password"
/> />
</div> </div>
<h3 class="error-text"> <h3 v-if="loginForm.error.length > 0" class="error-text">
{{ loginForm.error || '\u00A0' }} {{ loginForm.error }}
</h3> </h3>
<div class="dialog-buttons"> <div class="dialog-buttons">
<div class="spacer"></div> <div class="spacer"></div>
@ -37,13 +33,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { loginUser } from '@/repositories/User' import { loginUser, logoutUser } from '@/repositories/User'
import type { ISimpleError } from '@/repositories/Client' import type { ISimpleError } from '@/repositories/Client'
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
const confirmLoading = ref<boolean>(false) const confirmLoading = ref<boolean>(false)
const store = useDocumentStore() const store = useDocumentStore()
const logout = async () => {
try {
await logoutUser()
} finally {
location.reload()
}
}
const loginForm = reactive({ const loginForm = reactive({
username: '', username: '',
password: '', password: '',
@ -55,10 +59,13 @@ const login = async () => {
loginForm.error = '' loginForm.error = ''
confirmLoading.value = true confirmLoading.value = true
const msg = await loginUser(loginForm.username, loginForm.password) const msg = await loginUser(loginForm.username, loginForm.password)
store.login(msg.data.username, !!msg.data.privileged) console.log('Logged in', msg)
store.login(msg.username, !!msg.privileged)
} catch (error) { } catch (error) {
const httpError = error as ISimpleError const httpError = error as ISimpleError
loginForm.error = httpError.message || '🛑 Unknown error' if (httpError.name) {
loginForm.error = httpError.message
}
} finally { } finally {
confirmLoading.value = false confirmLoading.value = false
} }
@ -80,22 +87,18 @@ const login = async () => {
align-items: center; align-items: center;
} }
.button-login { .button-login {
color: #fff;
background: var(--soft-color);
cursor: pointer; cursor: pointer;
font-weight: bold;
border: 0; border: 0;
border-radius: .5rem; border-radius: .5rem;
padding: .5rem 2rem; padding: .5rem;
margin-left: auto; margin-left: auto;
transition: all var(--transition-time) linear; background-color: var(--accent-color);
color: var(--primary-color);
} }
.button-login:hover, .button-login:focus { .ant-btn-primary:not(:disabled):hover {
background: var(--accent-color); background-color: var(--blue-color);
box-shadow: 0 0 .3rem #000;
} }
.error-text { .error-text {
color: var(--red-color); color: var(--red-color);
height: 1em;
} }
</style> </style>

View File

@ -23,18 +23,15 @@ const props = withDefaults(
title: '' title: ''
} }
) )
const show = () => {
dialog.value!.showModal()
}
defineExpose({ show })
onMounted(() => { onMounted(() => {
show() dialog.value!.showModal()
}) })
</script> </script>
<style> <style>
/* Style for the background */ /* Style for the background */
dialog::backdrop { body:has(dialog[open])::before {
content: ''; content: '';
display: block; display: block;
position: fixed; position: fixed;
@ -43,7 +40,7 @@ dialog::backdrop {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #0008; background: #0008;
backdrop-filter: blur(0.4em); backdrop-filter: blur(0.2em);
z-index: 1000; z-index: 1000;
} }
@ -53,7 +50,6 @@ dialog[open] {
color: black; color: black;
display: block; display: block;
border: none; border: none;
font-size: 1.2rem;
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0.2rem 0.2rem 1rem #000; box-shadow: 0.2rem 0.2rem 1rem #000;
padding: 1rem; padding: 1rem;
@ -62,13 +58,11 @@ dialog[open] {
left: 0; left: 0;
z-index: 1001; z-index: 1001;
} }
input {
font: inherit;
}
dialog[open] > h1 { dialog[open] > h1 {
background: var(--soft-color); background: var(--accent-color);
color: #fff; color: black;
font-size: 1.2rem; font-size: 1rem;
margin: -1rem -1rem 0 -1rem; margin: -1rem -1rem 0 -1rem;
padding: 0.5rem 1rem 0.5rem 1rem; padding: 0.5rem 1rem 0.5rem 1rem;
} }

View File

@ -12,7 +12,7 @@ class ClientClass {
try { try {
msg = await res.json() msg = await res.json()
} catch (e) { } catch (e) {
throw new SimpleError(res.status, `🛑 ${res.status} ${res.statusText}`) throw new SimpleError(res.status, `HTTP ${res.status} ${res.statusText}`)
} }
if ('error' in msg) throw new SimpleError(msg.error.code, msg.error.message) if ('error' in msg) throw new SimpleError(msg.error.code, msg.error.message)
return msg return msg

View File

@ -15,41 +15,13 @@ export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEv
return webSocket return webSocket
} }
export const watchConnect = () => { export const watchConnect = async () => {
if (watchTimeout !== null) {
clearTimeout(watchTimeout)
watchTimeout = null
}
const store = useDocumentStore()
if (store.error !== 'Reconnecting...') store.error = 'Connecting...'
console.log(store.error)
wsWatch = connect(watchUrl, { wsWatch = connect(watchUrl, {
open() { console.log("Connected to", watchUrl)},
message: handleWatchMessage, message: handleWatchMessage,
close: watchReconnect, close: watchReconnect,
}) })
wsWatch.addEventListener("message", event => { await wsWatch
if (store.connected) return
const msg = JSON.parse(event.data)
if ('error' in msg) {
if (msg.error.code === 401) {
store.user.isLoggedIn = false
store.user.isOpenLoginModal = true
} else {
store.error = msg.error.message
}
return
}
if ("server" in msg) {
console.log('Connected to backend', msg)
store.connected = true
reconnectDuration = 500
store.error = ''
if (msg.user) store.login(msg.user.username, msg.user.privileged)
else if (store.isUserLogged) store.logout()
if (!msg.server.public && !msg.user) store.user.isOpenLoginModal = true
}
})
} }
export const watchDisconnect = () => { export const watchDisconnect = () => {
@ -58,24 +30,35 @@ export const watchDisconnect = () => {
wsWatch = null wsWatch = null
} }
let watchTimeout: any = null
const watchReconnect = (event: MessageEvent) => { const watchReconnect = (event: MessageEvent) => {
const store = useDocumentStore() const store = useDocumentStore()
if (store.connected) { if (store.connected) {
console.warn("Disconnected from server", event) console.warn("Disconnected from server", event)
store.connected = false store.connected = false
store.error = 'Reconnecting...'
} }
reconnectDuration = Math.min(5000, reconnectDuration + 500) reconnectDuration = Math.min(5000, reconnectDuration + 500)
// The server closes the websocket after errors, so we need to reopen it // The server closes the websocket after errors, so we need to reopen it
if (watchTimeout !== null) clearTimeout(watchTimeout) setTimeout(() => {
watchTimeout = setTimeout(watchConnect, reconnectDuration) wsWatch = connect(watchUrl, {
message: handleWatchMessage,
close: watchReconnect,
})
console.log("Attempting to reconnect...")
}, reconnectDuration)
} }
const handleWatchMessage = (event: MessageEvent) => { const handleWatchMessage = (event: MessageEvent) => {
const store = useDocumentStore()
const msg = JSON.parse(event.data) const msg = JSON.parse(event.data)
if ('error' in msg) {
if (msg.error.code === 401) {
store.user.isLoggedIn = false
store.user.isOpenLoginModal = true
} else {
store.error = msg.error.message
}
}
switch (true) { switch (true) {
case !!msg.root: case !!msg.root:
handleRootMessage(msg) handleRootMessage(msg)
@ -96,6 +79,9 @@ const handleWatchMessage = (event: MessageEvent) => {
function handleRootMessage({ root }: { root: DirEntry }) { function handleRootMessage({ root }: { root: DirEntry }) {
const store = useDocumentStore() const store = useDocumentStore()
console.log('Watch root', root) console.log('Watch root', root)
reconnectDuration = 500
store.connected = true
store.user.isLoggedIn = true
store.updateRoot(root) store.updateRoot(root)
tree = root tree = root
} }
@ -110,9 +96,8 @@ function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
delete node.dir[elem.name] delete node.dir[elem.name]
break // Deleted elements can't have further children break // Deleted elements can't have further children
} }
if (elem.name) { if (elem.name !== undefined) {
// @ts-ignore // @ts-ignore
console.log(node, elem.name)
node = node.dir[elem.name] ||= {} node = node.dir[elem.name] ||= {}
} }
if (elem.key !== undefined) node.key = elem.key if (elem.key !== undefined) node.key = elem.key

View File

@ -8,8 +8,6 @@ import type {
import { formatSize, formatUnixDate, haystackFormat } from '@/utils' import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { collator } from '@/utils' import { collator } from '@/utils'
import { logoutUser } from '@/repositories/User'
import { watchConnect } from '@/repositories/WS'
type FileData = { id: string; mtime: number; size: number; dir: DirectoryData } type FileData = { id: string; mtime: number; size: number; dir: DirectoryData }
type DirectoryData = { type DirectoryData = {
@ -110,14 +108,10 @@ export const useDocumentStore = defineStore({
this.user.privileged = privileged this.user.privileged = privileged
this.user.isLoggedIn = true this.user.isLoggedIn = true
this.user.isOpenLoginModal = false this.user.isOpenLoginModal = false
if (!this.connected) watchConnect()
},
loginDialog() {
this.user.isOpenLoginModal = true
}, },
async logout() { async logout() {
console.log("Logout") const res = await fetch('/logout', { method: 'POST' })
await logoutUser() if (!res.ok) throw Error(`Logout failed: ${res.statusText}`)
this.$reset() this.$reset()
history.go() // Reload page history.go() // Reload page
} }

View File

@ -1,11 +1,10 @@
import asyncio import asyncio
import typing import typing
from secrets import token_bytes
import msgspec import msgspec
from sanic import Blueprint from sanic import Blueprint
from cista import __version__, config, watching from cista import watching
from cista.fileio import FileServer from cista.fileio import FileServer
from cista.protocol import ControlTypes, FileRange, StatusMsg from cista.protocol import ControlTypes, FileRange, StatusMsg
from cista.util.apphelpers import asend, websocket_wrapper from cista.util.apphelpers import asend, websocket_wrapper
@ -84,27 +83,9 @@ async def control(req, ws):
@bp.websocket("watch") @bp.websocket("watch")
@websocket_wrapper @websocket_wrapper
async def watch(req, ws): async def watch(req, ws):
await ws.send(
msgspec.json.encode(
{
"server": {
"name": "Cista", # Should be configurable
"version": __version__,
"public": config.config.public,
},
"user": {
"username": req.ctx.username,
"privileged": req.ctx.user.privileged,
}
if req.ctx.user
else None,
}
).decode()
)
uuid = token_bytes(16)
try: try:
with watching.tree_lock: with watching.tree_lock:
q = watching.pubsub[uuid] = asyncio.Queue() q = watching.pubsub[ws] = asyncio.Queue()
# Init with disk usage and full tree # Init with disk usage and full tree
await ws.send(watching.format_du()) await ws.send(watching.format_du())
await ws.send(watching.format_tree()) await ws.send(watching.format_tree())
@ -112,4 +93,4 @@ async def watch(req, ws):
while True: while True:
await ws.send(await q.get()) await ws.send(await q.get())
finally: finally:
del watching.pubsub[uuid] del watching.pubsub[ws]

View File

@ -38,10 +38,8 @@ async def main_stop(app, loop):
async def use_session(req): async def use_session(req):
req.ctx.session = session.get(req) req.ctx.session = session.get(req)
try: try:
req.ctx.username = req.ctx.session["username"]
req.ctx.user = config.config.users[req.ctx.session["username"]] # type: ignore req.ctx.user = config.config.users[req.ctx.session["username"]] # type: ignore
except (AttributeError, KeyError, TypeError): except (AttributeError, KeyError, TypeError):
req.ctx.username = None
req.ctx.user = None req.ctx.user = None
# CSRF protection # CSRF protection
if req.method == "GET" and req.headers.upgrade != "websocket": if req.method == "GET" and req.headers.upgrade != "websocket":

View File

@ -45,7 +45,7 @@ class Rm(ControlBase):
sel = [root / filename.sanitize(p) for p in self.sel] sel = [root / filename.sanitize(p) for p in self.sel]
for p in sel: for p in sel:
if p.is_dir(): if p.is_dir():
shutil.rmtree(p) shutil.rmtree(p, ignore_errors=True)
else: else:
p.unlink() p.unlink()
@ -147,9 +147,9 @@ DirList = dict[str, FileEntry | DirEntry]
class UpdateEntry(msgspec.Struct, omit_defaults=True): class UpdateEntry(msgspec.Struct, omit_defaults=True):
"""Updates the named entry in the tree. Fields that are set replace old values. A list of entries recurses directories.""" """Updates the named entry in the tree. Fields that are set replace old values. A list of entries recurses directories."""
name: str name: str = ""
key: str
deleted: bool = False deleted: bool = False
key: str | None = None
size: int | None = None size: int | None = None
mtime: int | None = None mtime: int | None = None
dir: DirList | None = None dir: DirList | None = None

View File

@ -6,7 +6,6 @@ from pathlib import Path, PurePosixPath
import inotify.adapters import inotify.adapters
import msgspec import msgspec
from sanic.log import logging
from cista import config from cista import config
from cista.fileio import fuid from cista.fileio import fuid
@ -30,7 +29,7 @@ disk_usage = None
def watcher_thread(loop): def watcher_thread(loop):
global disk_usage, rootpath global disk_usage
while True: while True:
rootpath = config.config.path rootpath = config.config.path
@ -39,7 +38,6 @@ def watcher_thread(loop):
with tree_lock: with tree_lock:
# Initialize the tree from filesystem # Initialize the tree from filesystem
tree[""] = walk(rootpath) tree[""] = walk(rootpath)
print(" ".join(tree[""].dir.keys()))
msg = format_tree() msg = format_tree()
if msg != old: if msg != old:
asyncio.run_coroutine_threadsafe(broadcast(msg), loop) asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
@ -69,8 +67,8 @@ def watcher_thread(loop):
try: try:
update(path.relative_to(rootpath), loop) update(path.relative_to(rootpath), loop)
except Exception as e: except Exception as e:
print("Watching error", e, path, rootpath) print("Watching error", e)
raise break
i = None # Free the inotify object i = None # Free the inotify object
@ -122,8 +120,6 @@ def walk(path: Path) -> DirEntry | FileEntry | None:
def update(relpath: PurePosixPath, loop): def update(relpath: PurePosixPath, loop):
"""Called by inotify updates, check the filesystem and broadcast any changes.""" """Called by inotify updates, check the filesystem and broadcast any changes."""
if rootpath is None or relpath is None:
print("ERROR", rootpath, relpath)
new = walk(rootpath / relpath) new = walk(rootpath / relpath)
with tree_lock: with tree_lock:
update = update_internal(relpath, new) update = update_internal(relpath, new)
@ -164,7 +160,7 @@ def update_internal(
# Update parents # Update parents
update = [] update = []
for name, entry in elems[:-1]: for name, entry in elems[:-1]:
u = UpdateEntry(name, entry.key) u = UpdateEntry(name)
if szdiff: if szdiff:
entry.size += szdiff entry.size += szdiff
u.size = entry.size u.size = entry.size
@ -174,14 +170,14 @@ def update_internal(
# The last element is the one that changed # The last element is the one that changed
name, entry = elems[-1] name, entry = elems[-1]
parent = elems[-2][1] if len(elems) > 1 else tree parent = elems[-2][1] if len(elems) > 1 else tree
u = UpdateEntry(name, new.key if new else entry.key) u = UpdateEntry(name)
if new: if new:
parent[name] = new parent[name] = new
if u.size != new.size: if u.size != new.size:
u.size = new.size u.size = new.size
if u.mtime != new.mtime: if u.mtime != new.mtime:
u.mtime = new.mtime u.mtime = new.mtime
if isinstance(new, DirEntry) and u.dir != new.dir: if isinstance(new, DirEntry) and u.dir == new.dir:
u.dir = new.dir u.dir = new.dir
else: else:
del parent[name] del parent[name]
@ -191,12 +187,8 @@ def update_internal(
async def broadcast(msg): async def broadcast(msg):
try: for queue in pubsub.values():
for queue in pubsub.values(): await queue.put_nowait(msg)
queue.put_nowait(msg)
except Exception:
# Log because asyncio would silently eat the error
logging.exception("Broadcast error")
async def start(app, loop): async def start(app, loop):