UPdate.
This commit is contained in:
parent
dd108c2004
commit
f0591d4939
@ -33,6 +33,7 @@ from snek.view.about import AboutHTMLView, AboutMDView
|
|||||||
from snek.view.avatar import AvatarView
|
from snek.view.avatar import AvatarView
|
||||||
from snek.view.docs import DocsHTMLView, DocsMDView
|
from snek.view.docs import DocsHTMLView, DocsMDView
|
||||||
from snek.view.drive import DriveView
|
from snek.view.drive import DriveView
|
||||||
|
from snek.view.drive import DriveApiView
|
||||||
from snek.view.index import IndexView
|
from snek.view.index import IndexView
|
||||||
from snek.view.login import LoginView
|
from snek.view.login import LoginView
|
||||||
from snek.view.logout import LogoutView
|
from snek.view.logout import LogoutView
|
||||||
@ -178,7 +179,8 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view("/threads.html", ThreadsView)
|
self.router.add_view("/threads.html", ThreadsView)
|
||||||
self.router.add_view("/terminal.ws", TerminalSocketView)
|
self.router.add_view("/terminal.ws", TerminalSocketView)
|
||||||
self.router.add_view("/terminal.html", TerminalView)
|
self.router.add_view("/terminal.html", TerminalView)
|
||||||
self.router.add_view("/drive.json", DriveView)
|
self.router.add_view("/drive.json", DriveApiView)
|
||||||
|
self.router.add_view("/drive.html", DriveView)
|
||||||
self.router.add_view("/drive/{drive}.json", DriveView)
|
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||||
self.router.add_view("/stats.json", StatsView)
|
self.router.add_view("/stats.json", StatsView)
|
||||||
self.router.add_view("/user/{user}.html", UserView)
|
self.router.add_view("/user/{user}.html", UserView)
|
||||||
|
@ -9,6 +9,7 @@ class FileBrowser extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
this.path = this.getAttribute("path") || "";
|
||||||
this.renderShell();
|
this.renderShell();
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
@ -19,11 +20,11 @@ class FileBrowser extends HTMLElement {
|
|||||||
<style>
|
<style>
|
||||||
:host { display:block; font-family: system-ui, sans-serif; box-sizing: border-box; }
|
:host { display:block; font-family: system-ui, sans-serif; box-sizing: border-box; }
|
||||||
nav { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; align-items:center; }
|
nav { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; align-items:center; }
|
||||||
button { padding:.35rem .65rem; border:none; border-radius:4px; background:#0074d9; color:#fff; cursor:pointer; font:inherit; }
|
button { padding:.35rem .65rem; border:none; border-radius:4px; background:#f05a28; color:#fff; cursor:pointer; font:inherit; }
|
||||||
button:disabled { background:#999; cursor:not-allowed; }
|
button:disabled { background:#999; cursor:not-allowed; }
|
||||||
.crumb { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
.crumb { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:1rem; }
|
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:1rem; }
|
||||||
.tile { border:1px solid #ddd; border-radius:8px; padding:.5rem; background:#fafafa; text-align:center; cursor:pointer; transition:box-shadow .2s ease; }
|
.tile { border:1px solid #f05a28; border-radius:8px; padding:.5rem; background:#000000; text-align:center; cursor:pointer; transition:box-shadow .2s ease; }
|
||||||
.tile:hover { box-shadow:0 2px 8px rgba(0,0,0,.1); }
|
.tile:hover { box-shadow:0 2px 8px rgba(0,0,0,.1); }
|
||||||
img.thumb { width:100%; height:90px; object-fit:cover; border-radius:6px; }
|
img.thumb { width:100%; height:90px; object-fit:cover; border-radius:6px; }
|
||||||
.icon { font-size:48px; line-height:90px; }
|
.icon { font-size:48px; line-height:90px; }
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
<div class="logo no-select">{% block header_text %}{% endblock %}</div>
|
<div class="logo no-select">{% block header_text %}{% endblock %}</div>
|
||||||
<nav class="no-select" style="overflow:hidden;scroll-behavior:smooth">
|
<nav class="no-select" style="overflow:hidden;scroll-behavior:smooth">
|
||||||
<a class="no-select" href="/web.html">🏠</a>
|
<a class="no-select" href="/web.html">🏠</a>
|
||||||
|
<a class="no-select" href="/drive.html">📂</a>
|
||||||
<a class="no-select" href="/search-user.html">🔍</a>
|
<a class="no-select" href="/search-user.html">🔍</a>
|
||||||
<a class="no-select" style="display:none" id="install-button" href="#">📥</a>
|
<a class="no-select" style="display:none" id="install-button" href="#">📥</a>
|
||||||
<a class="no-select" href="/threads.html">👥</a>
|
<a class="no-select" href="/threads.html">👥</a>
|
||||||
|
9
src/snek/templates/drive.html
Normal file
9
src/snek/templates/drive.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "app.html" %}
|
||||||
|
|
||||||
|
{% block header_text %}Drive{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="container">
|
||||||
|
<file-manager path="{{path}}" style="flex: 1"></file-manager>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -3,43 +3,7 @@
|
|||||||
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Repository</h1>{% endblock %}
|
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Repository</h1>{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
{% include 'settings/repositories/form.html' %}
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
div,input,label,button{
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
|
||||||
input[type="text"] {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #ccc; border-radius: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
button, a.button {
|
|
||||||
background: #198754; color: #fff; border: none; border-radius: 5px;
|
|
||||||
padding: 0.1rem 0.8rem; text-decoration: none; cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
|
||||||
}
|
|
||||||
.
|
|
||||||
.cancel {
|
|
||||||
background: #6c757d;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { max-width: 98vw; }
|
|
||||||
form { padding: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<form action="/settings/repositories/create.html" method="post">
|
<form action="/settings/repositories/create.html" method="post">
|
||||||
<div>
|
<div>
|
||||||
@ -52,8 +16,9 @@ button, a.button {
|
|||||||
<i class="fa-solid fa-lock"></i> Private
|
<i class="fa-solid fa-lock"></i> Private
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit"><i class="fa-solid fa-plus"></i> Create</button>
|
<button type="submit"><i class="fa-solid fa-pen"></i> Update</button>
|
||||||
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i> Back</button>
|
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,33 +3,8 @@
|
|||||||
{% block header_text %}<h1><i class="fa-solid fa-trash-can"></i> Delete Repository</h1>{% endblock %}
|
{% block header_text %}<h1><i class="fa-solid fa-trash-can"></i> Delete Repository</h1>{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
{% include "settings/repositories/form.html" %}
|
||||||
<style>
|
<div class="container">
|
||||||
.repo-name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex; gap: 1rem; justify-content: left; margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background: #dc3545; color: #fff;
|
|
||||||
border: none; border-radius: 5px; padding: 0.6rem 1.2rem;
|
|
||||||
font-size: 1rem; cursor: pointer;
|
|
||||||
display: flex; align-items: center; gap: 0.5rem; text-decoration: none; justify-content: center;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.cancel {
|
|
||||||
background: #6c757d;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { max-width: 98vw; }
|
|
||||||
.confirm-box { padding: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="container">
|
|
||||||
<p>Are you sure you want to <strong>delete</strong> the following repository?</p>
|
<p>Are you sure you want to <strong>delete</strong> the following repository?</p>
|
||||||
<div class="repo-name"><i class="fa-solid fa-book"></i> {{ repository.name }}</div>
|
<div class="repo-name"><i class="fa-solid fa-book"></i> {{ repository.name }}</div>
|
||||||
<form method="post" style="margin-top:1.5rem;">
|
<form method="post" style="margin-top:1.5rem;">
|
||||||
|
28
src/snek/templates/settings/repositories/form.html
Normal file
28
src/snek/templates/settings/repositories/form.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
div {
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 15px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
||||||
|
button {
|
||||||
|
background: #0d6efd; color: #fff;
|
||||||
|
border: none; border-radius: 5px; padding: 0.6rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.cancel {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container { max-width: 98vw; }
|
||||||
|
form { padding: 1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -3,28 +3,8 @@
|
|||||||
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Repository</h1>{% endblock %}
|
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Repository</h1>{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
{% include "settings/repositories/form.html" %}
|
||||||
<style>
|
<div class="container">
|
||||||
form {
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
|
||||||
button {
|
|
||||||
background: #0d6efd; color: #fff;
|
|
||||||
border: none; border-radius: 5px; padding: 0.6rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
|
||||||
}
|
|
||||||
.cancel {
|
|
||||||
background: #6c757d;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { max-width: 98vw; }
|
|
||||||
form { padding: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="container">
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<!-- Assume hidden id for backend use -->
|
<!-- Assume hidden id for backend use -->
|
||||||
<input type="hidden" name="id" value="{{ repository.id }}">
|
<input type="hidden" name="id" value="{{ repository.id }}">
|
||||||
|
@ -11,47 +11,42 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""Run with: python server.py (Python ≥ 3.9)
|
|
||||||
Visit http://localhost:8080 to try the demo.
|
|
||||||
"""
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import mimetypes, urllib.parse
|
import mimetypes, urllib.parse
|
||||||
|
|
||||||
# ---------- Configuration --------------------------------------------------
|
|
||||||
BASE_DIR = Path(__file__).parent.resolve()
|
|
||||||
ROOT_DIR = (BASE_DIR / "storage").resolve() # files shown to the outside world
|
|
||||||
ASSETS_DIR = (BASE_DIR / "assets").resolve() # JS & demo HTML
|
|
||||||
ROOT_DIR.mkdir(exist_ok=True)
|
|
||||||
ASSETS_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# ---------- Helpers --------------------------------------------------------
|
|
||||||
|
|
||||||
def safe_resolve_path(rel: str) -> Path:
|
|
||||||
"""Return *absolute* path inside ROOT_DIR or raise FileNotFoundError."""
|
|
||||||
target = (ROOT_DIR / rel.lstrip("/")).resolve()
|
|
||||||
if target == ROOT_DIR or ROOT_DIR in target.parents:
|
|
||||||
return target
|
|
||||||
raise FileNotFoundError("Unsafe path")
|
|
||||||
|
|
||||||
# ---------- API view -------------------------------------------------------
|
|
||||||
|
|
||||||
class DriveView(BaseView):
|
class DriveView(BaseView):
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
|
target = await self.services.user.get_home_folder(self.session.get("uid"))
|
||||||
|
rel_path = self.request.match_info.get("rel_path", "")
|
||||||
|
if rel_path:
|
||||||
|
target = target.joinpath(rel_path)
|
||||||
|
|
||||||
|
if not target.exists():
|
||||||
|
return web.HTTPNotFound(reason="Path not found")
|
||||||
|
|
||||||
|
if target.is_dir():
|
||||||
|
return await self.render_template("drive.html",{"path": rel_path})
|
||||||
|
if target.is_file():
|
||||||
|
return web.FileResponse(target)
|
||||||
|
|
||||||
|
|
||||||
|
class DriveApiView(BaseView):
|
||||||
|
async def get(self):
|
||||||
|
target = await self.services.user.get_home_folder(self.session.get("uid"))
|
||||||
rel = self.request.query.get("path", "")
|
rel = self.request.query.get("path", "")
|
||||||
offset = int(self.request.query.get("offset", 0))
|
offset = int(self.request.query.get("offset", 0))
|
||||||
limit = int(self.request.query.get("limit", 20))
|
limit = int(self.request.query.get("limit", 20))
|
||||||
target = await self.services.user.get_home_folder(self.session.get("uid"))
|
|
||||||
if rel:
|
if rel:
|
||||||
target.joinpath(rel)
|
target = target.joinpath(rel)
|
||||||
|
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
return web.json_response({"error": "Not found"}, status=404)
|
return web.json_response({"error": "Not found"}, status=404)
|
||||||
|
|
||||||
# ---- Directory listing -------------------------------------------
|
|
||||||
if target.is_dir():
|
if target.is_dir():
|
||||||
entries = []
|
entries = []
|
||||||
# Directories first, then files – both alphabetical (case‑insensitive)
|
|
||||||
for p in sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower())):
|
for p in sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower())):
|
||||||
item_path = (Path(rel) / p.name).as_posix()
|
item_path = (Path(rel) / p.name).as_posix()
|
||||||
mime = mimetypes.guess_type(p.name)[0] if p.is_file() else "inode/directory"
|
mime = mimetypes.guess_type(p.name)[0] if p.is_file() else "inode/directory"
|
||||||
@ -73,10 +68,6 @@ class DriveView(BaseView):
|
|||||||
"pagination": {"offset": offset, "limit": limit, "total": total}
|
"pagination": {"offset": offset, "limit": limit, "total": total}
|
||||||
})
|
})
|
||||||
|
|
||||||
with open(target, "rb") as f:
|
|
||||||
content = f.read()
|
|
||||||
return web.Response(body=content, content_type=mimetypes.guess_type(target.name)[0])
|
|
||||||
# ---- Single file metadata ----------------------------------------
|
|
||||||
url = self.request.url.with_path(f"/drive/{urllib.parse.quote(rel)}")
|
url = self.request.url.with_path(f"/drive/{urllib.parse.quote(rel)}")
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"name": target.name,
|
"name": target.name,
|
||||||
|
Loading…
Reference in New Issue
Block a user