Compare commits
3 Commits
54d6ea6332
...
4c51029c9f
Author | SHA1 | Date | |
---|---|---|---|
4c51029c9f | |||
4de2027959 | |||
d5e1304c0d |
@ -3,46 +3,39 @@
|
||||
:root {
|
||||
--primary-color: #000;
|
||||
--primary-background: #ddd;
|
||||
--header-background: #246;
|
||||
--header-background: var(--soft-color);
|
||||
--header-color: #ccc;
|
||||
--input-background: #fff;
|
||||
--input-color: #000;
|
||||
--primary-color: #000;
|
||||
--soft-color: #146;
|
||||
--accent-color: #f80;
|
||||
--transition-time: 0.2s;
|
||||
/* The following are overridden by responsive layouts */
|
||||
--root-font-size: 1rem;
|
||||
--header-font-size: 1rem;
|
||||
--header-height: calc(8 * var(--header-font-size));
|
||||
--header-height: calc(6.5 * var(--header-font-size));
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary-color: #ddd;
|
||||
--primary-background: #003;
|
||||
--primary-background: var(--soft-color);
|
||||
--header-background: #000;
|
||||
--header-color: #ccc;
|
||||
--input-background: var(--soft-color);
|
||||
--input-color: #ddd;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.size,
|
||||
.modified {
|
||||
.modified,
|
||||
.summary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@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) {
|
||||
@media screen and (min-width: 1000px) {
|
||||
:root {
|
||||
--root-font-size: calc(16 * 100vw / 800);
|
||||
--root-font-size: calc(8px + 8 * 100vw / 1000);
|
||||
}
|
||||
header .buttons:has(input[type='search']) > div {
|
||||
display: none;
|
||||
@ -51,20 +44,51 @@
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1600px) and (--webkit-min-device-pixel-ratio: 3) {
|
||||
@media screen and (min-width: 2000px) {
|
||||
:root {
|
||||
--root-font-size: 2rem;
|
||||
--root-font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
/* Low (landscape) screens: smaller header */
|
||||
@media screen and (max-height: 600px) {
|
||||
:root {
|
||||
--header-font-size: calc(16 * 100vh / 600); /* 16px (1rem nominal) at 600px height */
|
||||
--header-font-size: calc(10px + 10 * 100vh / 600); /* 20px 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) {
|
||||
:root {
|
||||
--header-font-size: 0.5rem; /* Don't go smaller than this, no benefit */
|
||||
--header-font-size: 15px; /* Don't go smaller than this, no benefit */
|
||||
--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 {
|
||||
@ -119,6 +143,7 @@
|
||||
}
|
||||
}
|
||||
html {
|
||||
font-size: var(--root-font-size);
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Hide scrollbar for all browsers */
|
||||
@ -198,7 +223,7 @@ main {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.spacer { flex-grow: 1 }
|
||||
.smallgap { margin-left: 2em }
|
||||
.smallgap { flex-shrink: 1; width: 2em }
|
||||
|
||||
[data-tooltip]:hover:after {
|
||||
z-index: 101;
|
||||
@ -235,3 +260,9 @@ main {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.error-message {
|
||||
padding: .5em;
|
||||
font-weight: bold;
|
||||
background: var(--accent-color);
|
||||
color: #000;
|
||||
}
|
||||
|
@ -129,8 +129,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr class="summary">
|
||||
<td colspan="3" class="right">{{props.documents.length}} items shown:</td>
|
||||
<tr class="summary" v-if="props.documents.length > 1">
|
||||
<td colspan="3" class="right">{{props.documents.length}} items</td>
|
||||
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td>
|
||||
<td class="menu"></td>
|
||||
</tr>
|
||||
@ -140,7 +140,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watchEffect, onBeforeUpdate } from 'vue'
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import type { Document } from '@/repositories/Document'
|
||||
import FileRenameInput from './FileRenameInput.vue'
|
||||
@ -442,6 +442,7 @@ table td {
|
||||
}
|
||||
}
|
||||
thead tr {
|
||||
font-size: var(--header-font-size);
|
||||
background: linear-gradient(to bottom, #eee, #fff 30%, #ddd);
|
||||
color: #000;
|
||||
box-shadow: 0 0 .2rem black;
|
||||
@ -510,4 +511,7 @@ tbody .selection input {
|
||||
.loc {
|
||||
color: #888;
|
||||
}
|
||||
.summary {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<nav class="headermain">
|
||||
<div class="buttons">
|
||||
<template v-if="documentStore.error">
|
||||
<div class="error-message" @click="documentStore.error = ''">{{ documentStore.error }}</div>
|
||||
<div class="smallgap"></div>
|
||||
</template>
|
||||
<UploadButton />
|
||||
<SvgButton
|
||||
name="create-folder"
|
||||
@ -23,17 +27,6 @@
|
||||
<SvgButton name="cog" @click="settingsMenu" />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -64,12 +57,16 @@ const toggleSearchInput = () => {
|
||||
|
||||
const settingsMenu = (e: Event) => {
|
||||
// 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({
|
||||
// @ts-ignore
|
||||
x: e.target.getBoundingClientRect().right, y: e.target.getBoundingClientRect().bottom,
|
||||
items: [
|
||||
{ label: "Logout", onClick: () => { documentStore.logout() } },
|
||||
]
|
||||
items,
|
||||
})
|
||||
}
|
||||
|
||||
@ -91,8 +88,8 @@ defineExpose({
|
||||
flex-shrink: 1;
|
||||
}
|
||||
input[type='search'] {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-color);
|
||||
background: var(--input-background);
|
||||
color: var(--input-color);
|
||||
border: 0;
|
||||
border-radius: 0.1em;
|
||||
padding: 0.5em;
|
||||
|
@ -146,7 +146,7 @@ const download = async () => {
|
||||
<style>
|
||||
.select-text {
|
||||
color: var(--accent-color);
|
||||
text-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog v-if="store.user.isOpenLoginModal" title="Authentication required">
|
||||
<ModalDialog v-if="store.user.isOpenLoginModal" title="Authentication required" @blur="store.user.isOpenLoginModal = false">
|
||||
<form @submit.prevent="login">
|
||||
<div class="login-container">
|
||||
<label for="username">Username:</label>
|
||||
@ -7,6 +7,8 @@
|
||||
id="username"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
required
|
||||
v-model="loginForm.username"
|
||||
/>
|
||||
@ -16,12 +18,14 @@
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
required
|
||||
v-model="loginForm.password"
|
||||
/>
|
||||
</div>
|
||||
<h3 v-if="loginForm.error.length > 0" class="error-text">
|
||||
{{ loginForm.error }}
|
||||
<h3 class="error-text">
|
||||
{{ loginForm.error || '\u00A0' }}
|
||||
</h3>
|
||||
<div class="dialog-buttons">
|
||||
<div class="spacer"></div>
|
||||
@ -33,21 +37,13 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { loginUser, logoutUser } from '@/repositories/User'
|
||||
import { loginUser } from '@/repositories/User'
|
||||
import type { ISimpleError } from '@/repositories/Client'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
|
||||
const confirmLoading = ref<boolean>(false)
|
||||
const store = useDocumentStore()
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await logoutUser()
|
||||
} finally {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
@ -59,13 +55,10 @@ const login = async () => {
|
||||
loginForm.error = ''
|
||||
confirmLoading.value = true
|
||||
const msg = await loginUser(loginForm.username, loginForm.password)
|
||||
console.log('Logged in', msg)
|
||||
store.login(msg.username, !!msg.privileged)
|
||||
store.login(msg.data.username, !!msg.data.privileged)
|
||||
} catch (error) {
|
||||
const httpError = error as ISimpleError
|
||||
if (httpError.name) {
|
||||
loginForm.error = httpError.message
|
||||
}
|
||||
loginForm.error = httpError.message || '🛑 Unknown error'
|
||||
} finally {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
@ -87,18 +80,22 @@ const login = async () => {
|
||||
align-items: center;
|
||||
}
|
||||
.button-login {
|
||||
color: #fff;
|
||||
background: var(--soft-color);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border: 0;
|
||||
border-radius: .5rem;
|
||||
padding: .5rem;
|
||||
padding: .5rem 2rem;
|
||||
margin-left: auto;
|
||||
background-color: var(--accent-color);
|
||||
color: var(--primary-color);
|
||||
transition: all var(--transition-time) linear;
|
||||
}
|
||||
.ant-btn-primary:not(:disabled):hover {
|
||||
background-color: var(--blue-color);
|
||||
.button-login:hover, .button-login:focus {
|
||||
background: var(--accent-color);
|
||||
box-shadow: 0 0 .3rem #000;
|
||||
}
|
||||
.error-text {
|
||||
color: var(--red-color);
|
||||
height: 1em;
|
||||
}
|
||||
</style>
|
||||
|
@ -23,15 +23,18 @@ const props = withDefaults(
|
||||
title: ''
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
const show = () => {
|
||||
dialog.value!.showModal()
|
||||
}
|
||||
defineExpose({ show })
|
||||
onMounted(() => {
|
||||
show()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Style for the background */
|
||||
body:has(dialog[open])::before {
|
||||
dialog::backdrop {
|
||||
content: '';
|
||||
display: block;
|
||||
position: fixed;
|
||||
@ -40,7 +43,7 @@ body:has(dialog[open])::before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0008;
|
||||
backdrop-filter: blur(0.2em);
|
||||
backdrop-filter: blur(0.4em);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@ -50,6 +53,7 @@ dialog[open] {
|
||||
color: black;
|
||||
display: block;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0.2rem 0.2rem 1rem #000;
|
||||
padding: 1rem;
|
||||
@ -58,11 +62,13 @@ dialog[open] {
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
dialog[open] > h1 {
|
||||
background: var(--accent-color);
|
||||
color: black;
|
||||
font-size: 1rem;
|
||||
background: var(--soft-color);
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
margin: -1rem -1rem 0 -1rem;
|
||||
padding: 0.5rem 1rem 0.5rem 1rem;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class ClientClass {
|
||||
try {
|
||||
msg = await res.json()
|
||||
} catch (e) {
|
||||
throw new SimpleError(res.status, `HTTP ${res.status} ${res.statusText}`)
|
||||
throw new SimpleError(res.status, `🛑 ${res.status} ${res.statusText}`)
|
||||
}
|
||||
if ('error' in msg) throw new SimpleError(msg.error.code, msg.error.message)
|
||||
return msg
|
||||
|
@ -15,41 +15,21 @@ export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEv
|
||||
return webSocket
|
||||
}
|
||||
|
||||
export const watchConnect = async () => {
|
||||
wsWatch = connect(watchUrl, {
|
||||
open() { console.log("Connected to", watchUrl)},
|
||||
message: handleWatchMessage,
|
||||
close: watchReconnect,
|
||||
})
|
||||
await wsWatch
|
||||
}
|
||||
|
||||
export const watchDisconnect = () => {
|
||||
if (!wsWatch) return
|
||||
wsWatch.close()
|
||||
wsWatch = null
|
||||
}
|
||||
|
||||
const watchReconnect = (event: MessageEvent) => {
|
||||
const store = useDocumentStore()
|
||||
if (store.connected) {
|
||||
console.warn("Disconnected from server", event)
|
||||
store.connected = false
|
||||
export const watchConnect = () => {
|
||||
if (watchTimeout !== null) {
|
||||
clearTimeout(watchTimeout)
|
||||
watchTimeout = null
|
||||
}
|
||||
reconnectDuration = Math.min(5000, reconnectDuration + 500)
|
||||
// The server closes the websocket after errors, so we need to reopen it
|
||||
setTimeout(() => {
|
||||
const store = useDocumentStore()
|
||||
if (store.error !== 'Reconnecting...') store.error = 'Connecting...'
|
||||
console.log(store.error)
|
||||
|
||||
wsWatch = connect(watchUrl, {
|
||||
message: handleWatchMessage,
|
||||
close: watchReconnect,
|
||||
})
|
||||
console.log("Attempting to reconnect...")
|
||||
}, reconnectDuration)
|
||||
}
|
||||
|
||||
|
||||
const handleWatchMessage = (event: MessageEvent) => {
|
||||
const store = useDocumentStore()
|
||||
wsWatch.addEventListener("message", event => {
|
||||
if (store.connected) return
|
||||
const msg = JSON.parse(event.data)
|
||||
if ('error' in msg) {
|
||||
if (msg.error.code === 401) {
|
||||
@ -58,7 +38,44 @@ const handleWatchMessage = (event: MessageEvent) => {
|
||||
} 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 = () => {
|
||||
if (!wsWatch) return
|
||||
wsWatch.close()
|
||||
wsWatch = null
|
||||
}
|
||||
|
||||
let watchTimeout: any = null
|
||||
|
||||
const watchReconnect = (event: MessageEvent) => {
|
||||
const store = useDocumentStore()
|
||||
if (store.connected) {
|
||||
console.warn("Disconnected from server", event)
|
||||
store.connected = false
|
||||
store.error = 'Reconnecting...'
|
||||
}
|
||||
reconnectDuration = Math.min(5000, reconnectDuration + 500)
|
||||
// The server closes the websocket after errors, so we need to reopen it
|
||||
if (watchTimeout !== null) clearTimeout(watchTimeout)
|
||||
watchTimeout = setTimeout(watchConnect, reconnectDuration)
|
||||
}
|
||||
|
||||
|
||||
const handleWatchMessage = (event: MessageEvent) => {
|
||||
const msg = JSON.parse(event.data)
|
||||
switch (true) {
|
||||
case !!msg.root:
|
||||
handleRootMessage(msg)
|
||||
@ -79,9 +96,6 @@ const handleWatchMessage = (event: MessageEvent) => {
|
||||
function handleRootMessage({ root }: { root: DirEntry }) {
|
||||
const store = useDocumentStore()
|
||||
console.log('Watch root', root)
|
||||
reconnectDuration = 500
|
||||
store.connected = true
|
||||
store.user.isLoggedIn = true
|
||||
store.updateRoot(root)
|
||||
tree = root
|
||||
}
|
||||
@ -96,8 +110,9 @@ function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
||||
delete node.dir[elem.name]
|
||||
break // Deleted elements can't have further children
|
||||
}
|
||||
if (elem.name !== undefined) {
|
||||
if (elem.name) {
|
||||
// @ts-ignore
|
||||
console.log(node, elem.name)
|
||||
node = node.dir[elem.name] ||= {}
|
||||
}
|
||||
if (elem.key !== undefined) node.key = elem.key
|
||||
|
@ -8,6 +8,8 @@ import type {
|
||||
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
||||
import { defineStore } from 'pinia'
|
||||
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 DirectoryData = {
|
||||
@ -108,10 +110,14 @@ export const useDocumentStore = defineStore({
|
||||
this.user.privileged = privileged
|
||||
this.user.isLoggedIn = true
|
||||
this.user.isOpenLoginModal = false
|
||||
if (!this.connected) watchConnect()
|
||||
},
|
||||
loginDialog() {
|
||||
this.user.isOpenLoginModal = true
|
||||
},
|
||||
async logout() {
|
||||
const res = await fetch('/logout', { method: 'POST' })
|
||||
if (!res.ok) throw Error(`Logout failed: ${res.statusText}`)
|
||||
console.log("Logout")
|
||||
await logoutUser()
|
||||
this.$reset()
|
||||
history.go() // Reload page
|
||||
}
|
||||
|
25
cista/api.py
25
cista/api.py
@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
import typing
|
||||
from secrets import token_bytes
|
||||
|
||||
import msgspec
|
||||
from sanic import Blueprint
|
||||
|
||||
from cista import watching
|
||||
from cista import __version__, config, watching
|
||||
from cista.fileio import FileServer
|
||||
from cista.protocol import ControlTypes, FileRange, StatusMsg
|
||||
from cista.util.apphelpers import asend, websocket_wrapper
|
||||
@ -83,9 +84,27 @@ async def control(req, ws):
|
||||
@bp.websocket("watch")
|
||||
@websocket_wrapper
|
||||
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:
|
||||
with watching.tree_lock:
|
||||
q = watching.pubsub[ws] = asyncio.Queue()
|
||||
q = watching.pubsub[uuid] = asyncio.Queue()
|
||||
# Init with disk usage and full tree
|
||||
await ws.send(watching.format_du())
|
||||
await ws.send(watching.format_tree())
|
||||
@ -93,4 +112,4 @@ async def watch(req, ws):
|
||||
while True:
|
||||
await ws.send(await q.get())
|
||||
finally:
|
||||
del watching.pubsub[ws]
|
||||
del watching.pubsub[uuid]
|
||||
|
@ -38,8 +38,10 @@ async def main_stop(app, loop):
|
||||
async def use_session(req):
|
||||
req.ctx.session = session.get(req)
|
||||
try:
|
||||
req.ctx.username = req.ctx.session["username"]
|
||||
req.ctx.user = config.config.users[req.ctx.session["username"]] # type: ignore
|
||||
except (AttributeError, KeyError, TypeError):
|
||||
req.ctx.username = None
|
||||
req.ctx.user = None
|
||||
# CSRF protection
|
||||
if req.method == "GET" and req.headers.upgrade != "websocket":
|
||||
|
@ -45,7 +45,7 @@ class Rm(ControlBase):
|
||||
sel = [root / filename.sanitize(p) for p in self.sel]
|
||||
for p in sel:
|
||||
if p.is_dir():
|
||||
shutil.rmtree(p, ignore_errors=True)
|
||||
shutil.rmtree(p)
|
||||
else:
|
||||
p.unlink()
|
||||
|
||||
@ -147,9 +147,9 @@ DirList = dict[str, FileEntry | DirEntry]
|
||||
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."""
|
||||
|
||||
name: str = ""
|
||||
name: str
|
||||
key: str
|
||||
deleted: bool = False
|
||||
key: str | None = None
|
||||
size: int | None = None
|
||||
mtime: int | None = None
|
||||
dir: DirList | None = None
|
||||
|
@ -6,6 +6,7 @@ from pathlib import Path, PurePosixPath
|
||||
|
||||
import inotify.adapters
|
||||
import msgspec
|
||||
from sanic.log import logging
|
||||
|
||||
from cista import config
|
||||
from cista.fileio import fuid
|
||||
@ -29,7 +30,7 @@ disk_usage = None
|
||||
|
||||
|
||||
def watcher_thread(loop):
|
||||
global disk_usage
|
||||
global disk_usage, rootpath
|
||||
|
||||
while True:
|
||||
rootpath = config.config.path
|
||||
@ -38,6 +39,7 @@ def watcher_thread(loop):
|
||||
with tree_lock:
|
||||
# Initialize the tree from filesystem
|
||||
tree[""] = walk(rootpath)
|
||||
print(" ".join(tree[""].dir.keys()))
|
||||
msg = format_tree()
|
||||
if msg != old:
|
||||
asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
|
||||
@ -67,8 +69,8 @@ def watcher_thread(loop):
|
||||
try:
|
||||
update(path.relative_to(rootpath), loop)
|
||||
except Exception as e:
|
||||
print("Watching error", e)
|
||||
break
|
||||
print("Watching error", e, path, rootpath)
|
||||
raise
|
||||
i = None # Free the inotify object
|
||||
|
||||
|
||||
@ -120,6 +122,8 @@ def walk(path: Path) -> DirEntry | FileEntry | None:
|
||||
|
||||
def update(relpath: PurePosixPath, loop):
|
||||
"""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)
|
||||
with tree_lock:
|
||||
update = update_internal(relpath, new)
|
||||
@ -160,7 +164,7 @@ def update_internal(
|
||||
# Update parents
|
||||
update = []
|
||||
for name, entry in elems[:-1]:
|
||||
u = UpdateEntry(name)
|
||||
u = UpdateEntry(name, entry.key)
|
||||
if szdiff:
|
||||
entry.size += szdiff
|
||||
u.size = entry.size
|
||||
@ -170,14 +174,14 @@ def update_internal(
|
||||
# The last element is the one that changed
|
||||
name, entry = elems[-1]
|
||||
parent = elems[-2][1] if len(elems) > 1 else tree
|
||||
u = UpdateEntry(name)
|
||||
u = UpdateEntry(name, new.key if new else entry.key)
|
||||
if new:
|
||||
parent[name] = new
|
||||
if u.size != new.size:
|
||||
u.size = new.size
|
||||
if 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
|
||||
else:
|
||||
del parent[name]
|
||||
@ -187,8 +191,12 @@ def update_internal(
|
||||
|
||||
|
||||
async def broadcast(msg):
|
||||
try:
|
||||
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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user