Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
f38bb4bab9 | |||
26f9bef087 | |||
634dabe52d | |||
a383358369 | |||
369dc3ecaf | |||
0cf9c254e5 | |||
58b9dd3dd4 |
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -61,6 +62,7 @@ def _main():
|
|||||||
path = None
|
path = None
|
||||||
_confdir(args)
|
_confdir(args)
|
||||||
exists = config.conffile.exists()
|
exists = config.conffile.exists()
|
||||||
|
print(config.conffile, exists)
|
||||||
import_droppy = args["--import-droppy"]
|
import_droppy = args["--import-droppy"]
|
||||||
necessary_opts = exists or import_droppy or path
|
necessary_opts = exists or import_droppy or path
|
||||||
if not necessary_opts:
|
if not necessary_opts:
|
||||||
@ -117,7 +119,8 @@ def _confdir(args):
|
|||||||
raise ValueError("Config path is not a directory")
|
raise ValueError("Config path is not a directory")
|
||||||
# Accidentally pointed to the db.toml, use parent
|
# Accidentally pointed to the db.toml, use parent
|
||||||
confdir = confdir.parent
|
confdir = confdir.parent
|
||||||
config.conffile = confdir / config.conffile.name
|
os.environ["CISTA_HOME"] = confdir.as_posix()
|
||||||
|
config.init_confdir() # Uses environ if available
|
||||||
|
|
||||||
|
|
||||||
def _user(args):
|
def _user(args):
|
||||||
|
@ -3,6 +3,7 @@ import datetime
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import threading
|
import threading
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from multiprocessing import cpu_count
|
||||||
from pathlib import Path, PurePath, PurePosixPath
|
from pathlib import Path, PurePath, PurePosixPath
|
||||||
from stat import S_IFDIR, S_IFREG
|
from stat import S_IFDIR, S_IFREG
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@ -14,6 +15,7 @@ from blake3 import blake3
|
|||||||
from sanic import Blueprint, Sanic, empty, raw, redirect
|
from sanic import Blueprint, Sanic, empty, raw, redirect
|
||||||
from sanic.exceptions import Forbidden, NotFound
|
from sanic.exceptions import Forbidden, NotFound
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
|
from setproctitle import setproctitle
|
||||||
from stream_zip import ZIP_AUTO, stream_zip
|
from stream_zip import ZIP_AUTO, stream_zip
|
||||||
|
|
||||||
from cista import auth, config, preview, session, watching
|
from cista import auth, config, preview, session, watching
|
||||||
@ -30,11 +32,16 @@ app.blueprint(bp)
|
|||||||
app.exception(Exception)(handle_sanic_exception)
|
app.exception(Exception)(handle_sanic_exception)
|
||||||
|
|
||||||
|
|
||||||
|
setproctitle("cista-main")
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
@app.before_server_start
|
||||||
async def main_start(app, loop):
|
async def main_start(app, loop):
|
||||||
config.load_config()
|
config.load_config()
|
||||||
|
setproctitle(f"cista {config.config.path.name}")
|
||||||
|
workers = max(2, min(8, cpu_count()))
|
||||||
app.ctx.threadexec = ThreadPoolExecutor(
|
app.ctx.threadexec = ThreadPoolExecutor(
|
||||||
max_workers=8, thread_name_prefix="cista-ioworker"
|
max_workers=workers, thread_name_prefix="cista-ioworker"
|
||||||
)
|
)
|
||||||
await watching.start(app, loop)
|
await watching.start(app, loop)
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
|
from contextlib import suppress
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
@ -33,7 +35,23 @@ class Link(msgspec.Struct, omit_defaults=True):
|
|||||||
|
|
||||||
|
|
||||||
config = None
|
config = None
|
||||||
conffile = Path.home() / ".local/share/cista/db.toml"
|
conffile = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_confdir():
|
||||||
|
if p := os.environ.get("CISTA_HOME"):
|
||||||
|
home = Path(p)
|
||||||
|
else:
|
||||||
|
xdg = os.environ.get("XDG_CONFIG_HOME")
|
||||||
|
home = (
|
||||||
|
Path(xdg).expanduser() / "cista" if xdg else Path.home() / ".config/cista"
|
||||||
|
)
|
||||||
|
if not home.is_dir():
|
||||||
|
home.mkdir(parents=True, exist_ok=True)
|
||||||
|
home.chmod(0o700)
|
||||||
|
|
||||||
|
global conffile
|
||||||
|
conffile = home / "db.toml"
|
||||||
|
|
||||||
|
|
||||||
def derived_secret(*params, len=8) -> bytes:
|
def derived_secret(*params, len=8) -> bytes:
|
||||||
@ -61,8 +79,8 @@ def dec_hook(typ, obj):
|
|||||||
|
|
||||||
def config_update(modify):
|
def config_update(modify):
|
||||||
global config
|
global config
|
||||||
if not conffile.exists():
|
if conffile is None:
|
||||||
conffile.parent.mkdir(parents=True, exist_ok=True)
|
init_confdir()
|
||||||
tmpname = conffile.with_suffix(".tmp")
|
tmpname = conffile.with_suffix(".tmp")
|
||||||
try:
|
try:
|
||||||
f = tmpname.open("xb")
|
f = tmpname.open("xb")
|
||||||
@ -76,10 +94,6 @@ def config_update(modify):
|
|||||||
old = conffile.read_bytes()
|
old = conffile.read_bytes()
|
||||||
c = msgspec.toml.decode(old, type=Config, dec_hook=dec_hook)
|
c = msgspec.toml.decode(old, type=Config, dec_hook=dec_hook)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# No existing config file, make sure we have a folder...
|
|
||||||
confdir = conffile.parent
|
|
||||||
confdir.mkdir(parents=True, exist_ok=True)
|
|
||||||
confdir.chmod(0o700)
|
|
||||||
old = b""
|
old = b""
|
||||||
c = None
|
c = None
|
||||||
c = modify(c)
|
c = modify(c)
|
||||||
@ -92,7 +106,9 @@ def config_update(modify):
|
|||||||
f.write(new)
|
f.write(new)
|
||||||
f.close()
|
f.close()
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
conffile.unlink() # Windows doesn't support atomic replace
|
# Windows doesn't support atomic replace
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
conffile.unlink()
|
||||||
tmpname.rename(conffile) # Atomic replace
|
tmpname.rename(conffile) # Atomic replace
|
||||||
except:
|
except:
|
||||||
f.close()
|
f.close()
|
||||||
@ -120,6 +136,8 @@ def modifies_config(modify):
|
|||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
global config
|
global config
|
||||||
|
if conffile is None:
|
||||||
|
init_confdir()
|
||||||
config = msgspec.toml.decode(conffile.read_bytes(), type=Config, dec_hook=dec_hook)
|
config = msgspec.toml.decode(conffile.read_bytes(), type=Config, dec_hook=dec_hook)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import gc
|
||||||
import io
|
import io
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@ -17,6 +18,8 @@ from sanic.log import logger
|
|||||||
from cista import config
|
from cista import config
|
||||||
from cista.util.filename import sanitize
|
from cista.util.filename import sanitize
|
||||||
|
|
||||||
|
DISPLAYMATRIX = av.stream.SideData.DISPLAYMATRIX
|
||||||
|
|
||||||
bp = Blueprint("preview", url_prefix="/preview")
|
bp = Blueprint("preview", url_prefix="/preview")
|
||||||
|
|
||||||
|
|
||||||
@ -96,19 +99,19 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
|||||||
def process_video(path, *, maxsize, quality):
|
def process_video(path, *, maxsize, quality):
|
||||||
with av.open(str(path)) as container:
|
with av.open(str(path)) as container:
|
||||||
stream = container.streams.video[0]
|
stream = container.streams.video[0]
|
||||||
rotation = (
|
|
||||||
stream.side_data
|
|
||||||
and stream.side_data.get(av.stream.SideData.DISPLAYMATRIX)
|
|
||||||
or 0
|
|
||||||
)
|
|
||||||
stream.codec_context.skip_frame = "NONKEY"
|
stream.codec_context.skip_frame = "NONKEY"
|
||||||
|
rot = stream.side_data and stream.side_data.get(DISPLAYMATRIX) or 0
|
||||||
container.seek(container.duration // 8)
|
container.seek(container.duration // 8)
|
||||||
frame = next(container.decode(stream))
|
img = next(container.decode(stream)).to_image()
|
||||||
img = frame.to_image()
|
del stream
|
||||||
|
|
||||||
img.thumbnail((maxsize, maxsize))
|
img.thumbnail((maxsize, maxsize))
|
||||||
imgdata = io.BytesIO()
|
imgdata = io.BytesIO()
|
||||||
if rotation:
|
if rot:
|
||||||
img = img.rotate(rotation, expand=True)
|
img = img.rotate(rot, expand=True)
|
||||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
img.save(imgdata, format="webp", quality=quality, method=4)
|
||||||
return imgdata.getvalue()
|
del img
|
||||||
|
ret = imgdata.getvalue()
|
||||||
|
del imgdata
|
||||||
|
gc.collect()
|
||||||
|
return ret
|
||||||
|
@ -26,7 +26,6 @@ def run(*, dev=False):
|
|||||||
motd=False,
|
motd=False,
|
||||||
dev=dev,
|
dev=dev,
|
||||||
auto_reload=dev,
|
auto_reload=dev,
|
||||||
reload_dir={confdir},
|
|
||||||
access_log=True,
|
access_log=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
if dev:
|
if dev:
|
||||||
|
BIN
docs/cista.webp
BIN
docs/cista.webp
Binary file not shown.
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 40 KiB |
2
frontend/.npmrc
Normal file
2
frontend/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
audit=false
|
||||||
|
fund=false
|
@ -12,6 +12,9 @@
|
|||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@imengyu/vue3-context-menu": "^1.3.3",
|
"@imengyu/vue3-context-menu": "^1.3.3",
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
@ -21,7 +24,6 @@
|
|||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0",
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"unplugin-vue-components": "^0.25.2",
|
"unplugin-vue-components": "^0.25.2",
|
||||||
"vite-plugin-rewrite-all": "^1.0.1",
|
|
||||||
"vite-svg-loader": "^4.0.0",
|
"vite-svg-loader": "^4.0.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4"
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
<main>
|
<main>
|
||||||
<RouterView :path="path.pathList" :query="path.query" />
|
<RouterView :path="path.pathList" :query="path.query" />
|
||||||
</main>
|
</main>
|
||||||
|
<footer>
|
||||||
|
<TransferBar :status=store.uprogress @cancel=store.cancelUploads class=upload />
|
||||||
|
<TransferBar :status=store.dprogress @cancel=store.cancelDownloads class=download />
|
||||||
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SvgButton name="download" data-tooltip="Download" @click="download" />
|
<SvgButton name="download" data-tooltip="Download" @click="download" />
|
||||||
<TransferBar :status=progress @cancel=cancelDownloads />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -26,22 +25,22 @@ const status_init = {
|
|||||||
filepos: 0,
|
filepos: 0,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
}
|
}
|
||||||
const progress = reactive({...status_init})
|
store.dprogress = {...status_init}
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (Date.now() - progress.tlast > 3000) {
|
if (Date.now() - store.dprogress.tlast > 3000) {
|
||||||
// Reset
|
// Reset
|
||||||
progress.statbytes = 0
|
store.dprogress.statbytes = 0
|
||||||
progress.statdur = 1
|
store.dprogress.statdur = 1
|
||||||
} else {
|
} else {
|
||||||
// Running average by decay
|
// Running average by decay
|
||||||
progress.statbytes *= .9
|
store.dprogress.statbytes *= .9
|
||||||
progress.statdur *= .9
|
store.dprogress.statdur *= .9
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
const statReset = () => {
|
const statReset = () => {
|
||||||
Object.assign(progress, status_init)
|
Object.assign(store.dprogress, status_init)
|
||||||
progress.t0 = Date.now()
|
store.dprogress.t0 = Date.now()
|
||||||
progress.tlast = progress.t0 + 1
|
store.dprogress.tlast = store.dprogress.t0 + 1
|
||||||
}
|
}
|
||||||
const cancelDownloads = () => {
|
const cancelDownloads = () => {
|
||||||
location.reload() // FIXME
|
location.reload() // FIXME
|
||||||
@ -61,9 +60,9 @@ const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandl
|
|||||||
console.log('Downloading to filesystem', sel.recursive)
|
console.log('Downloading to filesystem', sel.recursive)
|
||||||
for (const [rel, full, doc] of sel.recursive) {
|
for (const [rel, full, doc] of sel.recursive) {
|
||||||
if (doc.dir) continue
|
if (doc.dir) continue
|
||||||
progress.files.push(rel)
|
store.dprogress.files.push(rel)
|
||||||
++progress.filecount
|
++store.dprogress.filecount
|
||||||
progress.total += doc.size
|
store.dprogress.total += doc.size
|
||||||
}
|
}
|
||||||
for (const [rel, full, doc] of sel.recursive) {
|
for (const [rel, full, doc] of sel.recursive) {
|
||||||
// Create any missing directories
|
// Create any missing directories
|
||||||
@ -73,6 +72,7 @@ const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandl
|
|||||||
}
|
}
|
||||||
const r = rel.slice(hdir.length)
|
const r = rel.slice(hdir.length)
|
||||||
for (const dir of r.split('/').slice(0, doc.dir ? undefined : -1)) {
|
for (const dir of r.split('/').slice(0, doc.dir ? undefined : -1)) {
|
||||||
|
if (!dir) continue
|
||||||
hdir += `${dir}/`
|
hdir += `${dir}/`
|
||||||
try {
|
try {
|
||||||
h = await h.getDirectoryHandle(dir.normalize('NFC'), { create: true })
|
h = await h.getDirectoryHandle(dir.normalize('NFC'), { create: true })
|
||||||
@ -101,22 +101,22 @@ const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandl
|
|||||||
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`)
|
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`)
|
||||||
}
|
}
|
||||||
if (res.body) {
|
if (res.body) {
|
||||||
++progress.fileidx
|
++store.dprogress.fileidx
|
||||||
const reader = res.body.getReader()
|
const reader = res.body.getReader()
|
||||||
await writable.truncate(0)
|
await writable.truncate(0)
|
||||||
store.error = "Direct download."
|
store.error = "Direct download."
|
||||||
progress.tlast = Date.now()
|
store.dprogress.tlast = Date.now()
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader.read()
|
const { value, done } = await reader.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
await writable.write(value)
|
await writable.write(value)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const size = value.byteLength
|
const size = value.byteLength
|
||||||
progress.xfer += size
|
store.dprogress.xfer += size
|
||||||
progress.filepos += size
|
store.dprogress.filepos += size
|
||||||
progress.statbytes += size
|
store.dprogress.statbytes += size
|
||||||
progress.statdur += now - progress.tlast
|
store.dprogress.statdur += now - store.dprogress.tlast
|
||||||
progress.tlast = now
|
store.dprogress.tlast = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await writable.close()
|
await writable.close()
|
||||||
|
@ -115,6 +115,7 @@ const rename = (doc: Doc, newName: string) => {
|
|||||||
}
|
}
|
||||||
defineExpose({
|
defineExpose({
|
||||||
newFolder() {
|
newFolder() {
|
||||||
|
console.log("New folder")
|
||||||
const now = Math.floor(Date.now() / 1000)
|
const now = Math.floor(Date.now() / 1000)
|
||||||
editing.value = new Doc({
|
editing.value = new Doc({
|
||||||
loc: loc.value,
|
loc: loc.value,
|
||||||
@ -124,6 +125,7 @@ defineExpose({
|
|||||||
mtime: now,
|
mtime: now,
|
||||||
size: 0,
|
size: 0,
|
||||||
})
|
})
|
||||||
|
store.cursor = editing.value.key
|
||||||
},
|
},
|
||||||
toggleSelectAll() {
|
toggleSelectAll() {
|
||||||
console.log('Select')
|
console.log('Select')
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
<div v-if="props.documents.length || editing" class="gallery" ref="gallery">
|
<div v-if="props.documents.length || editing" class="gallery" ref="gallery">
|
||||||
<GalleryFigure v-if="editing?.key === 'new'" :doc="editing" :key=editing.key :editing="{rename: mkdir, exit}" />
|
<GalleryFigure v-if="editing?.key === 'new'" :doc="editing" :key=editing.key :editing="{rename: mkdir, exit}" />
|
||||||
<template v-for="(doc, index) in documents" :key=doc.key>
|
<template v-for="(doc, index) in documents" :key=doc.key>
|
||||||
<GalleryFigure :doc=doc :editing="editing === doc ? {rename, exit} : null">
|
<GalleryFigure :doc=doc :editing="editing === doc ? {rename, exit} : null" @menu="contextMenu($event, doc)">
|
||||||
<BreadCrumb v-if=showFolderBreadcrumb(index) :path="doc.loc ? doc.loc.split('/') : []" class="folder-change"/>
|
<template v-if=showFolderBreadcrumb(index)>
|
||||||
|
<BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" class="folder-change"/>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</template>
|
||||||
</GalleryFigure>
|
</GalleryFigure>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -67,6 +70,7 @@ defineExpose({
|
|||||||
mtime: now,
|
mtime: now,
|
||||||
size: 0,
|
size: 0,
|
||||||
})
|
})
|
||||||
|
store.cursor = editing.value.key
|
||||||
},
|
},
|
||||||
toggleSelectAll() {
|
toggleSelectAll() {
|
||||||
console.log('Select')
|
console.log('Select')
|
||||||
@ -254,6 +258,9 @@ const contextMenu = (ev: MouseEvent, doc: Doc) => {
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
border-radius: .5em;
|
border-radius: .5em 0 0 .5em;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 0 1000000000 4rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
<MediaPreview ref=m :doc="doc" tabindex=-1 quality="sz=512" class="figcontent" />
|
<MediaPreview ref=m :doc="doc" tabindex=-1 quality="sz=512" class="figcontent" />
|
||||||
<div class="titlespacer"></div>
|
<div class="titlespacer"></div>
|
||||||
<figcaption @click.prevent>
|
<figcaption @click.prevent @contextmenu.prevent="$emit('menu', $event)">
|
||||||
<template v-if="editing">
|
<template v-if="editing">
|
||||||
<FileRenameInput :doc=doc :rename=editing.rename :exit=editing.exit />
|
<FileRenameInput :doc=doc :rename=editing.rename :exit=editing.exit />
|
||||||
</template>
|
</template>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<SvgButton
|
<SvgButton
|
||||||
name="create-folder"
|
name="create-folder"
|
||||||
data-tooltip="New folder"
|
data-tooltip="New folder"
|
||||||
@click="() => store.fileExplorer!.newFolder()"
|
@click="() => { console.log('New', store.fileExplorer); store.fileExplorer!.newFolder(); console.log('Done')}"
|
||||||
/>
|
/>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div class="spacer smallgap"></div>
|
<div class="spacer smallgap"></div>
|
||||||
|
@ -57,13 +57,12 @@ const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value <
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
position: fixed;
|
width: 100%;
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
}
|
||||||
.statustext {
|
.statustext {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 .5em;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
@ -84,4 +83,12 @@ span {
|
|||||||
.position { min-width: 4em }
|
.position { min-width: 4em }
|
||||||
.speed { min-width: 4em }
|
.speed { min-width: 4em }
|
||||||
|
|
||||||
|
.upload .statustext::before {
|
||||||
|
font-size: 1.5em;
|
||||||
|
content: '🔺'
|
||||||
|
}
|
||||||
|
.download .statustext::before {
|
||||||
|
font-size: 1.5em;
|
||||||
|
content: '🔻'
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<template>
|
||||||
|
<input ref="fileInput" @change="uploadHandler" type="file" multiple>
|
||||||
|
<input ref="folderInput" @change="uploadHandler" type="file" webkitdirectory>
|
||||||
|
</template>
|
||||||
|
<SvgButton name="add-file" data-tooltip="Upload files" @click="fileInput.click()" />
|
||||||
|
<SvgButton name="add-folder" data-tooltip="Upload folder" @click="folderInput.click()" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { connect, uploadUrl } from '@/repositories/WS';
|
import { connect, uploadUrl } from '@/repositories/WS';
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
@ -108,50 +117,50 @@ const uprogress_init = {
|
|||||||
filepos: 0,
|
filepos: 0,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
}
|
}
|
||||||
const uprogress = reactive({...uprogress_init})
|
store.uprogress = {...uprogress_init}
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (Date.now() - uprogress.tlast > 3000) {
|
if (Date.now() - store.uprogress.tlast > 3000) {
|
||||||
// Reset
|
// Reset
|
||||||
uprogress.statbytes = 0
|
store.uprogress.statbytes = 0
|
||||||
uprogress.statdur = 1
|
store.uprogress.statdur = 1
|
||||||
} else {
|
} else {
|
||||||
// Running average by decay
|
// Running average by decay
|
||||||
uprogress.statbytes *= .9
|
store.uprogress.statbytes *= .9
|
||||||
uprogress.statdur *= .9
|
store.uprogress.statdur *= .9
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
const statUpdate = ({name, size, start, end}: {name: string, size: number, start: number, end: number}) => {
|
const statUpdate = ({name, size, start, end}: {name: string, size: number, start: number, end: number}) => {
|
||||||
if (name !== uprogress.filename) return // If stats have been reset
|
if (name !== store.uprogress.filename) return // If stats have been reset
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
uprogress.xfer = uprogress.filestart + end
|
store.uprogress.xfer = store.uprogress.filestart + end
|
||||||
uprogress.filepos = end
|
store.uprogress.filepos = end
|
||||||
uprogress.statbytes += end - start
|
store.uprogress.statbytes += end - start
|
||||||
uprogress.statdur += now - uprogress.tlast
|
store.uprogress.statdur += now - store.uprogress.tlast
|
||||||
uprogress.tlast = now
|
store.uprogress.tlast = now
|
||||||
// File finished?
|
// File finished?
|
||||||
if (end === size) {
|
if (end === size) {
|
||||||
uprogress.filestart += size
|
store.uprogress.filestart += size
|
||||||
statNextFile()
|
statNextFile()
|
||||||
if (++uprogress.fileidx >= uprogress.filecount) statReset()
|
if (++store.uprogress.fileidx >= store.uprogress.filecount) statReset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const statNextFile = () => {
|
const statNextFile = () => {
|
||||||
const f = uprogress.files.shift()
|
const f = store.uprogress.files.shift()
|
||||||
if (!f) return statReset()
|
if (!f) return statReset()
|
||||||
uprogress.filepos = 0
|
store.uprogress.filepos = 0
|
||||||
uprogress.filesize = f.file.size
|
store.uprogress.filesize = f.file.size
|
||||||
uprogress.filename = f.cloudName
|
store.uprogress.filename = f.cloudName
|
||||||
}
|
}
|
||||||
const statReset = () => {
|
const statReset = () => {
|
||||||
Object.assign(uprogress, uprogress_init)
|
Object.assign(store.uprogress, uprogress_init)
|
||||||
uprogress.t0 = Date.now()
|
store.uprogress.t0 = Date.now()
|
||||||
uprogress.tlast = uprogress.t0 + 1
|
store.uprogress.tlast = store.uprogress.t0 + 1
|
||||||
}
|
}
|
||||||
const statsAdd = (f: CloudFile[]) => {
|
const statsAdd = (f: CloudFile[]) => {
|
||||||
if (uprogress.files.length === 0) statReset()
|
if (store.uprogress.files.length === 0) statReset()
|
||||||
uprogress.total += f.reduce((a, b) => a + b.file.size, 0)
|
store.uprogress.total += f.reduce((a, b) => a + b.file.size, 0)
|
||||||
uprogress.filecount += f.length
|
store.uprogress.filecount += f.length
|
||||||
uprogress.files = [...uprogress.files, ...f]
|
store.uprogress.files = [...store.uprogress.files, ...f]
|
||||||
statNextFile()
|
statNextFile()
|
||||||
}
|
}
|
||||||
let upqueue = [] as CloudFile[]
|
let upqueue = [] as CloudFile[]
|
||||||
@ -181,7 +190,7 @@ const WSCreate = async () => await new Promise<WebSocket>(resolve => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
ws.sendData = async (data: any) => {
|
ws.sendData = async (data: any) => {
|
||||||
// Wait until the WS is ready to send another message
|
// Wait until the WS is ready to send another message
|
||||||
uprogress.status = "uploading"
|
store.uprogress.status = "uploading"
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
const t = setInterval(() => {
|
const t = setInterval(() => {
|
||||||
if (ws.bufferedAmount > 1<<20) return
|
if (ws.bufferedAmount > 1<<20) return
|
||||||
@ -189,7 +198,7 @@ const WSCreate = async () => await new Promise<WebSocket>(resolve => {
|
|||||||
clearInterval(t)
|
clearInterval(t)
|
||||||
}, 1)
|
}, 1)
|
||||||
})
|
})
|
||||||
uprogress.status = "processing"
|
store.uprogress.status = "processing"
|
||||||
ws.send(data)
|
ws.send(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -210,7 +219,7 @@ const worker = async () => {
|
|||||||
if (f.cloudPos === f.file.size) upqueue.shift()
|
if (f.cloudPos === f.file.size) upqueue.shift()
|
||||||
}
|
}
|
||||||
if (upqueue.length) startWorker()
|
if (upqueue.length) startWorker()
|
||||||
uprogress.status = "idle"
|
store.uprogress.status = "idle"
|
||||||
workerRunning = false
|
workerRunning = false
|
||||||
}
|
}
|
||||||
let workerRunning: any = false
|
let workerRunning: any = false
|
||||||
@ -233,12 +242,3 @@ onUnmounted(() => {
|
|||||||
removeEventListener('drop', uploadHandler)
|
removeEventListener('drop', uploadHandler)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
|
||||||
<template>
|
|
||||||
<input ref="fileInput" @change="uploadHandler" type="file" multiple>
|
|
||||||
<input ref="folderInput" @change="uploadHandler" type="file" webkitdirectory>
|
|
||||||
</template>
|
|
||||||
<SvgButton name="add-file" data-tooltip="Upload files" @click="fileInput.click()" />
|
|
||||||
<SvgButton name="add-folder" data-tooltip="Upload folder" @click="folderInput.click()" />
|
|
||||||
<TransferBar :status=uprogress @cancel=cancelUploads />
|
|
||||||
</template>
|
|
||||||
|
@ -19,6 +19,8 @@ export const useMainStore = defineStore({
|
|||||||
cursor: '' as string,
|
cursor: '' as string,
|
||||||
server: {} as Record<string, any>,
|
server: {} as Record<string, any>,
|
||||||
dialog: '' as '' | 'login' | 'settings',
|
dialog: '' as '' | 'login' | 'settings',
|
||||||
|
uprogress: {} as any,
|
||||||
|
dprogress: {} as any,
|
||||||
prefs: {
|
prefs: {
|
||||||
gallery: false,
|
gallery: false,
|
||||||
sortListing: '' as SortOrder,
|
sortListing: '' as SortOrder,
|
||||||
@ -89,7 +91,13 @@ export const useMainStore = defineStore({
|
|||||||
},
|
},
|
||||||
focusBreadcrumb() {
|
focusBreadcrumb() {
|
||||||
(document.querySelector('.breadcrumb') as HTMLAnchorElement).focus()
|
(document.querySelector('.breadcrumb') as HTMLAnchorElement).focus()
|
||||||
}
|
},
|
||||||
|
cancelDownloads() {
|
||||||
|
location.reload() // FIXME
|
||||||
|
},
|
||||||
|
cancelUploads() {
|
||||||
|
location.reload() // FIXME
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
sortOrder(): SortOrder { return this.query ? this.prefs.sortFiltered : this.prefs.sortListing },
|
sortOrder(): SortOrder { return this.query ? this.prefs.sortFiltered : this.prefs.sortListing },
|
||||||
|
@ -27,7 +27,6 @@ import Router from '@/router/index'
|
|||||||
import { needleFormat, localeIncludes, collator } from '@/utils'
|
import { needleFormat, localeIncludes, collator } from '@/utils'
|
||||||
import { sorted } from '@/utils/docsort'
|
import { sorted } from '@/utils/docsort'
|
||||||
import FileExplorer from '@/components/FileExplorer.vue'
|
import FileExplorer from '@/components/FileExplorer.vue'
|
||||||
import cog from '@/assets/svg/cog.svg'
|
|
||||||
|
|
||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
const fileExplorer = ref()
|
const fileExplorer = ref()
|
||||||
|
@ -4,7 +4,6 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import pluginRewriteAll from 'vite-plugin-rewrite-all'
|
|
||||||
import svgLoader from 'vite-svg-loader'
|
import svgLoader from 'vite-svg-loader'
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ const dev_backend = {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
pluginRewriteAll(),
|
|
||||||
svgLoader(), // import svg files
|
svgLoader(), // import svg files
|
||||||
Components(), // auto import components
|
Components(), // auto import components
|
||||||
],
|
],
|
||||||
|
@ -27,6 +27,7 @@ dependencies = [
|
|||||||
"pyjwt",
|
"pyjwt",
|
||||||
"pymupdf",
|
"pymupdf",
|
||||||
"sanic",
|
"sanic",
|
||||||
|
"setproctitle",
|
||||||
"stream-zip",
|
"stream-zip",
|
||||||
"tomli_w",
|
"tomli_w",
|
||||||
]
|
]
|
||||||
@ -48,7 +49,7 @@ source = "vcs"
|
|||||||
|
|
||||||
[tool.hatch.build]
|
[tool.hatch.build]
|
||||||
artifacts = ["cista/wwwroot"]
|
artifacts = ["cista/wwwroot"]
|
||||||
hooks.custom.path = "scripts/build-frontend.py"
|
targets.sdist.hooks.custom.path = "scripts/build-frontend.py"
|
||||||
hooks.vcs.version-file = "cista/_version.py"
|
hooks.vcs.version-file = "cista/_version.py"
|
||||||
hooks.vcs.template = """
|
hooks.vcs.template = """
|
||||||
# This file is automatically generated by hatch build.
|
# This file is automatically generated by hatch build.
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
# noqa: INP001
|
# noqa: INP001
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from sys import stderr
|
||||||
|
|
||||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||||
|
|
||||||
@ -7,6 +10,18 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|||||||
class CustomBuildHook(BuildHookInterface):
|
class CustomBuildHook(BuildHookInterface):
|
||||||
def initialize(self, version, build_data):
|
def initialize(self, version, build_data):
|
||||||
super().initialize(version, build_data)
|
super().initialize(version, build_data)
|
||||||
print("Building Cista frontend...")
|
stderr.write(">>> Building Cista frontend\n")
|
||||||
subprocess.run("npm install --prefix frontend".split(" "), check=True) # noqa: S603
|
npm = shutil.which("npm")
|
||||||
subprocess.run("npm run build --prefix frontend".split(" "), check=True) # noqa: S603
|
if npm is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"NodeJS `npm` is required for building Cista but it was not found"
|
||||||
|
)
|
||||||
|
# npm --prefix doesn't work on Windows, so we chdir instead
|
||||||
|
os.chdir("frontend")
|
||||||
|
try:
|
||||||
|
stderr.write("### npm install\n")
|
||||||
|
subprocess.run([npm, "install"], check=True) # noqa: S603
|
||||||
|
stderr.write("\n### npm run build\n")
|
||||||
|
subprocess.run([npm, "run", "build"], check=True) # noqa: S603
|
||||||
|
finally:
|
||||||
|
os.chdir("..")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user