Frontend included in repository.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from cista._version import __version__
|
||||
|
||||
@@ -3,13 +3,13 @@ from pathlib import Path
|
||||
|
||||
from docopt import docopt
|
||||
|
||||
import cista
|
||||
from cista import app, config, droppy, serve, server80
|
||||
from cista._version import version
|
||||
from cista.util import pwgen
|
||||
|
||||
app, server80.app # Needed for Sanic multiprocessing
|
||||
|
||||
doc = f"""Cista {version} - A file storage for the web.
|
||||
doc = f"""Cista {cista.__version__} - A file storage for the web.
|
||||
|
||||
Usage:
|
||||
cista [-c <confdir>] [-l <host>] [--import-droppy] [--dev] [<path>]
|
||||
|
||||
27
cista/app.py
27
cista/app.py
@@ -3,14 +3,17 @@ from importlib.resources import files
|
||||
|
||||
import msgspec
|
||||
from html5tagger import E
|
||||
from sanic import Forbidden, Sanic, SanicException, errorpages
|
||||
from sanic import Forbidden, Sanic, SanicException, errorpages, raw
|
||||
from sanic.log import logger
|
||||
from sanic.response import html, json, redirect
|
||||
import mimetypes
|
||||
|
||||
from cista import config, session, watching
|
||||
from cista.util import filename
|
||||
from cista.auth import authbp
|
||||
from cista.fileio import FileServer
|
||||
from cista.protocol import ControlBase, ErrorMsg, FileRange, StatusMsg
|
||||
from urllib.parse import unquote
|
||||
|
||||
app = Sanic("cista")
|
||||
fileserver = FileServer()
|
||||
@@ -62,19 +65,15 @@ async def start_fileserver(app, _):
|
||||
async def stop_fileserver(app, _):
|
||||
await fileserver.stop()
|
||||
|
||||
@app.get("/")
|
||||
async def index_page(request):
|
||||
s = config.config.public or session.get(request)
|
||||
if not s:
|
||||
return redirect("/login")
|
||||
index = files("cista").joinpath("static", "index.html").read_text()
|
||||
flash = request.cookies.message
|
||||
if flash:
|
||||
index += str(E.dialog(flash, id="flash", open=True, style="position: fixed; top: 0; left: 0; width: 100%; opacity: .8"))
|
||||
res = html(index)
|
||||
session.flash(res, None)
|
||||
return res
|
||||
return html(index)
|
||||
@app.get("/<path:path>")
|
||||
async def wwwroot(request, path=""):
|
||||
name = filename.sanitize(unquote(path)) if path else "index.html"
|
||||
try:
|
||||
index = files("cista").joinpath("wwwroot", name).read_bytes()
|
||||
except OSError as e:
|
||||
raise NotFound(f"File not found: /{path}", extra={"name": name, "exception": repr(e)})
|
||||
mime = mimetypes.guess_type(name)[0] or "application/octet-stream"
|
||||
return raw(index, content_type=mime)
|
||||
|
||||
@app.websocket('/api/upload')
|
||||
async def upload(request, ws):
|
||||
|
||||
@@ -3,9 +3,8 @@ import os
|
||||
import unicodedata
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from pathvalidate import sanitize_filepath
|
||||
|
||||
from cista import config
|
||||
from cista.util import filename
|
||||
from cista.util.asynclink import AsyncLink
|
||||
from cista.util.lrucache import LRUCache
|
||||
|
||||
@@ -14,14 +13,6 @@ def fuid(stat) -> str:
|
||||
"""Unique file ID. Stays the same on renames and modification."""
|
||||
return config.derived_secret("filekey-inode", stat.st_dev, stat.st_ino).hex()
|
||||
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
filename = unicodedata.normalize("NFC", filename)
|
||||
# UNIX filenames can contain backslashes but for compatibility we replace them with dashes
|
||||
filename = filename.replace("\\", "-")
|
||||
filename = sanitize_filepath(filename)
|
||||
filename = filename.strip("/")
|
||||
return PurePosixPath(filename).as_posix()
|
||||
|
||||
class File:
|
||||
def __init__(self, filename):
|
||||
self.path = config.config.path / filename
|
||||
@@ -92,12 +83,12 @@ class FileServer:
|
||||
self.cache.close()
|
||||
|
||||
def upload(self, name, pos, data, file_size):
|
||||
name = sanitize_filename(name)
|
||||
name = filename.sanitize(name)
|
||||
f = self.cache[name]
|
||||
f.write(pos, data, file_size=file_size)
|
||||
return len(data)
|
||||
|
||||
def download(self, name, start, end):
|
||||
name = sanitize_filename(name)
|
||||
name = filename.sanitize(name)
|
||||
f = self.cache[name]
|
||||
return f[start: end]
|
||||
|
||||
@@ -6,7 +6,7 @@ import msgspec
|
||||
from sanic import BadRequest
|
||||
|
||||
from cista import config
|
||||
from cista.fileio import sanitize_filename
|
||||
from cista.util import filename
|
||||
|
||||
## Control commands
|
||||
|
||||
@@ -17,24 +17,24 @@ class ControlBase(msgspec.Struct, tag_field="op", tag=str.lower):
|
||||
class MkDir(ControlBase):
|
||||
path: str
|
||||
def __call__(self):
|
||||
path = config.config.path / sanitize_filename(self.path)
|
||||
path = config.config.path / filename.sanitize(self.path)
|
||||
path.mkdir(parents=False, exist_ok=False)
|
||||
|
||||
class Rename(ControlBase):
|
||||
path: str
|
||||
to: str
|
||||
def __call__(self):
|
||||
to = sanitize_filename(self.to)
|
||||
to = filename.sanitize(self.to)
|
||||
if "/" in to:
|
||||
raise BadRequest("Rename 'to' name should only contain filename, not path")
|
||||
path = config.config.path / sanitize_filename(self.path)
|
||||
path = config.config.path / filename.sanitize(self.path)
|
||||
path.rename(path.with_name(to))
|
||||
|
||||
class Rm(ControlBase):
|
||||
sel: list[str]
|
||||
def __call__(self):
|
||||
root = config.config.path
|
||||
sel = [root / sanitize_filename(p) for p in self.sel]
|
||||
sel = [root / filename.sanitize(p) for p in self.sel]
|
||||
for p in sel:
|
||||
shutil.rmtree(p, ignore_errors=True)
|
||||
|
||||
@@ -43,8 +43,8 @@ class Mv(ControlBase):
|
||||
dst: str
|
||||
def __call__(self):
|
||||
root = config.config.path
|
||||
sel = [root / sanitize_filename(p) for p in self.sel]
|
||||
dst = root / sanitize_filename(self.dst)
|
||||
sel = [root / filename.sanitize(p) for p in self.sel]
|
||||
dst = root / filename.sanitize(self.dst)
|
||||
if not dst.is_dir():
|
||||
raise BadRequest("The destination must be a directory")
|
||||
for p in sel:
|
||||
@@ -55,8 +55,8 @@ class Cp(ControlBase):
|
||||
dst: str
|
||||
def __call__(self):
|
||||
root = config.config.path
|
||||
sel = [root / sanitize_filename(p) for p in self.sel]
|
||||
dst = root / sanitize_filename(self.dst)
|
||||
sel = [root / filename.sanitize(p) for p in self.sel]
|
||||
dst = root / filename.sanitize(self.dst)
|
||||
if not dst.is_dir():
|
||||
raise BadRequest("The destination must be a directory")
|
||||
for p in sel:
|
||||
|
||||
11
cista/util/filename.py
Normal file
11
cista/util/filename.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pathvalidate import sanitize_filepath
|
||||
import unicodedata
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
def sanitize(filename: str) -> str:
|
||||
filename = unicodedata.normalize("NFC", filename)
|
||||
# UNIX filenames can contain backslashes but for compatibility we replace them with dashes
|
||||
filename = filename.replace("\\", "-")
|
||||
filename = sanitize_filepath(filename)
|
||||
filename = filename.strip("/")
|
||||
return PurePosixPath(filename).as_posix()
|
||||
1
cista/wwwroot/assets/AboutView-4d995ba2.css
Normal file
1
cista/wwwroot/assets/AboutView-4d995ba2.css
Normal file
@@ -0,0 +1 @@
|
||||
@media (min-width: 1024px){.about{min-height:100vh;display:flex;align-items:center}}
|
||||
1
cista/wwwroot/assets/AboutView-ba1efa64.js
Normal file
1
cista/wwwroot/assets/AboutView-ba1efa64.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as e,o as t,c as o,a as s}from"./index-689b26c8.js";const _={},c={class:"about"},a=s("h1",null,"This is an about page",-1),n=[a];function i(r,u){return t(),o("div",c,n)}const l=e(_,[["render",i]]);export{l as default};
|
||||
9
cista/wwwroot/assets/index-689b26c8.js
Normal file
9
cista/wwwroot/assets/index-689b26c8.js
Normal file
File diff suppressed because one or more lines are too long
1
cista/wwwroot/assets/index-9f680dd7.css
Normal file
1
cista/wwwroot/assets/index-9f680dd7.css
Normal file
@@ -0,0 +1 @@
|
||||
:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px}@media (prefers-color-scheme: dark){:root{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2)}}*,*:before,*:after{box-sizing:border-box;margin:0;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:2rem;font-weight:400}a,.green{text-decoration:none;color:#00bd7e;transition:.4s}@media (hover: hover){a:hover{background-color:#00bd7e33}}@media (min-width: 1024px){body{display:flex;place-items:center}#app{display:grid;grid-template-columns:1fr 1fr;padding:0 2rem}}h1[data-v-a47c673d]{font-weight:500;font-size:2.6rem;position:relative;top:-10px}h3[data-v-a47c673d]{font-size:1.2rem}.greetings h1[data-v-a47c673d],.greetings h3[data-v-a47c673d]{text-align:center}@media (min-width: 1024px){.greetings h1[data-v-a47c673d],.greetings h3[data-v-a47c673d]{text-align:left}}header[data-v-85852c48]{line-height:1.5;max-height:100vh}.logo[data-v-85852c48]{display:block;margin:0 auto 2rem}nav[data-v-85852c48]{width:100%;font-size:12px;text-align:center;margin-top:2rem}nav a.router-link-exact-active[data-v-85852c48]{color:var(--color-text)}nav a.router-link-exact-active[data-v-85852c48]:hover{background-color:transparent}nav a[data-v-85852c48]{display:inline-block;padding:0 1rem;border-left:1px solid var(--color-border)}nav a[data-v-85852c48]:first-of-type{border:0}@media (min-width: 1024px){header[data-v-85852c48]{display:flex;place-items:center;padding-right:calc(var(--section-gap) / 2)}.logo[data-v-85852c48]{margin:0 2rem 0 0}header .wrapper[data-v-85852c48]{display:flex;place-items:flex-start;flex-wrap:wrap}nav[data-v-85852c48]{text-align:left;margin-left:-1rem;font-size:1rem;padding:1rem 0;margin-top:1rem}}.item[data-v-fd0742eb]{margin-top:2rem;display:flex;position:relative}.details[data-v-fd0742eb]{flex:1;margin-left:1rem}i[data-v-fd0742eb]{display:flex;place-items:center;place-content:center;width:32px;height:32px;color:var(--color-text)}h3[data-v-fd0742eb]{font-size:1.2rem;font-weight:500;margin-bottom:.4rem;color:var(--color-heading)}@media (min-width: 1024px){.item[data-v-fd0742eb]{margin-top:0;padding:.4rem 0 1rem calc(var(--section-gap) / 2)}i[data-v-fd0742eb]{top:calc(50% - 25px);left:-26px;position:absolute;border:1px solid var(--color-border);background:var(--color-background);border-radius:8px;width:50px;height:50px}.item[data-v-fd0742eb]:before{content:" ";border-left:1px solid var(--color-border);position:absolute;left:0;bottom:calc(50% + 25px);height:calc(50% - 25px)}.item[data-v-fd0742eb]:after{content:" ";border-left:1px solid var(--color-border);position:absolute;left:0;top:calc(50% + 25px);height:calc(50% - 25px)}.item[data-v-fd0742eb]:first-of-type:before{display:none}.item[data-v-fd0742eb]:last-of-type:after{display:none}}
|
||||
1
cista/wwwroot/assets/logo-277e0e97.svg
Normal file
1
cista/wwwroot/assets/logo-277e0e97.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
BIN
cista/wwwroot/favicon.ico
Normal file
BIN
cista/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
15
cista/wwwroot/index.html
Normal file
15
cista/wwwroot/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/assets/index-689b26c8.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-9f680dd7.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user