migrate to uv; add docker configs

This commit is contained in:
2026-04-09 00:53:31 +03:00
parent a88203e98f
commit 606dc930e8
25 changed files with 461 additions and 832 deletions

34
app/conf.py Normal file
View File

@@ -0,0 +1,34 @@
from pathlib import Path
from environs import env
env.read_env()
BASE_HOSTS = env.list('BASE_HOSTS', default=['localhost:8000', '127.0.0.1:8000'])
DEBUG = env.bool('DEBUG', default=False)
FILES_DIR = Path(__file__).parent.parent / 'files'
SUBDOMAIN_FILES_DIR = Path(__file__).parent.parent / 'subdomain_files'
STATIC_DIR = Path(__file__).parent / '~static'
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',
},
}

59
app/filelist.tpl Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Directory Index: /{{ path }}</title>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link href="/~static/darkly.css" rel="stylesheet" type="text/css" />
<link href="/~static/icons.css" rel="stylesheet" type="text/css" />
<style>
table tbody tr td {
padding: 0 !important;
}
table tbody tr td a,
table tbody tr td span {
display: block;
padding: .3rem;
}
</style>
</head>
<body>
<div class="container" style="padding: 15px 0">
<h1 class="h3">Directory Index: /{{ path }}</h1>
<table class="table table-hover table-bordered table-sm">
<thead>
<tr>
<th><a href="{{ get_sort_link(sort, 'name', hidden) }}">
Name {{ get_sort_icon('name', sort) | safe }}
</a></th>
<th style="width: 100px;"><a href="{{ get_sort_link(sort, 'size', hidden) }}">
Size {{ get_sort_icon('size', sort) | safe }}
</a></th>
<th style="width: 170px;"><a href="{{ get_sort_link(sort, 'created', hidden) }}">
Created {{ get_sort_icon('created', sort) | safe }}
</a></th>
</tr>
</thead>
<tbody>
{% for item in lst %}
{% with dir=item.is_dir, file=not item.is_dir %}
<tr>
<td>
<a href="{{ item.link_name }}{% if dir %}{{ query }}{% endif %}" {% if file %}target="_blank"{% endif %}>
{{ item.icon | safe }} {{ item.display_name }}
</a>
</td>
<td>{% if file %}<span title="{{ item.size }} bytes">{{ item.formatted_size }}</span>{% endif %}</td>
<td><span>{{ item.formatted_date }}</span></td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
<small class="d-flex text-muted">
<a href="https://t.me/bakatrouble" target="_blank" class="mr-auto">@bakatrouble</a>
<a href="https://gitea.bakatrouble.me/bakatrouble/autoindex" target="_blank">Source code</a>
</small>
</div>
</body>
</html>

69
app/main.py Normal file
View File

@@ -0,0 +1,69 @@
from pathlib import Path
from sanic import Sanic, HTTPResponse
from sanic.request import Request
from sanic.response import text, html, file_stream, redirect
from sanic.exceptions import NotFound, MethodNotAllowed
from sanic_cors import CORS
from sanic_ext import Extend
from conf import STATIC_DIR
from utils import get_j2env, get_sort_icon, get_sort_link, resolve_path, list_dir
DEBUG = Path('.debug').exists()
app = Sanic('autoindex', strict_slashes=True)
Extend(app)
cors = CORS(app)
app.static('/~static/', STATIC_DIR, use_content_range=True, stream_large_files=True)
print(STATIC_DIR)
j2env = get_j2env(DEBUG)
@app.get(r'/<path:.*/?>')
async def index(request: Request, path=''):
path = path.replace('%20', ' ')
domain = request.host
query = f'?{request.query_string}' if request.query_string else ''
try:
resolved_path, resolved_query = resolve_path(domain, path)
except ValueError:
return text('GTFO', 400)
if resolved_path.is_dir():
if path and path[-1] != '/':
return redirect(f'/{path}/')
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 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():
stream = await file_stream(resolved_path)
if request.method == 'HEAD':
stream.headers['Content-Length'] = resolved_path.stat().st_size
return HTTPResponse(
'', status=200, headers=stream.headers, content_type=stream.content_type
)
return stream
raise NotFound('Path was not found')
if __name__ == '__main__':
if DEBUG:
app.run(host='0.0.0.0', port=8000, debug=True, auto_reload=True)
else:
app.run(host='::', port=8000, workers=4)

105
app/utils.py Normal file
View File

@@ -0,0 +1,105 @@
from dataclasses import dataclass
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, FILES_DIR, SUBDOMAIN_FILES_DIR
def icon_html(cls):
return f'<i class="{cls}"></i>'
@dataclass
class ListEntry:
name: str
mime: str | None
is_dir: bool
size: int
created: float
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(__file__).parent.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 FILES_DIR.joinpath(*path_parts), joined_parts
else:
for host in BASE_HOSTS:
if host in domain:
return SUBDOMAIN_FILES_DIR.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

12
app/~static/darkly.css Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -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="&#xe800;" 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="&#xe801;" 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="&#xe802;" 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="&#xe803;" 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="&#xe804;" 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="&#xe805;" 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="&#xe806;" d="M460 550l-230-400-230 400 460 0z" horiz-adv-x="460" />
<glyph glyph-name="up-dir" unicode="&#xe807;" d="M0 150l230 400 230-400-460 0z" horiz-adv-x="460" />
<glyph glyph-name="level-up" unicode="&#xf148;" 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>

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

63
app/~static/icons.css Normal file

File diff suppressed because one or more lines are too long