Compare commits
11 Commits
e9125098f8
...
02c5bc8026
| Author | SHA1 | Date | |
|---|---|---|---|
| 02c5bc8026 | |||
| 5a0935b8f4 | |||
| a7080dd767 | |||
| 7c2b8e177f | |||
| bec7f593fa | |||
| 7f54aba4af | |||
| 8ac541f597 | |||
| e176172e9b | |||
| 9fe05b28b6 | |||
| 3d8c8c1ec7 | |||
| 8bd20d815c |
236
app.py
236
app.py
@ -20,15 +20,18 @@ import search
|
|||||||
import auth
|
import auth
|
||||||
import analytics as a
|
import analytics as a
|
||||||
|
|
||||||
cache_audio = diskcache.Cache('./filecache_audio', size_limit= 48 * 1024**3) # 48 GB limit
|
with open("app_config.json", 'r') as file:
|
||||||
cache_image = diskcache.Cache('./filecache_image', size_limit= 48 * 1024**3) # 48 GB limit
|
app_config = json.load(file)
|
||||||
cache_video = diskcache.Cache('./filecache_video', size_limit= 48 * 1024**3) # 48 GB limit
|
|
||||||
cache_other = diskcache.Cache('./filecache_other', size_limit= 48 * 1024**3) # 48 GB limit
|
cache_audio = diskcache.Cache('./filecache_audio', size_limit= app_config['filecache_size_limit_audio'] * 1024**3)
|
||||||
|
cache_image = diskcache.Cache('./filecache_image', size_limit= app_config['filecache_size_limit_image'] * 1024**3)
|
||||||
|
cache_video = diskcache.Cache('./filecache_video', size_limit= app_config['filecache_size_limit_video'] * 1024**3)
|
||||||
|
cache_other = diskcache.Cache('./filecache_other', size_limit= app_config['filecache_size_limit_other'] * 1024**3)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
||||||
|
|
||||||
app.config['SECRET_KEY'] = '85c1117eb3a5f2c79f0ff395bada8ff8d9a257b99ef5e143'
|
app.config['SECRET_KEY'] = app_config['SECRET_KEY']
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90)
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90)
|
||||||
if os.environ.get('FLASK_ENV') == 'production':
|
if os.environ.get('FLASK_ENV') == 'production':
|
||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
||||||
@ -65,7 +68,7 @@ thread_lock = threading.Lock()
|
|||||||
@lru_cache(maxsize=10)
|
@lru_cache(maxsize=10)
|
||||||
def get_cached_image(size):
|
def get_cached_image(size):
|
||||||
dimensions = tuple(map(int, size.split('-')[1].split('x')))
|
dimensions = tuple(map(int, size.split('-')[1].split('x')))
|
||||||
original_logo_path = os.path.join(app.root_path, 'static', 'logo.png')
|
original_logo_path = os.path.join(app.root_path, 'custom_logo', 'logoB.png')
|
||||||
|
|
||||||
with Image.open(original_logo_path) as img:
|
with Image.open(original_logo_path) as img:
|
||||||
img = img.convert("RGBA")
|
img = img.convert("RGBA")
|
||||||
@ -153,7 +156,7 @@ def generate_breadcrumbs(subpath=None):
|
|||||||
breadcrumbs.append({'name': part, 'path': path_accum})
|
breadcrumbs.append({'name': part, 'path': path_accum})
|
||||||
return breadcrumbs
|
return breadcrumbs
|
||||||
|
|
||||||
@app.route('/static/icons/<string:size>.png')
|
@app.route('/icon/<string:size>.png')
|
||||||
def serve_resized_icon(size):
|
def serve_resized_icon(size):
|
||||||
cached_image_bytes = get_cached_image(size)
|
cached_image_bytes = get_cached_image(size)
|
||||||
response = send_file(
|
response = send_file(
|
||||||
@ -163,6 +166,22 @@ def serve_resized_icon(size):
|
|||||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@app.route('/custom_logo/<string:filename>.png')
|
||||||
|
def custom_logo(filename):
|
||||||
|
file_path = os.path.join('custom_logo', f"{filename}.png")
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
abort(404)
|
||||||
|
with open(file_path, 'rb') as file:
|
||||||
|
image_data = file.read()
|
||||||
|
|
||||||
|
# Create a BytesIO object using the binary image data
|
||||||
|
image_io = io.BytesIO(image_data)
|
||||||
|
image_io.seek(0) # Important: reset the stream position
|
||||||
|
|
||||||
|
response = send_file(image_io, mimetype='image/png')
|
||||||
|
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||||
|
return response
|
||||||
|
|
||||||
@app.route('/sw.js')
|
@app.route('/sw.js')
|
||||||
def serve_sw():
|
def serve_sw():
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static'), 'sw.js', mimetype='application/javascript')
|
return send_from_directory(os.path.join(app.root_path, 'static'), 'sw.js', mimetype='application/javascript')
|
||||||
@ -218,95 +237,114 @@ def api_browse(subpath):
|
|||||||
@app.route("/media/<path:subpath>")
|
@app.route("/media/<path:subpath>")
|
||||||
@auth.require_secret
|
@auth.require_secret
|
||||||
def serve_file(subpath):
|
def serve_file(subpath):
|
||||||
|
# ─── 1) Locate the real file on disk ───
|
||||||
root, *relative_parts = subpath.split('/')
|
root, *relative_parts = subpath.split('/')
|
||||||
base_path = session['folders'][root]
|
base_path = session['folders'].get(root)
|
||||||
full_path = os.path.join(base_path, *relative_parts)
|
full_path = os.path.join(base_path or '', *relative_parts)
|
||||||
|
|
||||||
if not os.path.isfile(full_path):
|
if not os.path.isfile(full_path):
|
||||||
app.logger.error(f"File not found: {full_path}")
|
app.logger.error(f"File not found: {full_path}")
|
||||||
return "File not found", 404
|
return "File not found", 404
|
||||||
|
|
||||||
|
# ─── 2) Prep request info ───
|
||||||
mime, _ = mimetypes.guess_type(full_path)
|
mime, _ = mimetypes.guess_type(full_path)
|
||||||
mime = mime or 'application/octet-stream'
|
mime = mime or 'application/octet-stream'
|
||||||
range_header = request.headers.get('Range')
|
|
||||||
ip_address = request.remote_addr
|
|
||||||
user_agent = request.headers.get('User-Agent')
|
|
||||||
is_cache_request = request.headers.get('X-Cache-Request') == 'true'
|
is_cache_request = request.headers.get('X-Cache-Request') == 'true'
|
||||||
|
ip_address = request.remote_addr
|
||||||
|
user_agent = request.headers.get('User-Agent')
|
||||||
|
|
||||||
# Check cache first (using diskcache)
|
# skip logging on cache hits or on audio GETs (per your rules)
|
||||||
response = None
|
do_log = not is_cache_request
|
||||||
|
if mime == 'audio/mpeg' and request.method != 'HEAD':
|
||||||
|
do_log = False
|
||||||
|
|
||||||
# determine the cache to use based on the file type
|
# ─── 3) Pick the right cache ───
|
||||||
if mime and mime.startswith('audio/'):
|
if mime.startswith('audio/'): cache = cache_audio
|
||||||
cache = cache_audio
|
elif mime.startswith('image/'): cache = cache_image
|
||||||
elif mime and mime.startswith('image/'):
|
elif mime.startswith('video/'): cache = cache_video
|
||||||
cache = cache_image
|
else: cache = cache_other
|
||||||
elif mime and mime.startswith('video/'):
|
|
||||||
cache = cache_video
|
|
||||||
else:
|
|
||||||
cache = cache_other
|
|
||||||
|
|
||||||
# Check if the file is already cached
|
# ─── 4) Try to stream directly from diskcache ───
|
||||||
if is_cache_request:
|
try:
|
||||||
logging = False
|
# returns a file-like whose .name is the real path on disk
|
||||||
else:
|
with cache.read(subpath) as reader:
|
||||||
logging = True
|
file_path = reader.name
|
||||||
|
filesize = os.path.getsize(file_path)
|
||||||
|
response = send_file(
|
||||||
|
file_path,
|
||||||
|
mimetype= mime,
|
||||||
|
conditional= True # enable Range / If-Modified / etc
|
||||||
|
)
|
||||||
|
cached_hit = True
|
||||||
|
|
||||||
cached = cache.get(subpath)
|
except KeyError:
|
||||||
if cached:
|
# cache miss → generate & write back to cache
|
||||||
cached_file_bytes, mime = cached
|
cached_hit = False
|
||||||
cached_file = io.BytesIO(cached_file_bytes)
|
|
||||||
filesize = len(cached_file.getbuffer())
|
if mime.startswith('image/'):
|
||||||
response = send_file(cached_file, mimetype=mime)
|
# ─── 4a) Image branch: thumbnail & cache the JPEG ───
|
||||||
else:
|
|
||||||
if mime and mime.startswith('image/'):
|
|
||||||
# Image processing branch (with caching)
|
|
||||||
try:
|
try:
|
||||||
with Image.open(full_path) as img:
|
with Image.open(full_path) as img:
|
||||||
img.thumbnail((1920, 1920))
|
img.thumbnail((1920, 1920))
|
||||||
if img.mode in ("RGBA", "P"):
|
if img.mode in ("RGBA", "P"):
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
output_format = 'JPEG'
|
|
||||||
output_mime = 'image/jpeg'
|
|
||||||
save_kwargs = {'quality': 85}
|
|
||||||
|
|
||||||
img_bytes_io = io.BytesIO()
|
thumb_io = io.BytesIO()
|
||||||
img.save(img_bytes_io, format=output_format, **save_kwargs)
|
img.save(thumb_io, format='JPEG', quality=85)
|
||||||
thumb_bytes = img_bytes_io.getvalue()
|
thumb_io.seek(0)
|
||||||
filesize = len(thumb_bytes)
|
|
||||||
cache.set(subpath, (thumb_bytes, output_mime))
|
# write thumbnail into diskcache as a real file
|
||||||
response = send_file(io.BytesIO(thumb_bytes), mimetype=output_mime, conditional=True)
|
cache.set(subpath, thumb_io, read=True)
|
||||||
|
|
||||||
|
# now re-open from cache to get the on-disk path
|
||||||
|
with cache.read(subpath) as reader:
|
||||||
|
file_path = reader.name
|
||||||
|
filesize = os.path.getsize(file_path)
|
||||||
|
response = send_file(
|
||||||
|
file_path,
|
||||||
|
mimetype= 'image/jpeg',
|
||||||
|
conditional= True
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Image processing failed for {subpath}: {e}")
|
app.logger.error(f"Image processing failed for {subpath}: {e}")
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Cache non-image files: read bytes and cache
|
# ─── 4b) Non-image branch: cache original file ───
|
||||||
try:
|
try:
|
||||||
with open(full_path, 'rb') as f:
|
# store the real file on diskcache
|
||||||
file_bytes = f.read()
|
cache.set(subpath, open(full_path, 'rb'), read=True)
|
||||||
cache.set(subpath, (file_bytes, mime))
|
|
||||||
file_bytes_io = io.BytesIO(file_bytes)
|
# read back to get its path
|
||||||
filesize = len(file_bytes_io.getbuffer())
|
with cache.read(subpath) as reader:
|
||||||
response = send_file(file_bytes_io, mimetype=mime, conditional=True)
|
file_path = reader.name
|
||||||
|
filesize = os.path.getsize(file_path)
|
||||||
|
response = send_file(
|
||||||
|
file_path,
|
||||||
|
mimetype= mime,
|
||||||
|
conditional= True
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Failed to read file {subpath}: {e}")
|
app.logger.error(f"Failed to cache file {subpath}: {e}")
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
# Set Cache-Control header (browser caching for 1 day)
|
# ─── 5) Common headers & logging ───
|
||||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||||
|
|
||||||
# special rules for audio files.
|
if do_log:
|
||||||
# HEAD request checks if the audio file is available. GET requests coming from the audi player itself and can be made multiple times.
|
a.log_file_access(
|
||||||
# a HEAD request only for logging will be ignored because of rules before
|
subpath, filesize, mime,
|
||||||
if mime and mime.startswith('audio/mpeg') and request.method != 'HEAD':
|
ip_address, user_agent,
|
||||||
logging = False
|
session['device_id'], cached_hit
|
||||||
|
)
|
||||||
if logging:
|
|
||||||
a.log_file_access(subpath, filesize, mime, ip_address, user_agent, session['device_id'], bool(cached))
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/transcript/<path:subpath>")
|
@app.route("/transcript/<path:subpath>")
|
||||||
@auth.require_secret
|
@auth.require_secret
|
||||||
def get_transcript(subpath):
|
def get_transcript(subpath):
|
||||||
@ -322,61 +360,6 @@ def get_transcript(subpath):
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
return content, 200, {'Content-Type': 'text/markdown; charset=utf-8'}
|
return content, 200, {'Content-Type': 'text/markdown; charset=utf-8'}
|
||||||
|
|
||||||
@app.route("/crawl/<path:subpath>")
|
|
||||||
@auth.require_secret
|
|
||||||
def crawl_and_cache(subpath):
|
|
||||||
"""
|
|
||||||
Crawls through a directory and caches each file.
|
|
||||||
For images, it creates a thumbnail (max 1200x1200) and caches the processed image.
|
|
||||||
For non-images, it simply reads and caches the file bytes.
|
|
||||||
"""
|
|
||||||
root, *relative_parts = subpath.split('/')
|
|
||||||
base_path = session['folders'][root]
|
|
||||||
full_path = os.path.join(base_path, *relative_parts)
|
|
||||||
|
|
||||||
cached_files = [] # List to hold cached file relative paths
|
|
||||||
|
|
||||||
# Walk through all subdirectories and files
|
|
||||||
for root, dirs, files in os.walk(full_path):
|
|
||||||
for filename in files:
|
|
||||||
full_path_file = os.path.join(root, filename)
|
|
||||||
|
|
||||||
# Skip if this file is already in the cache
|
|
||||||
if cache.get(full_path_file):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Determine the MIME type
|
|
||||||
mime, _ = mimetypes.guess_type(full_path)
|
|
||||||
mime = mime or 'application/octet-stream'
|
|
||||||
|
|
||||||
# Process image files differently
|
|
||||||
if mime.startswith('image/'):
|
|
||||||
try:
|
|
||||||
with Image.open(full_path) as img:
|
|
||||||
# Create a thumbnail (max 1200x1200)
|
|
||||||
img.thumbnail((1200, 1200))
|
|
||||||
img_bytes_io = io.BytesIO()
|
|
||||||
# Save processed image as PNG
|
|
||||||
img.save(img_bytes_io, format='PNG', quality=85)
|
|
||||||
img_bytes = img_bytes_io.getvalue()
|
|
||||||
# Cache the processed image bytes along with its mime type
|
|
||||||
cache.set(full_path_file, (img_bytes, mime))
|
|
||||||
cached_files.append(full_path_file)
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"Image processing failed for {full_path_file}: {e}")
|
|
||||||
else:
|
|
||||||
# Process non-image files
|
|
||||||
try:
|
|
||||||
with open(full_path_file, 'rb') as f:
|
|
||||||
file_bytes = f.read()
|
|
||||||
cache.set(full_path_file, (file_bytes, mime))
|
|
||||||
cached_files.append(full_path_file)
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"Failed to read file {full_path_file}: {e}")
|
|
||||||
|
|
||||||
# Return the list of cached files as a JSON response
|
|
||||||
return json.dumps({"cached_files": cached_files}, indent=4), 200
|
|
||||||
|
|
||||||
def query_recent_connections():
|
def query_recent_connections():
|
||||||
global clients_connected, background_thread_running
|
global clients_connected, background_thread_running
|
||||||
background_thread_running = True
|
background_thread_running = True
|
||||||
@ -445,9 +428,20 @@ def handle_request_initial_data():
|
|||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
@auth.require_secret
|
@auth.require_secret
|
||||||
def index(path):
|
def index(path):
|
||||||
title_short = os.environ.get('TITLE_SHORT', 'Default Title')
|
title_short = app_config.get('TITLE_SHORT', 'Default Title')
|
||||||
title_long = os.environ.get('TITLE_LONG', 'Default Title')
|
title_long = app_config.get('TITLE_LONG' , 'Default Title')
|
||||||
return render_template("app.html", title_short=title_short, title_long=title_long)
|
header_color = app_config.get('header_color' , '#000')
|
||||||
|
header_text_color = app_config.get('header_text_color', '#fff')
|
||||||
|
background_color = app_config.get('background_color', '#fff')
|
||||||
|
main_text_color = app_config.get('main_text_color', '#000')
|
||||||
|
return render_template("app.html",
|
||||||
|
title_short=title_short,
|
||||||
|
title_long=title_long,
|
||||||
|
header_color=header_color,
|
||||||
|
header_text_color=header_text_color,
|
||||||
|
main_text_color=main_text_color,
|
||||||
|
background_color=background_color,
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
socketio.run(app, debug=True, host='0.0.0.0')
|
socketio.run(app, debug=True, host='0.0.0.0')
|
||||||
|
|||||||
19
auth.py
19
auth.py
@ -9,6 +9,9 @@ import base64
|
|||||||
|
|
||||||
folder_config = {}
|
folder_config = {}
|
||||||
|
|
||||||
|
with open("app_config.json", 'r') as file:
|
||||||
|
app_config = json.load(file)
|
||||||
|
|
||||||
def require_secret(f):
|
def require_secret(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
@ -79,8 +82,20 @@ def require_secret(f):
|
|||||||
session['device_id'] = os.urandom(32).hex()
|
session['device_id'] = os.urandom(32).hex()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return render_template('index.html')
|
title_short = app_config.get('TITLE_SHORT', 'Default Title')
|
||||||
|
title_long = app_config.get('TITLE_LONG' , 'Default Title')
|
||||||
|
header_color = app_config.get('header_color' , '#000')
|
||||||
|
header_text_color = app_config.get('header_text_color', '#fff')
|
||||||
|
background_color = app_config.get('background_color', '#fff')
|
||||||
|
main_text_color = app_config.get('main_text_color', '#000')
|
||||||
|
return render_template('index.html',
|
||||||
|
title_short=title_short,
|
||||||
|
title_long=title_long,
|
||||||
|
header_color=header_color,
|
||||||
|
header_text_color=header_text_color,
|
||||||
|
main_text_color=main_text_color,
|
||||||
|
background_color=background_color,
|
||||||
|
)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
@require_secret
|
@require_secret
|
||||||
|
|||||||
100
keygen.py
Normal file
100
keygen.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
salt = "UqEEf08yMDE3qIQodAGAyNQ2EPFhb26f2o85MTIyMeAAkqlANiXcF"
|
||||||
|
|
||||||
|
def generate_secret_key(identifier: str, expiry: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate a secret key with the following structure:
|
||||||
|
- encoded_data: Base64 encoding of (identifier + expiry)
|
||||||
|
- signature: 32 characters from the URL-safe base64 HMAC signature
|
||||||
|
- id_length_hex: 2-character hexadecimal number representing the length of the identifier
|
||||||
|
The HMAC is computed over (encoded_data + id_length_hex).
|
||||||
|
"""
|
||||||
|
global salt
|
||||||
|
if len(identifier) > 255:
|
||||||
|
raise ValueError("Identifier must be at most 255 characters long.")
|
||||||
|
if len(expiry) != 8:
|
||||||
|
raise ValueError("Expiry must be an 8-character string in DDMMYYYY format.")
|
||||||
|
|
||||||
|
# Concatenate identifier and expiry, then base64 encode the result.
|
||||||
|
plain_data = identifier + expiry
|
||||||
|
# Remove padding for compactness; we'll add it back during decoding.
|
||||||
|
encoded_data = base64.urlsafe_b64encode(plain_data.encode()).decode().rstrip("=")
|
||||||
|
|
||||||
|
# Format the length of the identifier as a 2-digit hexadecimal string.
|
||||||
|
id_length_hex = f"{len(identifier):02X}"
|
||||||
|
|
||||||
|
# Build the message for HMAC: encoded_data + id_length_hex.
|
||||||
|
message = encoded_data + id_length_hex
|
||||||
|
|
||||||
|
# Compute HMAC using SHA-256.
|
||||||
|
hmac_digest = hmac.new(salt.encode(), message.encode(), hashlib.sha256).digest()
|
||||||
|
|
||||||
|
# Get a URL-safe base64 representation and take the first 32 characters as the signature.
|
||||||
|
signature = base64.urlsafe_b64encode(hmac_digest).decode()[:32]
|
||||||
|
|
||||||
|
# Construct the final secret key.
|
||||||
|
secret_key = encoded_data + signature + id_length_hex
|
||||||
|
return secret_key
|
||||||
|
|
||||||
|
def decode_secret_key(secret_key: str):
|
||||||
|
"""
|
||||||
|
Decode the secret key and extract the identifier and expiry.
|
||||||
|
|
||||||
|
Token structure:
|
||||||
|
- encoded_data: secret_key[0 : (len(secret_key)-34)] (variable length)
|
||||||
|
- signature: secret_key[-34:-2] (32 characters)
|
||||||
|
- id_length_hex: secret_key[-2:] (2 characters)
|
||||||
|
|
||||||
|
The HMAC is verified over (encoded_data + id_length_hex). Then, encoded_data is base64-decoded
|
||||||
|
(with proper padding added) to yield (identifier + expiry), where the last 8 characters represent expiry.
|
||||||
|
The identifier length is determined from id_length_hex.
|
||||||
|
"""
|
||||||
|
global salt
|
||||||
|
|
||||||
|
if len(secret_key) < 34: # Must at least have signature (32) + id_length_hex (2)
|
||||||
|
raise ValueError("Invalid key length.")
|
||||||
|
|
||||||
|
# Extract the last 2 characters: id_length_hex.
|
||||||
|
id_length_hex = secret_key[-2:]
|
||||||
|
try:
|
||||||
|
id_length = int(id_length_hex, 16)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("Invalid identifier length prefix in key.")
|
||||||
|
|
||||||
|
# The signature is the 32 characters preceding the last 2.
|
||||||
|
signature = secret_key[-34:-2]
|
||||||
|
# The remainder is the encoded_data.
|
||||||
|
encoded_data = secret_key[:-34]
|
||||||
|
|
||||||
|
# Verify the signature.
|
||||||
|
message = encoded_data + id_length_hex
|
||||||
|
expected_hmac = hmac.new(salt.encode(), message.encode(), hashlib.sha256).digest()
|
||||||
|
expected_signature = base64.urlsafe_b64encode(expected_hmac).decode()[:32]
|
||||||
|
|
||||||
|
if not hmac.compare_digest(signature, expected_signature):
|
||||||
|
raise ValueError("Invalid key signature.")
|
||||||
|
|
||||||
|
# Base64-decode the encoded_data.
|
||||||
|
# Add back any missing '=' padding.
|
||||||
|
padding = '=' * (-len(encoded_data) % 4)
|
||||||
|
plain_data = base64.urlsafe_b64decode(encoded_data + padding).decode()
|
||||||
|
|
||||||
|
# plain_data should be (identifier + expiry), where expiry is the last 8 characters.
|
||||||
|
if len(plain_data) != id_length + 8:
|
||||||
|
raise ValueError("Decoded data length does not match expected identifier length and expiry.")
|
||||||
|
|
||||||
|
identifier = plain_data[:id_length]
|
||||||
|
expiry = plain_data[id_length:]
|
||||||
|
|
||||||
|
return identifier, expiry
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
key = generate_secret_key("Besondere Gottesdienste", "30042025")
|
||||||
|
print(key)
|
||||||
|
identifier, expiry = decode_secret_key(key)
|
||||||
|
print("identifier:", identifier)
|
||||||
|
print("expiry:", expiry)
|
||||||
63
migrate_cache.py
Normal file
63
migrate_cache.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
migrate_cache.py
|
||||||
|
|
||||||
|
Migrate DiskCache caches from “old” in‐memory entries (bytes, mime) into
|
||||||
|
“new” on‐disk read=True entries so that Flask can send them via send_file(path).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python migrate_cache.py /path/to/filecache_audio \
|
||||||
|
/path/to/filecache_image \
|
||||||
|
/path/to/filecache_video \
|
||||||
|
/path/to/filecache_other
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from diskcache import Cache
|
||||||
|
|
||||||
|
def migrate_cache(cache_path):
|
||||||
|
"""
|
||||||
|
Walks every key in the cache at `cache_path`. If the value is a tuple of
|
||||||
|
(bytes, mime), re‐writes it via read=True so DiskCache stores it as a file.
|
||||||
|
"""
|
||||||
|
print(f"➡ Migrating cache at {cache_path!r}")
|
||||||
|
cache = Cache(cache_path)
|
||||||
|
migrated = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
# Iterate keys without loading everything into memory
|
||||||
|
for key in cache.iterkeys():
|
||||||
|
try:
|
||||||
|
val = cache.get(key)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [ERROR] key={key!r} get failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detect old‐style entries: (bytes, mime)
|
||||||
|
if (
|
||||||
|
isinstance(val, tuple)
|
||||||
|
and len(val) == 2
|
||||||
|
and isinstance(val[0], (bytes, bytearray))
|
||||||
|
and isinstance(val[1], str)
|
||||||
|
):
|
||||||
|
data, mime = val
|
||||||
|
buf = io.BytesIO(data)
|
||||||
|
buf.seek(0)
|
||||||
|
# Re‐store as an on‐disk file
|
||||||
|
cache.set(key, buf, read=True)
|
||||||
|
migrated += 1
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
cache.close()
|
||||||
|
print(f" → Done: migrated={migrated}, skipped={skipped}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: migrate_cache.py <cache_dir1> [<cache_dir2> ...]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for directory in sys.argv[1:]:
|
||||||
|
migrate_cache(directory)
|
||||||
@ -7,9 +7,7 @@ html, body {
|
|||||||
/* Global Styles */
|
/* Global Styles */
|
||||||
body {
|
body {
|
||||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
background-color: #f4f7f9;
|
|
||||||
padding-top: 70px; /* Adjust to your header height */
|
padding-top: 70px; /* Adjust to your header height */
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
/* Header styles */
|
/* Header styles */
|
||||||
.site-header {
|
.site-header {
|
||||||
@ -20,8 +18,6 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #34495e; /* Using the theme color */
|
|
||||||
color: #eee;
|
|
||||||
}
|
}
|
||||||
.site-header img.logo {
|
.site-header img.logo {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
|||||||
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 183 KiB |
BIN
static/logoW.png
BIN
static/logoW.png
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
@ -7,12 +7,12 @@
|
|||||||
"theme_color": "#34495e",
|
"theme_color": "#34495e",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static/icons/logo-192x192.png",
|
"src": "/icon/logo-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/icons/logo-512x512.png",
|
"src": "/icon/logo-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,10 @@ const assets = [
|
|||||||
'/static/gallery.js',
|
'/static/gallery.js',
|
||||||
'/static/audioplayer.css',
|
'/static/audioplayer.css',
|
||||||
'/static/audioplayer.js',
|
'/static/audioplayer.js',
|
||||||
'/static/icons/logo-192x192.png',
|
'/icon/logo-192x192.png',
|
||||||
'/static/icons/logo-512x512.png',
|
'/icon/logo-512x512.png',
|
||||||
'/static/logo.png',
|
'/custom_logo/logoB.png',
|
||||||
'/static/logoW.png'
|
'/custom_logo/logoW.png'
|
||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener('install', e => {
|
self.addEventListener('install', e => {
|
||||||
|
|||||||
@ -3,23 +3,22 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
<meta property="og:title" content="Gottesdienste Speyer und Schwegenheim" />
|
<meta property="og:title" content="{{ title_long }}" />
|
||||||
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
||||||
<meta property="og:image" content="https://app.bethaus-speyer.de/static/icons/logo-200x200.png" />
|
<meta property="og:image" content="/icon/logo-200x200.png" />
|
||||||
<meta property="og:url" content="https://app.bethaus-speyer.de" />
|
|
||||||
|
|
||||||
<title>{{ title_short }}</title>
|
<title>{{ title_short }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
|
<meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
|
||||||
<meta name="author" content="Bethaus Speyer">
|
<meta name="author" content="{{ title_short }}">
|
||||||
<link rel="icon" href="/static/icons/logo-192x192.png" type="image/png" sizes="192x192">
|
<link rel="icon" href="/icon/logo-192x192.png" type="image/png" sizes="192x192">
|
||||||
|
|
||||||
|
|
||||||
<!-- Web App Manifest -->
|
<!-- Web App Manifest -->
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
|
|
||||||
<!-- Android Theme Color -->
|
<!-- Android Theme Color -->
|
||||||
<meta name="theme-color" content="#34495e">
|
<meta name="theme-color" content="{{ header_color }}">
|
||||||
|
|
||||||
<!-- Apple-specific tags -->
|
<!-- Apple-specific tags -->
|
||||||
<link rel="touch-icon" href="{{ url_for('static', filename='icons/icon-192x192.png') }}">
|
<link rel="touch-icon" href="{{ url_for('static', filename='icons/icon-192x192.png') }}">
|
||||||
@ -32,11 +31,21 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='gallery.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='gallery.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='audioplayer.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='audioplayer.css') }}">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: {{ background_color }};
|
||||||
|
color: {{ main_text_color }};
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
background-color: {{ header_color }};
|
||||||
|
color: {{ header_text_color }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<img src="/static/logoW.png" alt="Logo" class="logo">
|
<img src="/custom_logo/logoW.png" alt="Logo" class="logo">
|
||||||
</a>
|
</a>
|
||||||
<h1>{{ title_long }}</h1>
|
<h1>{{ title_long }}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -3,25 +3,33 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
<meta property="og:title" content="Gottesdienste Speyer und Schwegenheim" />
|
<meta property="og:title" content="{{ title_long }}" />
|
||||||
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
||||||
<meta property="og:image" content="https://app.bethaus-speyer.de/static/icons/logo-300x300.png" />
|
<meta property="og:image" content="/icon/logo-300x300.png" />
|
||||||
<meta property="og:url" content="https://app.bethaus-speyer.de" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
|
|
||||||
<title>Gottesdienste Speyer und Schwegenheim</title>
|
<title>{{ title_long }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: {{ background_color }};
|
||||||
|
color: {{ main_text_color }};
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
background-color: {{ header_color }};
|
||||||
|
color: {{ header_text_color }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<img src="https://app.bethaus-speyer.de/static/icons/logo-300x300.png" alt="Logo" class="logo">
|
<img src="/icon/logo-300x300.png" alt="Logo" class="logo">
|
||||||
<h1>Gottesdienste Speyer und Schwegenheim</h1>
|
<h1>{{ title_long }}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="content">Etwas ist schief gelaufen. Entweder ein Link ist veraltet oder die App ist gerade gestört.<br>Versuche den Link aus der Telegram-Gruppe erneut anklicken um dies zu beheben.</div>
|
<div id="content">Etwas ist schief gelaufen. Entweder ein Link ist veraltet oder die App ist gerade gestört.<br>Versuche den Freigabelink erneut anzuklicken.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -3,25 +3,33 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
<meta property="og:title" content="Gottesdienste Speyer und Schwegenheim" />
|
<meta property="og:title" content="{{ title_long }}" />
|
||||||
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
||||||
<meta property="og:image" content="https://app.bethaus-speyer.de/static/icons/logo-300x300.png" />
|
<meta property="og:image" content="/icon/logo-300x300.png" />
|
||||||
<meta property="og:url" content="https://app.bethaus-speyer.de" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
|
|
||||||
<title>Gottesdienste Speyer und Schwegenheim</title>
|
<title>{{ title_long }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: {{ background_color }};
|
||||||
|
color: {{ main_text_color }};
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
background-color: {{ header_color }};
|
||||||
|
color: {{ header_text_color }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<img src="https://app.bethaus-speyer.de/static/icons/logo-300x300.png" alt="Logo" class="logo">
|
<img src="/icon/logo-300x300.png" alt="Logo" class="logo">
|
||||||
<h1>Gottesdienste Speyer und Schwegenheim</h1>
|
<h1>{{ title_long }}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="content">Du hast keine gültigen Links im Speicher.<br>Bitte den Link aus der Telegram-Gruppe erneut anklicken.</div>
|
<div id="content">Du hast keine Links die noch gültig sind.<br>Bitte den Freigabelink erneut anklicken.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,14 +5,14 @@
|
|||||||
|
|
||||||
<meta property="og:title" content="Gottesdienste Speyer und Schwegenheim" />
|
<meta property="og:title" content="Gottesdienste Speyer und Schwegenheim" />
|
||||||
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
<meta property="og:description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft." />
|
||||||
<meta property="og:image" content="https://app.bethaus-speyer.de/static/icons/logo-200x200.png" />
|
<meta property="og:image" content="https://app.bethaus-speyer.de/icon/logo-200x200.png" />
|
||||||
<meta property="og:url" content="https://app.bethaus-speyer.de" />
|
<meta property="og:url" content="https://app.bethaus-speyer.de" />
|
||||||
|
|
||||||
<title>{{ title_short }}</title>
|
<title>{{ title_short }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
|
<meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
|
||||||
<meta name="author" content="Bethaus Speyer">
|
<meta name="author" content="Bethaus Speyer">
|
||||||
<link rel="icon" href="/static/icons/logo-192x192.png" type="image/png" sizes="192x192">
|
<link rel="icon" href="/icon/logo-192x192.png" type="image/png" sizes="192x192">
|
||||||
|
|
||||||
|
|
||||||
<!-- Web App Manifest -->
|
<!-- Web App Manifest -->
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="/static/logoW.png" alt="Logo" class="logo">
|
<img src="/custom_logo/logoW.png" alt="Logo" class="logo">
|
||||||
</a>
|
</a>
|
||||||
<h1>{{ title_long }}</h1>
|
<h1>{{ title_long }}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user