feat(auth): add token authentication with SHA-256 hashed storage
This commit is contained in:
@@ -23,6 +23,25 @@ python3 share.py -p 3001 -d ~/Downloads/shared --ttl 7
|
|||||||
| `-p`, `--port` | `8888` | Port |
|
| `-p`, `--port` | `8888` | Port |
|
||||||
| `-d`, `--dir` | `~/Downloads/shared` | Upload directory |
|
| `-d`, `--dir` | `~/Downloads/shared` | Upload directory |
|
||||||
| `--ttl` | `7` | File lifetime in days (0 = keep forever) |
|
| `--ttl` | `7` | File lifetime in days (0 = keep forever) |
|
||||||
|
| `--token` | | Auth token (empty = open access) |
|
||||||
|
| `--cert` | | Path to SSL certificate |
|
||||||
|
| `--key` | | Path to SSL key |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Set `token` in `share.config` or pass `--token`. All endpoints require a valid token.
|
||||||
|
|
||||||
|
The token can be stored as plain text or as a SHA-256 hash:
|
||||||
|
|
||||||
|
```
|
||||||
|
# generate hash
|
||||||
|
python3 share.py hash mytoken
|
||||||
|
|
||||||
|
# store in share.config
|
||||||
|
token = 9c56cc51b374c3ba189210d5b6d0d04...
|
||||||
|
```
|
||||||
|
|
||||||
|
If the value is a 64-character hex string, it is treated as a hash. Otherwise it is hashed automatically at startup.
|
||||||
|
|
||||||
## systemd (Arch)
|
## systemd (Arch)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ _cp.read_dict({
|
|||||||
"server": {
|
"server": {
|
||||||
"port": "3001", "dir": str(Path.home() / "Downloads" / "shared"),
|
"port": "3001", "dir": str(Path.home() / "Downloads" / "shared"),
|
||||||
"ttl": "7", "san": "", "lang": "ru", "refresh": "5",
|
"ttl": "7", "san": "", "lang": "ru", "refresh": "5",
|
||||||
"per_page": "10,25,50,100",
|
"per_page": "10,25,50,100", "token": "",
|
||||||
|
"cert": "", "key": "",
|
||||||
},
|
},
|
||||||
"internal": {
|
"internal": {
|
||||||
"secs_per_day": "86400", "cleanup_interval": "3600",
|
"secs_per_day": "86400", "cleanup_interval": "3600",
|
||||||
@@ -42,6 +43,9 @@ def load_config() -> dict:
|
|||||||
p.add_argument("--san", nargs="*")
|
p.add_argument("--san", nargs="*")
|
||||||
p.add_argument("--lang")
|
p.add_argument("--lang")
|
||||||
p.add_argument("--refresh", type=int)
|
p.add_argument("--refresh", type=int)
|
||||||
|
p.add_argument("--token")
|
||||||
|
p.add_argument("--cert", help="Path to SSL certificate file")
|
||||||
|
p.add_argument("--key", help="Path to SSL key file")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
for key, val in vars(args).items():
|
for key, val in vars(args).items():
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""HTTP server and request handler."""
|
"""HTTP server and request handler."""
|
||||||
|
|
||||||
import email.parser
|
import email.parser
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import http.server
|
import http.server
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -27,11 +29,17 @@ def file_type(path: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class ShareServer(http.server.ThreadingHTTPServer):
|
class ShareServer(http.server.ThreadingHTTPServer):
|
||||||
def __init__(self, addr, handler, *, upload_dir: Path, html: bytes, ssl_ctx: ssl.SSLContext | None = None):
|
def __init__(self, addr, handler, *, upload_dir: Path, html: bytes, ssl_ctx: ssl.SSLContext | None = None, token: str = "", token_is_hash: bool = False):
|
||||||
super().__init__(addr, handler)
|
super().__init__(addr, handler)
|
||||||
self.upload_dir = upload_dir
|
self.upload_dir = upload_dir
|
||||||
self.html = html
|
self.html = html
|
||||||
self.ssl_ctx = ssl_ctx
|
self.ssl_ctx = ssl_ctx
|
||||||
|
if not token:
|
||||||
|
self.token_hash = ""
|
||||||
|
elif token_is_hash:
|
||||||
|
self.token_hash = token
|
||||||
|
else:
|
||||||
|
self.token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
def get_request(self):
|
def get_request(self):
|
||||||
client, addr = self.socket.accept()
|
client, addr = self.socket.accept()
|
||||||
@@ -70,6 +78,73 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
# ── Auth ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hash(token: str) -> str:
|
||||||
|
return hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
|
def _match(self, value: str) -> bool:
|
||||||
|
return hmac.compare_digest(self._hash(value), self.server.token_hash)
|
||||||
|
|
||||||
|
def _check_auth(self) -> bool:
|
||||||
|
if not self.server.token_hash:
|
||||||
|
return True
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
qs = urllib.parse.parse_qs(parsed.query)
|
||||||
|
if qs.get("token", [None])[0] and self._match(qs["token"][0]):
|
||||||
|
return True
|
||||||
|
auth = self.headers.get("Authorization", "")
|
||||||
|
if auth.startswith("Bearer ") and self._match(auth[7:]):
|
||||||
|
return True
|
||||||
|
cookie = self.headers.get("Cookie", "")
|
||||||
|
for part in cookie.split(";"):
|
||||||
|
part = part.strip()
|
||||||
|
if part.startswith("share_token=") and hmac.compare_digest(part[12:], self.server.token_hash):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
_AUTH_PAGE = b'''<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Share</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e0e0e0;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||||
|
.card{background:#141414;border-radius:12px;padding:40px 32px;width:100%;max-width:360px;text-align:center}
|
||||||
|
h1{font-size:1.3rem;color:#fff;margin-bottom:24px}
|
||||||
|
input{width:100%;padding:12px 14px;background:#1a1a1a;border:1px solid #333;border-radius:8px;color:#e0e0e0;font-size:.95rem;outline:none;margin-bottom:16px;transition:border-color .2s}
|
||||||
|
input:focus{border-color:#4fc3f7}
|
||||||
|
button{width:100%;padding:12px;background:#4fc3f7;color:#000;border:none;border-radius:8px;font-size:.95rem;font-weight:600;cursor:pointer;transition:opacity .2s}
|
||||||
|
button:hover{opacity:.85}
|
||||||
|
.err{color:#ef5350;font-size:.85rem;margin-top:12px;min-height:1.2em}
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Share</h1>
|
||||||
|
<form id="f">
|
||||||
|
<input type="password" id="tok" placeholder="Token" autofocus>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<div class="err" id="err"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const f=document.getElementById('f'),tok=document.getElementById('tok'),err=document.getElementById('err');
|
||||||
|
f.onsubmit=e=>{e.preventDefault();err.textContent='';
|
||||||
|
fetch('auth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:tok.value})})
|
||||||
|
.then(r=>{if(r.ok)location.reload();else{err.textContent='Invalid token';tok.select()}})
|
||||||
|
.catch(()=>{err.textContent='Connection error'})};
|
||||||
|
</script>
|
||||||
|
</body></html>'''
|
||||||
|
|
||||||
|
def _send_auth_page(self):
|
||||||
|
self.send_response(401)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(self._AUTH_PAGE)))
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(self._AUTH_PAGE)
|
||||||
|
|
||||||
# ── Routes ──
|
# ── Routes ──
|
||||||
|
|
||||||
ROUTES = {
|
ROUTES = {
|
||||||
@@ -78,14 +153,29 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
("/index.html","_route_index"),
|
("/index.html","_route_index"),
|
||||||
("/files", "_route_files"),
|
("/files", "_route_files"),
|
||||||
("/dl/", "_route_download"),
|
("/dl/", "_route_download"),
|
||||||
|
("/logout", "_route_logout"),
|
||||||
],
|
],
|
||||||
"POST": [("/upload", "_route_upload")],
|
"POST": [("/upload", "_route_upload"), ("/auth", "_route_auth")],
|
||||||
"DELETE": [("/del/", "_route_delete")],
|
"DELETE": [("/del/", "_route_delete")],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _dispatch(self):
|
def _dispatch(self):
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
path_only = parsed.path
|
||||||
|
|
||||||
for prefix, method in self.ROUTES.get(self.command, []):
|
for prefix, method in self.ROUTES.get(self.command, []):
|
||||||
if self.path == prefix or (len(prefix) > 1 and prefix.endswith("/") and self.path.startswith(prefix)):
|
if path_only == prefix or (len(prefix) > 1 and prefix.endswith("/") and path_only.startswith(prefix)):
|
||||||
|
if method not in ("_route_auth",) and not self._check_auth():
|
||||||
|
if self.command == "GET" and path_only in ("/", "/index.html"):
|
||||||
|
return self._send_auth_page()
|
||||||
|
self.send_response(401)
|
||||||
|
body = json.dumps({"error": "unauthorized"}).encode()
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
return
|
||||||
return getattr(self, method)(prefix)
|
return getattr(self, method)(prefix)
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
@@ -103,7 +193,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self._json([{"name": p.name, "size": st.st_size, "type": file_type(p)} for p, st in entries])
|
self._json([{"name": p.name, "size": st.st_size, "type": file_type(p)} for p, st in entries])
|
||||||
|
|
||||||
def _safe_path(self, prefix: str) -> Path:
|
def _safe_path(self, prefix: str) -> Path:
|
||||||
name = urllib.parse.unquote(self.path[len(prefix):])
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
name = urllib.parse.unquote(parsed.path[len(prefix):])
|
||||||
return self.server.upload_dir / Path(name).name
|
return self.server.upload_dir / Path(name).name
|
||||||
|
|
||||||
def _route_download(self, prefix):
|
def _route_download(self, prefix):
|
||||||
@@ -141,6 +232,38 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
count = self._save_parts(ct, body)
|
count = self._save_parts(ct, body)
|
||||||
self._json({"ok": True, "count": count})
|
self._json({"ok": True, "count": count})
|
||||||
|
|
||||||
|
def _route_auth(self, _prefix):
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(length) if length else b""
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
data = {}
|
||||||
|
if data.get("token") and self._match(data["token"]):
|
||||||
|
resp = json.dumps({"ok": True}).encode()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(resp)))
|
||||||
|
self.send_header("Set-Cookie", f"share_token={self._hash(data['token'])}; Path=/; HttpOnly; SameSite=Strict; Max-Age=31536000")
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp)
|
||||||
|
else:
|
||||||
|
resp = json.dumps({"ok": False}).encode()
|
||||||
|
self.send_response(401)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(resp)))
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp)
|
||||||
|
|
||||||
|
def _route_logout(self, _prefix):
|
||||||
|
self.send_response(302)
|
||||||
|
self.send_header("Set-Cookie", "share_token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0")
|
||||||
|
self.send_header("Location", "/")
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
def _route_delete(self, prefix):
|
def _route_delete(self, prefix):
|
||||||
safe = self._safe_path(prefix)
|
safe = self._safe_path(prefix)
|
||||||
if safe.is_file():
|
if safe.is_file():
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ ttl = 7
|
|||||||
san =
|
san =
|
||||||
lang = ru
|
lang = ru
|
||||||
refresh = 5
|
refresh = 5
|
||||||
|
token = 755b7c9ca04986c0dd496353e9d94ce713f2df0657b9b4008cb4d617679e103f
|
||||||
per_page = 10,25,50,100
|
per_page = 10,25,50,100
|
||||||
|
|
||||||
[internal]
|
[internal]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Share — local file sharing server (HTTPS)."""
|
"""Share — local file sharing server (HTTPS)."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -13,6 +15,11 @@ from server import Handler, ShareServer
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "hash":
|
||||||
|
token = sys.argv[2] if len(sys.argv) > 2 else input("Token: ")
|
||||||
|
print(hashlib.sha256(token.encode()).hexdigest())
|
||||||
|
return
|
||||||
|
|
||||||
conf = load_config()
|
conf = load_config()
|
||||||
|
|
||||||
port = int(conf["port"])
|
port = int(conf["port"])
|
||||||
@@ -29,12 +36,18 @@ def main():
|
|||||||
html = (BASE_DIR / "static" / "index.html").read_bytes()
|
html = (BASE_DIR / "static" / "index.html").read_bytes()
|
||||||
html = html.replace(b"<!--CFG-->", cfg_tag)
|
html = html.replace(b"<!--CFG-->", cfg_tag)
|
||||||
|
|
||||||
cert_dir = BASE_DIR / ".certs"
|
if conf["cert"] and conf["key"]:
|
||||||
cert, key = ensure_cert(cert_dir, extra_ips=san_list)
|
cert, key = Path(conf["cert"]), Path(conf["key"])
|
||||||
|
else:
|
||||||
|
cert_dir = BASE_DIR / ".certs"
|
||||||
|
cert, key = ensure_cert(cert_dir, extra_ips=san_list)
|
||||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
ctx.load_cert_chain(cert, key)
|
ctx.load_cert_chain(cert, key)
|
||||||
|
|
||||||
server = ShareServer(("0.0.0.0", port), Handler, upload_dir=upload_dir, html=html, ssl_ctx=ctx)
|
token = conf["token"]
|
||||||
|
# If token looks like a SHA-256 hash, pass it as pre-hashed
|
||||||
|
is_hash = len(token) == 64 and all(c in "0123456789abcdef" for c in token)
|
||||||
|
server = ShareServer(("0.0.0.0", port), Handler, upload_dir=upload_dir, html=html, ssl_ctx=ctx, token=token, token_is_hash=is_hash)
|
||||||
|
|
||||||
print("\n Share running")
|
print("\n Share running")
|
||||||
print(f" Local: https://localhost:{port}")
|
print(f" Local: https://localhost:{port}")
|
||||||
|
|||||||
+19
-5
@@ -127,6 +127,14 @@ h1 { font-size: 1.4rem; margin-bottom: 16px; color: #fff }
|
|||||||
margin-bottom: 16px; position: relative;
|
margin-bottom: 16px; position: relative;
|
||||||
}
|
}
|
||||||
#header h1 { margin-bottom: 0 }
|
#header h1 { margin-bottom: 0 }
|
||||||
|
#logout-btn {
|
||||||
|
position: absolute; left: 0;
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
color: var(--text-muted); padding: 4px;
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
transition: color .2s;
|
||||||
|
}
|
||||||
|
#logout-btn:hover { color: var(--danger) }
|
||||||
#lang-sw {
|
#lang-sw {
|
||||||
position: absolute; right: 0;
|
position: absolute; right: 0;
|
||||||
display: flex; background: var(--bg-card-hover); border-radius: 6px;
|
display: flex; background: var(--bg-card-hover); border-radius: 6px;
|
||||||
@@ -156,6 +164,7 @@ h1 { font-size: 1.4rem; margin-bottom: 16px; color: #fff }
|
|||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<div id="header">
|
<div id="header">
|
||||||
|
<button id="logout-btn" title="Logout"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></button>
|
||||||
<h1 id="title"></h1>
|
<h1 id="title"></h1>
|
||||||
<div id="lang-sw"><span data-l="ru">RU</span><span data-l="en">EN</span></div>
|
<div id="lang-sw"><span data-l="ru">RU</span><span data-l="en">EN</span></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,6 +227,7 @@ const I18N = {
|
|||||||
uploadError: 'Ошибка соединения',
|
uploadError: 'Ошибка соединения',
|
||||||
deleted: 'Удалено',
|
deleted: 'Удалено',
|
||||||
perPage: 'На странице:',
|
perPage: 'На странице:',
|
||||||
|
logout: 'Выход',
|
||||||
sz: [' Б', ' КБ', ' МБ', ' ГБ'],
|
sz: [' Б', ' КБ', ' МБ', ' ГБ'],
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
@@ -238,6 +248,7 @@ const I18N = {
|
|||||||
uploadError: 'Upload error',
|
uploadError: 'Upload error',
|
||||||
deleted: 'Deleted',
|
deleted: 'Deleted',
|
||||||
perPage: 'Per page:',
|
perPage: 'Per page:',
|
||||||
|
logout: 'Logout',
|
||||||
sz: [' B', ' KB', ' MB', ' GB'],
|
sz: [' B', ' KB', ' MB', ' GB'],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -254,6 +265,7 @@ function setLang(l) {
|
|||||||
$('drop-text').textContent = t().drop;
|
$('drop-text').textContent = t().drop;
|
||||||
$('drop-hint').textContent = t().hint;
|
$('drop-hint').textContent = t().hint;
|
||||||
$('files-title').textContent = t().files;
|
$('files-title').textContent = t().files;
|
||||||
|
$('logout-btn').title = t().logout;
|
||||||
loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +338,7 @@ function toPngBlob(blob) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copyFile(name, type) {
|
function copyFile(name, type) {
|
||||||
const url = '/dl/' + encodeURIComponent(name);
|
const url = 'dl/' + encodeURIComponent(name);
|
||||||
|
|
||||||
if (type === 'text') {
|
if (type === 'text') {
|
||||||
fetch(url).then(r => { if (!r.ok) throw 0; return r.text() }).then(text => copyText(text))
|
fetch(url).then(r => { if (!r.ok) throw 0; return r.text() }).then(text => copyText(text))
|
||||||
@@ -382,7 +394,7 @@ function upload(fileList) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = () => { done(); toast(t().uploadError) };
|
xhr.onerror = () => { done(); toast(t().uploadError) };
|
||||||
xhr.open('POST', '/upload');
|
xhr.open('POST', 'upload');
|
||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +468,7 @@ function renderFiles() {
|
|||||||
<span class="fname" data-action="copy" data-name="${esc(f.name)}" data-type="${f.type}" title="${t().copy}">${esc(f.name)}</span>
|
<span class="fname" data-action="copy" data-name="${esc(f.name)}" data-type="${f.type}" title="${t().copy}">${esc(f.name)}</span>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span class="fsize">${fmtSize(f.size)}</span>
|
<span class="fsize">${fmtSize(f.size)}</span>
|
||||||
<a class="btn dl" href="/dl/${encodeURIComponent(f.name)}" title="${t().download}"><svg width="14" height="14"><use href="#icon-dl"/></svg></a>
|
<a class="btn dl" href="dl/${encodeURIComponent(f.name)}" title="${t().download}"><svg width="14" height="14"><use href="#icon-dl"/></svg></a>
|
||||||
<span class="btn del" data-action="del" data-name="${esc(f.name)}" title="${t().delete}"><svg width="14" height="14"><use href="#icon-del"/></svg></span>
|
<span class="btn del" data-action="del" data-name="${esc(f.name)}" title="${t().delete}"><svg width="14" height="14"><use href="#icon-del"/></svg></span>
|
||||||
</div>
|
</div>
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
@@ -465,7 +477,7 @@ function renderFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadFiles() {
|
function loadFiles() {
|
||||||
fetch('/files').then(r => r.json()).then(files => {
|
fetch('files').then(r => r.json()).then(files => {
|
||||||
allFiles = files;
|
allFiles = files;
|
||||||
renderFiles();
|
renderFiles();
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@@ -480,7 +492,7 @@ flist.addEventListener('click', e => {
|
|||||||
const name = el.dataset.name;
|
const name = el.dataset.name;
|
||||||
if (action === 'copy') copyFile(name, el.dataset.type);
|
if (action === 'copy') copyFile(name, el.dataset.type);
|
||||||
else if (action === 'del') {
|
else if (action === 'del') {
|
||||||
fetch('/del/' + encodeURIComponent(name), { method: 'DELETE' })
|
fetch('del/' + encodeURIComponent(name), { method: 'DELETE' })
|
||||||
.then(() => { toast(t().deleted); loadFiles() })
|
.then(() => { toast(t().deleted); loadFiles() })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -532,6 +544,8 @@ document.addEventListener('paste', e => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('logout-btn').addEventListener('click', () => { window.location = 'logout' });
|
||||||
|
|
||||||
setLang(lang);
|
setLang(lang);
|
||||||
let pollId = setInterval(loadFiles, REFRESH_MS);
|
let pollId = setInterval(loadFiles, REFRESH_MS);
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user