Favicon, title, automatic & manual server naming (#2)
Server name may be set in config file. If unset, backend uses the folder name being served. This is shown in page title for site root, and subfolder names are also now shown. New icon of Droppy icon, changing only the color. Reviewed-on: #2
This commit is contained in:
parent
e26cb8f70a
commit
fb03fa5430
|
@ -1,9 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang=en>
|
<html lang=en>
|
||||||
<meta charset=UTF-8>
|
<meta charset=UTF-8>
|
||||||
<title>Cista</title>
|
<title>Cista Storage</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/src/assets/logo.svg">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,241 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<title>Storage</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
max-width: 100ch;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1em;
|
|
||||||
background-color: #333;
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
text-align: right;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
td:first-child {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div>
|
|
||||||
<h2>Quick file upload</h2>
|
|
||||||
<p>Uses parallel WebSocket connections for increased bandwidth /api/upload</p>
|
|
||||||
<input type=file id=fileInput>
|
|
||||||
<progress id=progressBar value=0 max=1></progress>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>Files</h2>
|
|
||||||
<ul id=file_list></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let files = {}
|
|
||||||
let flatfiles = {}
|
|
||||||
|
|
||||||
function createWatchSocket() {
|
|
||||||
const wsurl = new URL("/api/watch", location.href.replace(/^http/, 'ws'))
|
|
||||||
const ws = new WebSocket(wsurl)
|
|
||||||
ws.onmessage = event => {
|
|
||||||
msg = JSON.parse(event.data)
|
|
||||||
if (msg.update) {
|
|
||||||
tree_update(msg.update)
|
|
||||||
file_list(files)
|
|
||||||
} else {
|
|
||||||
console.log("Unkonwn message from watch socket", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createWatchSocket()
|
|
||||||
|
|
||||||
function tree_update(msg) {
|
|
||||||
console.log("Tree update", msg)
|
|
||||||
let node = files
|
|
||||||
for (const elem of msg) {
|
|
||||||
if (elem.deleted) {
|
|
||||||
const p = node.dir[elem.name].path
|
|
||||||
delete node.dir[elem.name]
|
|
||||||
delete flatfiles[p]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (elem.name !== undefined) node = node.dir[elem.name] ||= {}
|
|
||||||
if (elem.size !== undefined) node.size = elem.size
|
|
||||||
if (elem.mtime !== undefined) node.mtime = elem.mtime
|
|
||||||
if (elem.dir !== undefined) node.dir = elem.dir
|
|
||||||
}
|
|
||||||
// Update paths and flatfiles
|
|
||||||
files.path = "/"
|
|
||||||
const nodes = [files]
|
|
||||||
flatfiles = {}
|
|
||||||
while (node = nodes.pop()) {
|
|
||||||
flatfiles[node.path] = node
|
|
||||||
if (node.dir === undefined) continue
|
|
||||||
for (const name of Object.keys(node.dir)) {
|
|
||||||
const child = node.dir[name]
|
|
||||||
child.path = node.path + name + (child.dir === undefined ? "" : "/")
|
|
||||||
nodes.push(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
|
||||||
|
|
||||||
const compare_path = (a, b) => collator.compare(a.path, b.path)
|
|
||||||
const compare_time = (a, b) => a.mtime > b.mtime
|
|
||||||
|
|
||||||
function file_list(files) {
|
|
||||||
const table = document.getElementById("file_list")
|
|
||||||
const sorted = Object.values(flatfiles).sort(compare_time)
|
|
||||||
table.innerHTML = ""
|
|
||||||
for (const f of sorted) {
|
|
||||||
const {path, size, mtime} = f
|
|
||||||
const tr = document.createElement("tr")
|
|
||||||
const name_td = document.createElement("td")
|
|
||||||
const size_td = document.createElement("td")
|
|
||||||
const mtime_td = document.createElement("td")
|
|
||||||
const a = document.createElement("a")
|
|
||||||
table.appendChild(tr)
|
|
||||||
tr.appendChild(name_td)
|
|
||||||
tr.appendChild(size_td)
|
|
||||||
tr.appendChild(mtime_td)
|
|
||||||
name_td.appendChild(a)
|
|
||||||
size_td.textContent = size
|
|
||||||
mtime_td.textContent = formatUnixDate(mtime)
|
|
||||||
a.textContent = path
|
|
||||||
a.href = `/files${path}`
|
|
||||||
/*a.onclick = event => {
|
|
||||||
if (window.showSaveFilePicker) {
|
|
||||||
event.preventDefault()
|
|
||||||
download_ws(name, size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a.download = ""*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUnixDate(t) {
|
|
||||||
const date = new Date(t * 1000)
|
|
||||||
const now = new Date()
|
|
||||||
const diff = date - now
|
|
||||||
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
||||||
|
|
||||||
if (Math.abs(diff) <= 60000) {
|
|
||||||
return formatter.format(Math.round(diff / 1000), 'second')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(diff) <= 3600000) {
|
|
||||||
return formatter.format(Math.round(diff / 60000), 'minute')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(diff) <= 86400000) {
|
|
||||||
return formatter.format(Math.round(diff / 3600000), 'hour')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(diff) <= 604800000) {
|
|
||||||
return formatter.format(Math.round(diff / 86400000), 'day')
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download_ws(name, size) {
|
|
||||||
const fh = await window.showSaveFilePicker({
|
|
||||||
suggestedName: name,
|
|
||||||
})
|
|
||||||
const writer = await fh.createWritable()
|
|
||||||
writer.truncate(size)
|
|
||||||
const wsurl = new URL("/api/download", location.href.replace(/^http/, 'ws'))
|
|
||||||
const ws = new WebSocket(wsurl)
|
|
||||||
let pos = 0
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log("Downloading over WebSocket", name, size)
|
|
||||||
ws.send(JSON.stringify({name, start: 0, end: size, size}))
|
|
||||||
}
|
|
||||||
ws.onmessage = event => {
|
|
||||||
if (typeof event.data === 'string') {
|
|
||||||
const msg = JSON.parse(event.data)
|
|
||||||
console.log("Download finished", msg)
|
|
||||||
ws.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log("Received chunk", name, pos, pos + event.data.size)
|
|
||||||
pos += event.data.size
|
|
||||||
writer.write(event.data)
|
|
||||||
}
|
|
||||||
ws.onclose = () => {
|
|
||||||
if (pos < size) {
|
|
||||||
console.log("Download aborted", name, pos)
|
|
||||||
writer.truncate(pos)
|
|
||||||
}
|
|
||||||
writer.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileInput = document.getElementById("fileInput")
|
|
||||||
const progress = document.getElementById("progressBar")
|
|
||||||
const numConnections = 2
|
|
||||||
const chunkSize = 1<<20
|
|
||||||
const wsConnections = new Set()
|
|
||||||
|
|
||||||
//for (let i = 0; i < numConnections; i++) createUploadWS()
|
|
||||||
|
|
||||||
function createUploadWS() {
|
|
||||||
const wsurl = new URL("/api/upload", location.href.replace(/^http/, 'ws'))
|
|
||||||
const ws = new WebSocket(wsurl)
|
|
||||||
ws.binaryType = 'arraybuffer'
|
|
||||||
ws.onopen = () => {
|
|
||||||
wsConnections.add(ws)
|
|
||||||
console.log("Upload socket connected")
|
|
||||||
}
|
|
||||||
ws.onmessage = event => {
|
|
||||||
msg = JSON.parse(event.data)
|
|
||||||
if (msg.written) progress.value += +msg.written
|
|
||||||
else console.log(`Error: ${msg.error}`)
|
|
||||||
}
|
|
||||||
ws.onclose = () => {
|
|
||||||
wsConnections.delete(ws)
|
|
||||||
console.log("Upload socket disconnected, reconnecting...")
|
|
||||||
setTimeout(createUploadWS, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load(file, start, end) {
|
|
||||||
const reader = new FileReader()
|
|
||||||
const load = new Promise(resolve => reader.onload = resolve)
|
|
||||||
reader.readAsArrayBuffer(file.slice(start, end))
|
|
||||||
const event = await load
|
|
||||||
return event.target.result
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendChunk(file, start, end, ws) {
|
|
||||||
const chunk = await load(file, start, end)
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
start: start,
|
|
||||||
end: end
|
|
||||||
}))
|
|
||||||
ws.send(chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener("change", async function() {
|
|
||||||
const file = this.files[0]
|
|
||||||
const numChunks = Math.ceil(file.size / chunkSize)
|
|
||||||
progress.value = 0
|
|
||||||
progress.max = file.size
|
|
||||||
|
|
||||||
console.log(wsConnections)
|
|
||||||
for (let i = 0; i < numChunks; i++) {
|
|
||||||
const ws = Array.from(wsConnections)[i % wsConnections.size]
|
|
||||||
const start = i * chunkSize
|
|
||||||
const end = Math.min(file.size, start + chunkSize)
|
|
||||||
const res = await sendChunk(file, start, end, ws)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
2
cista-front/public/robots.txt
Normal file
2
cista-front/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
|
@ -15,7 +15,7 @@
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import type HeaderMain from '@/components/HeaderMain.vue'
|
import type HeaderMain from '@/components/HeaderMain.vue'
|
||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||||
import { watchConnect, watchDisconnect } from '@/repositories/WS'
|
import { watchConnect, watchDisconnect } from '@/repositories/WS'
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
|
|
||||||
|
@ -35,6 +35,9 @@ const path: ComputedRef<Path> = computed(() => {
|
||||||
pathList
|
pathList
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
watchEffect(() => {
|
||||||
|
document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage'
|
||||||
|
})
|
||||||
onMounted(watchConnect)
|
onMounted(watchConnect)
|
||||||
onUnmounted(watchDisconnect)
|
onUnmounted(watchDisconnect)
|
||||||
// Update human-readable x seconds ago messages from mtimes
|
// Update human-readable x seconds ago messages from mtimes
|
||||||
|
|
1
cista-front/src/assets/logo.svg
Normal file
1
cista-front/src/assets/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><rect width="512" height="512" fill="#f80" rx="64" ry="64"/><path fill="#fff" d="M381 298h-84V167h-66L339 35l108 132h-66zm-168-84h-84v131H63l108 132 108-132h-66z"/></svg>
|
After Width: | Height: | Size: 258 B |
|
@ -42,6 +42,7 @@ export const watchConnect = () => {
|
||||||
}
|
}
|
||||||
if ("server" in msg) {
|
if ("server" in msg) {
|
||||||
console.log('Connected to backend', msg)
|
console.log('Connected to backend', msg)
|
||||||
|
store.server = msg.server
|
||||||
store.connected = true
|
store.connected = true
|
||||||
reconnectDuration = 500
|
reconnectDuration = 500
|
||||||
store.error = ''
|
store.error = ''
|
||||||
|
|
|
@ -33,6 +33,7 @@ export const useDocumentStore = defineStore({
|
||||||
fileExplorer: null,
|
fileExplorer: null,
|
||||||
error: '' as string,
|
error: '' as string,
|
||||||
connected: false,
|
connected: false,
|
||||||
|
server: {} as Record<string, any>,
|
||||||
user: {
|
user: {
|
||||||
username: '',
|
username: '',
|
||||||
privileged: false,
|
privileged: false,
|
||||||
|
|
|
@ -89,7 +89,7 @@ async def watch(req, ws):
|
||||||
msgspec.json.encode(
|
msgspec.json.encode(
|
||||||
{
|
{
|
||||||
"server": {
|
"server": {
|
||||||
"name": "Cista", # Should be configurable
|
"name": config.config.name or config.config.path.name,
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"public": config.config.public,
|
"public": config.config.public,
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,7 @@ class Config(msgspec.Struct):
|
||||||
listen: str
|
listen: str
|
||||||
secret: str = secrets.token_hex(12)
|
secret: str = secrets.token_hex(12)
|
||||||
public: bool = False
|
public: bool = False
|
||||||
|
name: str = ""
|
||||||
users: dict[str, User] = {}
|
users: dict[str, User] = {}
|
||||||
links: dict[str, Link] = {}
|
links: dict[str, Link] = {}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user