@ -1,117 +1,57 @@ | |||
from bottle import route, request, run, redirect, template, default_app, abort | |||
import os | |||
from datetime import datetime | |||
from mimetypes import guess_type | |||
from os import environ | |||
base_hosts = [ | |||
'drop.bakatrouble.pw', | |||
'drop.bakatrouble.me', | |||
'127.0.0.1.xip.io:8080', | |||
] | |||
from sanic import Sanic | |||
from sanic.request import Request | |||
from sanic.response import text, html, file_stream, redirect | |||
from sanic.exceptions import abort | |||
from utils import get_j2env, get_sort_icon, get_sort_link, resolve_path, list_dir | |||
def format_size(size): | |||
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB']: | |||
if size < 2048: | |||
return f'{size} {unit}' | |||
else: | |||
size //= 1024 | |||
return f'{size} PiB' | |||
DEBUG = environ.get('ENV', '').upper() != 'PRODUCTION' | |||
def format_date(ts): | |||
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') | |||
app = Sanic() | |||
app.static('/~static/', '~static/', use_content_range=True, stream_large_files=True) | |||
j2env = get_j2env(DEBUG) | |||
def get_sort_icon(test, sort): | |||
if sort == test: | |||
return '<i class="sort descending icon"></i>' | |||
elif sort == f'-{test}': | |||
return '<i class="sort ascending icon"></i>' | |||
else: | |||
return '' | |||
def get_sort_link(current, sort, hidden): | |||
return f'?sort={"-" if current == sort else ""}{sort}{"&hidden" if hidden else ""}' | |||
def get_file_icon(name): | |||
mime = guess_type(name)[0] | |||
if mime: | |||
t = mime.split('/')[0] | |||
if t in ['text', 'image', 'audio', 'video']: | |||
return t | |||
return '' | |||
@app.route('/') | |||
@app.route('/<path:path>') | |||
async def index(request: Request, path=''): | |||
domain = request.host | |||
query = f'?{request.query_string}' if request.query_string else '' | |||
class ListEntry: | |||
def __init__(self, dir, name): | |||
path = os.path.join(dir, name) | |||
self.name = name | |||
self.isdir = os.path.isdir(path) | |||
self.size = os.path.getsize(path) | |||
self.created = os.path.getctime(path) | |||
def resolve_path(domain, path): | |||
if domain in base_hosts: | |||
return os.path.join(os.path.dirname(__file__), 'files', *path.split('/')) | |||
else: | |||
for host in base_hosts: | |||
if host in domain: | |||
return os.path.join(os.path.dirname(__file__), 'subdomain_files', | |||
domain[:domain.index(host)-1], *path.split('/')) | |||
raise ValueError | |||
def list_dir(dir, sort=None, hidden=False): | |||
lst = [ListEntry(dir, name) for name in os.listdir(dir) if hidden or not name.startswith('.')] | |||
lst = sorted(lst, key={ | |||
'name': lambda item: (not item.isdir, item.name.lower()), | |||
'-name': lambda item: (item.isdir, item.name.lower()), | |||
'size': lambda item: (not item.isdir, item.size), | |||
'-size': lambda item: (item.isdir, item.size), | |||
'created': lambda item: (not item.isdir, item.created), | |||
'-created': lambda item: (item.isdir, item.created) | |||
}[sort], reverse=sort.startswith('-')) | |||
return lst | |||
@route('/') | |||
@route('/<path:path>') | |||
def index(path=''): | |||
domain = request.urlparts.netloc | |||
query = request.urlparts.query | |||
try: | |||
resolved_path = resolve_path(domain, path) | |||
resolved_path, resolved_query = resolve_path(domain, path) | |||
except ValueError: | |||
return 'GTFO' | |||
if os.path.isdir(resolved_path): | |||
return text('GTFO', 400) | |||
if resolved_path.is_dir(): | |||
if path and path[-1] != '/': | |||
return redirect(f'/{path}/') | |||
hidden = 'hidden' in request.GET | |||
sort = request.GET.get('sort', 'name') | |||
hidden = request.args.get('hidden') is not None | |||
sort = request.args.get('sort', 'name') | |||
if sort not in ['name', '-name', 'size', '-size', 'created', '-created']: | |||
sort = 'name' | |||
return template('filelist', | |||
lst=list_dir(resolved_path, sort, hidden), | |||
format_size=format_size, | |||
format_date=format_date, | |||
get_sort_icon=get_sort_icon, | |||
get_sort_link=get_sort_link, | |||
sort=sort, | |||
hidden=hidden, | |||
path=path, | |||
query=query, | |||
guess_type=guess_type, | |||
get_file_icon=get_file_icon) | |||
else: | |||
return abort(404) | |||
return html(j2env.get_template('filelist.tpl').render( | |||
lst=list_dir(resolved_path, sort, hidden, root=not resolved_query), | |||
get_sort_icon=get_sort_icon, | |||
get_sort_link=get_sort_link, | |||
sort=sort, | |||
hidden=hidden, | |||
path=resolved_query, | |||
query=query, | |||
)) | |||
elif resolved_path.is_file(): | |||
return await file_stream(resolved_path) | |||
abort(404, 'Path was not found') | |||
application = default_app() | |||
if __name__ == '__main__': | |||
run(host='localhost', port=8080, debug=True, reloader=True) | |||
if DEBUG: | |||
app.run(host='localhost', port=8080, debug=True, auto_reload=True) | |||
else: | |||
app.run(sock='/tmp/drop.sock', workers=2) |
@ -0,0 +1,29 @@ | |||
BASE_HOSTS = [ | |||
'drop.bakatrouble.pw', | |||
'drop.bakatrouble.me', | |||
'127.0.0.1.xip.io:8080', | |||
'localhost:8080', | |||
] | |||
SORT_KEYS = { | |||
'name': lambda item: (not item.is_dir, item.name.lower()), | |||
'-name': lambda item: (item.is_dir, item.name.lower()), | |||
'size': lambda item: (not item.is_dir, item.size), | |||
'-size': lambda item: (item.is_dir, item.size), | |||
'created': lambda item: (not item.is_dir, item.created), | |||
'-created': lambda item: (item.is_dir, item.created), | |||
} | |||
ICONS = { | |||
'sort_asc': 'icon-up-dir', | |||
'sort_desc': 'icon-down-dir', | |||
'types': { | |||
'parent': 'icon-level-up', | |||
'dir': 'icon-folder', | |||
'file': 'icon-doc', | |||
'text': 'icon-doc-text', | |||
'image': 'icon-picture-outline', | |||
'music': 'icon-music-outline', | |||
'video': 'icon-video', | |||
}, | |||
} |
@ -1,65 +1,45 @@ | |||
server { | |||
server_name drop.bakatrouble.pw; | |||
location /_ { | |||
alias /srv/apps/drop/files; | |||
} | |||
location / { | |||
include uwsgi_params; | |||
uwsgi_pass unix:///tmp/drop.sock; | |||
} | |||
listen 443 ssl; # managed by Certbot | |||
ssl_certificate /etc/letsencrypt/live/bakatrouble.pw/fullchain.pem; # managed by Certbot | |||
ssl_certificate_key /etc/letsencrypt/live/bakatrouble.pw/privkey.pem; # managed by Certbot | |||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot | |||
location @drop_app { | |||
proxy_pass unix:///tmp/drop.sock; | |||
proxy_set_header Host $host; | |||
proxy_set_header X-Forwarded-For $remote_addr; | |||
} | |||
if ($scheme != "https") { | |||
return 301 https://$host$request_uri; | |||
} # managed by Certbot | |||
server { | |||
server_name ~^drop\.bakatrouble\.(pw|me)$; | |||
root /srv/apps/drop/files; | |||
location /~static/ { | |||
alias /srv/apps/drop/~static; | |||
} | |||
location / { | |||
try_files $uri @drop_app; | |||
} | |||
include /etc/nginx/letsencrypt-serv.conf; | |||
} | |||
server { | |||
server_name ~^(?<dir>.*)\.drop\.bakatrouble\.pw$; | |||
location /_ { | |||
alias /srv/apps/drop/subdomain_files/$dir; | |||
} | |||
location / { | |||
include uwsgi_params; | |||
uwsgi_pass unix:///tmp/drop.sock; | |||
} | |||
server_name ~^(?<dir>.*)\.drop\.bakatrouble\.(pw|me)$; | |||
root /srv/apps/drop/subdomain_files/$dir; | |||
listen 443 ssl; # managed by Certbot | |||
ssl_certificate /etc/letsencrypt/live/bakatrouble.pw/fullchain.pem; # managed by Certbot | |||
ssl_certificate_key /etc/letsencrypt/live/bakatrouble.pw/privkey.pem; # managed by Certbot | |||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot | |||
location /~static/ { | |||
alias /srv/apps/drop/~static; | |||
} | |||
location / { | |||
try_files $uri @drop_app; | |||
} | |||
if ($scheme != "https") { | |||
return 301 https://$host$request_uri; | |||
} # managed by Certbot | |||
include /etc/nginx/letsencrypt-serv.conf; | |||
} | |||
server { | |||
if ($host = drop.bakatrouble.pw) { | |||
return 301 https://$host$request_uri; | |||
} # managed by Certbot | |||
if ($host ~ ^(?<dir>.*)\.drop\.bakatrouble\.pw$) { | |||
server { | |||
if ($host ~ ^(.*\.)?drop\.bakatrouble\.(pw|me)$) { | |||
return 301 https://$host$request_uri; | |||
} # managed by Certbot | |||
} | |||
listen 80; | |||
server_name drop.bakatrouble.pw ~^(?<dir>.*)\.drop\.bakatrouble\.pw$; | |||
return 404; # managed by Certbot | |||
} | |||
listen 80; | |||
server_name ~^(.*\.)?drop\.bakatrouble\.(pw|me)$; | |||
return 404; | |||
} |
@ -1,9 +1,10 @@ | |||
[program:drop] | |||
user = arch | |||
directory = /srv/apps/drop | |||
command = /srv/apps/drop/venv/bin/uwsgi --ini /srv/apps/drop/configs/uwsgi.ini | |||
command = /srv/apps/drop/venv/bin/python /srv/apps/drop/autoindex.py | |||
autostart = true | |||
autorestart = true | |||
stderr_logfile = /srv/apps/drop/logs/uwsgi_err.log | |||
stdout_logfile = /srv/apps/drop/logs/uwsgi_out.log | |||
stderr_logfile = /srv/apps/drop/logs/app.log | |||
stdout_logfile = /srv/apps/drop/logs/app.log | |||
stopsignal = INT | |||
environment = ENV=PRODUCTION |
@ -1,7 +0,0 @@ | |||
[uwsgi] | |||
socket = /tmp/drop.sock | |||
chmod-socket = 666 | |||
module = autoindex:application | |||
master = true | |||
processes = 2 | |||
enable-threads = true |
@ -0,0 +1,9 @@ | |||
aiofiles==0.4.0 | |||
httptools==0.0.13 | |||
Jinja2==2.10 | |||
MarkupSafe==1.1.1 | |||
multidict==4.5.2 | |||
sanic==18.12.0 | |||
ujson==1.35 | |||
uvloop==0.12.1 | |||
websockets==6.0 |
@ -0,0 +1,97 @@ | |||
from datetime import datetime | |||
from mimetypes import guess_type | |||
from pathlib import Path | |||
from jinja2 import Environment, FileSystemLoader | |||
from conf import BASE_HOSTS, SORT_KEYS, ICONS | |||
def icon_html(cls): | |||
return f'<i class="{cls}"></i>' | |||
class ListEntry: | |||
def __init__(self, path: Path): | |||
self.name = path.name | |||
self.mime = guess_type(self.name)[0] | |||
self.is_dir = path.is_dir() | |||
self.size = path.stat().st_size | |||
self.created = path.stat().st_ctime | |||
@property | |||
def link_name(self): | |||
if self.is_dir: | |||
return f'{self.name}/' | |||
return self.name | |||
@property | |||
def display_name(self): | |||
if self.is_dir: | |||
return f'{self.name}/' | |||
return f'{self.name} [{self.mime}]' | |||
@property | |||
def icon(self): | |||
if self.name == '..': | |||
return icon_html(ICONS['types']['parent']) | |||
elif self.is_dir: | |||
return icon_html(ICONS['types']['dir']) | |||
elif self.mime: | |||
t = self.mime.split('/')[0] | |||
if t in ICONS['types']: | |||
return icon_html(ICONS['types'][t]) | |||
return icon_html(ICONS['types']['file']) | |||
@property | |||
def formatted_date(self): | |||
return datetime.fromtimestamp(self.created).strftime('%Y-%m-%d %H:%M:%S') | |||
@property | |||
def formatted_size(self): | |||
size = self.size | |||
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB']: | |||
if size < 2048: | |||
return f'{size} {unit}' | |||
else: | |||
size //= 1024 | |||
return f'{size} PiB' | |||
def get_j2env(debug=False): | |||
return Environment(loader=FileSystemLoader([Path('.').absolute()]), | |||
trim_blocks=True, optimized=debug, cache_size=0 if debug else 400) | |||
def get_sort_icon(test, sort): | |||
if sort == test: | |||
return icon_html(ICONS['sort_asc']) | |||
elif sort == f'-{test}': | |||
return icon_html(ICONS['sort_desc']) | |||
else: | |||
return '' | |||
def get_sort_link(current, sort, hidden): | |||
return f'?sort={"-" if current == sort else ""}{sort}{"&hidden" if hidden else ""}' | |||
def resolve_path(domain, path): | |||
path_parts = [i for i in path.split('/') if i not in ['.', '..']] | |||
joined_parts = '/'.join(path_parts) | |||
if domain in BASE_HOSTS: | |||
return Path('files').joinpath(*path_parts), joined_parts | |||
else: | |||
for host in BASE_HOSTS: | |||
if host in domain: | |||
return Path('subdomain_files').joinpath(domain[:domain.index(host)-1], *path_parts), joined_parts | |||
raise ValueError | |||
def list_dir(directory: Path, sort, hidden=False, root=True): | |||
lst = [ListEntry(path) for path in directory.iterdir() if hidden or not path.name.startswith('.')] | |||
lst = sorted(lst, key=SORT_KEYS[sort], reverse=sort.startswith('-')) | |||
if not root: | |||
lst = [ListEntry(Path('..'))] + lst | |||
return lst |
@ -0,0 +1,28 @@ | |||
<?xml version="1.0" standalone="no"?> | |||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||
<svg xmlns="http://www.w3.org/2000/svg"> | |||
<metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata> | |||
<defs> | |||
<font id="fontello" horiz-adv-x="1000" > | |||
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" /> | |||
<missing-glyph horiz-adv-x="1000" /> | |||
<glyph glyph-name="music-outline" unicode="" d="M729 797q42 0 74-31t31-73l0-529q0-86-70-147t-165-61q-93 0-159 57-30-50-85-80t-120-29q-96 0-166 61t-69 148q0 66 44 120t112 76l0 318q0 40 27 69t65 34l468 65q2 0 7 1t6 1z m-313-633q0 59 45 103t112 52l0 90-157-22 0-223z m313 0l0 529-469-66 0-413q-17 3-25 3-55 0-93-31t-38-73q0-44 39-75t92-30 92 30 38 75l0 319 260 38 0-203q-17 2-26 2-54 0-92-31t-38-74 38-74 92-30 92 30 38 74z" horiz-adv-x="834" /> | |||
<glyph glyph-name="picture-outline" unicode="" d="M339 559q-31 0-55-23t-24-56q0-31 23-54t56-23q31 0 54 22t23 55-23 56-54 23z m0 51q54 0 92-37t38-93-38-92-92-38-92 38-38 92 38 93 92 37z m156-416q-49 0-94 26t-88 27q-40 0-84-105l592 0q-18 85-46 145t-46 63q-34 0-96-70-25-27-41-42t-43-29-54-15z m234 209q46 0 85-78t55-157l16-78-729 0q3 9 7 23t18 49 32 63 43 50 57 23q55 0 101-26t81-25q25 0 54 24t53 54 57 53 70 25z m312 260l0-625q0-43-30-73t-73-31l-834 0q-44 0-74 31t-30 73l0 625q0 42 30 73t74 31l834 0q42 0 73-31t30-73z m-103-625l0 625-834 0 0-625 834 0z" horiz-adv-x="1041" /> | |||
<glyph glyph-name="video" unicode="" d="M209 533l416 0 0-365-416 0 0 365z m364-313l0 260-313 0 0-260 313 0z m105 625q65 0 110-46t46-110l0-678q0-65-46-110t-110-46l-157 0 0 104-208 0 0-104-157 0q-65 0-110 46t-46 110l0 678q0 65 46 110t110 46l157 0 0-105 208 0 0 105 157 0z m51-209l0 53q0 21-15 36t-36 15l-53 0 0-104-416 0 0 104-53 0q-21 0-37-15t-15-36l0-53q21 0 37-15t15-37-15-36-37-15l0-53q21 0 37-16t15-36-15-37-37-15l0-52q21 0 37-16t15-37-15-36-37-15l0-52q21 0 37-16t15-37-15-36-37-15l0-53q0-21 15-37t37-15l53 0 0 105 416 0 0-105 53 0q21 0 36 15t15 37l0 53q-21 0-36 15t-15 36 15 37 36 16l0 52q-21 0-36 15t-15 36 15 37 36 16l0 52q-21 0-36 15t-15 37 15 36 36 16l0 53q-21 0-36 15t-15 36 15 37 36 15z" horiz-adv-x="834" /> | |||
<glyph glyph-name="doc-text" unicode="" d="M678-119l-522 0q-65 0-110 46t-46 111l0 625q0 65 46 110t110 46l522 0q65 0 110-46t46-110l0-625q0-65-46-111t-110-46z m-522 834q-21 0-37-15t-15-37l0-625q0-21 15-37t37-16l522 0q21 0 36 16t15 37l0 625q0 21-15 37t-36 15l-522 0z m469-312l-416 0q-26 0-26 25 0 11 7 19t19 7l416 0q11 0 19-7t7-19q0-25-26-25z m0 156l-416 0q-11 0-19 8t-7 18q0 25 26 25l416 0q26 0 26-25 0-11-7-18t-19-8z m0-312l-416 0q-11 0-19 7t-7 19q0 25 26 25l416 0q26 0 26-25 0-11-7-19t-19-7z m0-157l-416 0q-26 0-26 25 0 12 7 19t19 8l416 0q11 0 19-8t7-19q0-25-26-25z" horiz-adv-x="834" /> | |||
<glyph glyph-name="doc" unicode="" d="M818 595q16-16 16-36l0-521q0-65-46-111t-110-46l-522 0q-65 0-110 46t-46 111l0 625q0 65 46 110t110 46l417 0q22 0 37-15z m-110-36l-135 134 0-56q0-32 23-55t55-23l57 0z m-30-574q21 0 36 16t15 37l0 469-78 0q-53 0-92 38t-38 92l0 78-365 0q-21 0-37-15t-15-37l0-625q0-21 15-37t37-16l522 0z" horiz-adv-x="834" /> | |||
<glyph glyph-name="folder" unicode="" d="M781 663q65 0 111-46t46-110l0-417q0-65-46-110t-111-46l-625 0q-65 0-110 46t-46 110l0 520q0 65 46 111t110 46l209 0q43 0 73-31t31-73l312 0z m-625 0q-21 0-37-16t-15-37l0-103 730 0q0 21-16 36t-37 16l-312 0q-44 0-74 31t-30 73l-209 0z m625-625q22 0 37 15t16 37l0 364-730 0 0-364q0-21 15-37t37-15l625 0z" horiz-adv-x="938" /> | |||
<glyph glyph-name="down-dir" unicode="" d="M460 550l-230-400-230 400 460 0z" horiz-adv-x="460" /> | |||
<glyph glyph-name="up-dir" unicode="" d="M0 150l230 400 230-400-460 0z" horiz-adv-x="460" /> | |||
<glyph glyph-name="level-up" unicode="" d="M568 514q-10-21-32-21h-107v-482q0-8-5-13t-13-5h-393q-12 0-16 10-5 11 2 19l89 108q5 6 14 6h179v357h-107q-23 0-33 21-9 20 5 38l179 214q10 12 27 12t28-12l178-214q15-18 5-38z" horiz-adv-x="571.4" /> | |||
</font> | |||
</defs> | |||
</svg> |