implement multisecret function
This commit is contained in:
parent
5ace803cc0
commit
663d5b50f9
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,4 +2,4 @@
|
|||||||
/filecache
|
/filecache
|
||||||
/__pycache__
|
/__pycache__
|
||||||
/access_log.db
|
/access_log.db
|
||||||
/allowed_secrets.json
|
/folder_config.json
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"dev_key_f83745ft0g5rg3": {
|
|
||||||
"expiry" : "31.12.2000",
|
|
||||||
"file_root": "\\\\path\\if\\using\\windows"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
192
app.py
192
app.py
@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory
|
from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory, abort
|
||||||
import os
|
import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
@ -12,7 +12,7 @@ import geoip2.database
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from urllib.parse import urlparse, unquote
|
from urllib.parse import urlparse, unquote
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
cache = diskcache.Cache('./filecache', size_limit= 48 * 1024**3) # 32 GB limit
|
cache = diskcache.Cache('./filecache', size_limit= 48 * 1024**3) # 48 GB limit
|
||||||
|
|
||||||
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)
|
||||||
@ -23,58 +23,74 @@ if os.environ.get('FLASK_ENV') == 'production':
|
|||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
||||||
app.config['SESSION_COOKIE_SECURE'] = True
|
app.config['SESSION_COOKIE_SECURE'] = True
|
||||||
|
|
||||||
def load_allowed_secrets(filename='allowed_secrets.json'):
|
with open('folder_config.json') as file:
|
||||||
with open(filename) as f:
|
app.config['folder_config'] = json.load(file)
|
||||||
secrets = json.load(f)
|
|
||||||
for key, value in secrets.items():
|
|
||||||
if 'expiry' in value:
|
|
||||||
value['expiry'] = datetime.strptime(value['expiry'], '%d.%m.%Y').date()
|
|
||||||
return secrets
|
|
||||||
|
|
||||||
def require_secret(f):
|
def require_secret(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
allowed_secrets = load_allowed_secrets()
|
# Your config list:
|
||||||
today = date.today()
|
folder_config = app.config['folder_config']
|
||||||
|
|
||||||
def is_valid(secret_data):
|
def is_valid(config_item, provided_secret):
|
||||||
expiry_date = secret_data.get('expiry')
|
"""
|
||||||
is_valid = expiry_date and today <= expiry_date
|
Checks if today's date is <= validity date
|
||||||
return is_valid
|
AND if the provided secret matches config_item['secret'].
|
||||||
|
"""
|
||||||
|
folder_validity = config_item['validity']
|
||||||
|
# Convert string to a date if necessary:
|
||||||
|
if isinstance(folder_validity, str):
|
||||||
|
folder_validity = datetime.strptime(folder_validity, '%d.%m.%Y').date()
|
||||||
|
|
||||||
# Check if a secret was provided via GET parameter
|
# Return whether it's still valid and secrets match:
|
||||||
get_secret = request.args.get('secret')
|
return (
|
||||||
if get_secret is not None:
|
date.today() <= folder_validity and
|
||||||
secret_data = allowed_secrets.get(get_secret)
|
provided_secret == config_item['secret']
|
||||||
if secret_data:
|
)
|
||||||
if is_valid(secret_data):
|
|
||||||
# Valid secret provided in URL: update session and config
|
|
||||||
session['secret'] = get_secret
|
|
||||||
session.permanent = True
|
|
||||||
app.config['FILE_ROOT'] = secret_data.get('file_root')
|
|
||||||
print("session:", session['secret'])
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
else:
|
|
||||||
# Secret provided via URL is expired or invalid
|
|
||||||
return render_template('error.html', message="Invalid or expired secret."), 403
|
|
||||||
|
|
||||||
|
# 1) Get secret from query params (if any)
|
||||||
|
args_secret = request.args.get('secret')
|
||||||
|
|
||||||
|
# 2) Initialize 'allowed_secrets' in the session if missing
|
||||||
|
if 'allowed_secrets' not in session:
|
||||||
|
session['allowed_secrets'] = []
|
||||||
|
|
||||||
# If no secret provided via GET, check the session
|
# 3) If a new secret is provided, check if it’s valid, and add to session if so
|
||||||
session_secret = session.get('secret')
|
if args_secret:
|
||||||
if session_secret is not None:
|
for config_item in folder_config:
|
||||||
secret_data = allowed_secrets.get(session_secret)
|
if is_valid(config_item, args_secret):
|
||||||
if secret_data:
|
if args_secret not in session['allowed_secrets']:
|
||||||
if is_valid(secret_data):
|
session['allowed_secrets'].append(args_secret)
|
||||||
session.permanent = True
|
session.permanent = True # Make the session permanent
|
||||||
app.config['FILE_ROOT'] = secret_data.get('file_root')
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
else:
|
|
||||||
# Session secret exists but is expired
|
|
||||||
return render_template('error.html', message="Invalid or expired secret."), 403
|
|
||||||
|
|
||||||
# No secret provided at all; show the public index page
|
# 4) Re-check validity of each secret in session['allowed_secrets']
|
||||||
return render_template('index.html')
|
# If a secret is no longer valid (or not in config), remove it.
|
||||||
|
for secret_in_session in session['allowed_secrets'][:]:
|
||||||
|
# Find the current config item with matching secret
|
||||||
|
config_item = next(
|
||||||
|
(c for c in folder_config if c['secret'] == secret_in_session),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
# If the config item doesn’t exist or is invalid, remove secret
|
||||||
|
if config_item is None or not is_valid(config_item, secret_in_session):
|
||||||
|
session['allowed_secrets'].remove(secret_in_session)
|
||||||
|
|
||||||
|
# 5) Build session['folders'] fresh from the valid secrets
|
||||||
|
session['folders'] = {}
|
||||||
|
for secret_in_session in session.get('allowed_secrets', []):
|
||||||
|
config_item = next(
|
||||||
|
(c for c in folder_config if c['secret'] == secret_in_session),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if config_item:
|
||||||
|
for folder_info in config_item['folders']:
|
||||||
|
session['folders'][folder_info['foldername']] = folder_info['folderpath']
|
||||||
|
|
||||||
|
# 6) If we have folders, proceed; otherwise show index
|
||||||
|
if session['folders']:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
@ -165,7 +181,7 @@ def list_directory_contents(directory, subpath):
|
|||||||
file_entry['has_transcript'] = True
|
file_entry['has_transcript'] = True
|
||||||
transcript_rel_path = os.path.join(subpath, "Transkription", transcript_filename) if subpath else os.path.join("Transkription", transcript_filename)
|
transcript_rel_path = os.path.join(subpath, "Transkription", transcript_filename) if subpath else os.path.join("Transkription", transcript_filename)
|
||||||
transcript_rel_path = transcript_rel_path.replace(os.sep, '/')
|
transcript_rel_path = transcript_rel_path.replace(os.sep, '/')
|
||||||
file_entry['transcript_url'] = url_for('get_transcript', filename=transcript_rel_path)
|
file_entry['transcript_url'] = url_for('get_transcript', subpath=transcript_rel_path)
|
||||||
else:
|
else:
|
||||||
file_entry['has_transcript'] = False
|
file_entry['has_transcript'] = False
|
||||||
else:
|
else:
|
||||||
@ -176,7 +192,7 @@ def list_directory_contents(directory, subpath):
|
|||||||
return directories, files
|
return directories, files
|
||||||
|
|
||||||
|
|
||||||
def generate_breadcrumbs(subpath):
|
def generate_breadcrumbs(subpath=None):
|
||||||
breadcrumbs = [{'name': 'Home', 'path': ''}]
|
breadcrumbs = [{'name': 'Home', 'path': ''}]
|
||||||
if subpath:
|
if subpath:
|
||||||
parts = subpath.split('/')
|
parts = subpath.split('/')
|
||||||
@ -191,8 +207,20 @@ def generate_breadcrumbs(subpath):
|
|||||||
@app.route('/api/path/<path:subpath>')
|
@app.route('/api/path/<path:subpath>')
|
||||||
@require_secret
|
@require_secret
|
||||||
def api_browse(subpath):
|
def api_browse(subpath):
|
||||||
file_root = app.config['FILE_ROOT']
|
if subpath == '': # root directory
|
||||||
directory = os.path.join(file_root, subpath.replace('/', os.sep))
|
foldernames = []
|
||||||
|
for foldername, folderpath in session['folders'].items():
|
||||||
|
foldernames.append({'name': foldername, 'path': foldername})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'breadcrumbs': generate_breadcrumbs(),
|
||||||
|
'directories': foldernames,
|
||||||
|
'files': []
|
||||||
|
})
|
||||||
|
|
||||||
|
root, *relative_parts = subpath.split('/')
|
||||||
|
base_path = session['folders'][root]
|
||||||
|
directory = os.path.join(base_path, *relative_parts)
|
||||||
|
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
return jsonify({'error': 'Directory not found'}), 404
|
return jsonify({'error': 'Directory not found'}), 404
|
||||||
@ -399,11 +427,12 @@ def log_file_access(full_path):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@app.route("/media/<path:filename>")
|
@app.route("/media/<path:subpath>")
|
||||||
@require_secret
|
@require_secret
|
||||||
def serve_file(filename):
|
def serve_file(subpath):
|
||||||
decoded_filename = unquote(filename).replace('/', os.sep)
|
root, *relative_parts = subpath.split('/')
|
||||||
full_path = os.path.normpath(os.path.join(app.config['FILE_ROOT'], decoded_filename))
|
base_path = session['folders'][root]
|
||||||
|
full_path = os.path.join(base_path, *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}")
|
||||||
@ -424,7 +453,7 @@ def serve_file(filename):
|
|||||||
|
|
||||||
# Check cache first (using diskcache)
|
# Check cache first (using diskcache)
|
||||||
response = None
|
response = None
|
||||||
cached = cache.get(filename)
|
cached = cache.get(subpath)
|
||||||
if cached:
|
if cached:
|
||||||
cached_file_bytes, mime = cached
|
cached_file_bytes, mime = cached
|
||||||
cached_file = io.BytesIO(cached_file_bytes)
|
cached_file = io.BytesIO(cached_file_bytes)
|
||||||
@ -438,20 +467,20 @@ def serve_file(filename):
|
|||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='PNG', quality=85)
|
img.save(img_bytes, format='PNG', quality=85)
|
||||||
img_bytes = img_bytes.getvalue()
|
img_bytes = img_bytes.getvalue()
|
||||||
cache.set(filename, (img_bytes, mime))
|
cache.set(subpath, (img_bytes, mime))
|
||||||
response = send_file(io.BytesIO(img_bytes), mimetype=mime)
|
response = send_file(io.BytesIO(img_bytes), mimetype=mime)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Image processing failed for {filename}: {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
|
# Cache non-image files: read bytes and cache
|
||||||
try:
|
try:
|
||||||
with open(full_path, 'rb') as f:
|
with open(full_path, 'rb') as f:
|
||||||
file_bytes = f.read()
|
file_bytes = f.read()
|
||||||
cache.set(filename, (file_bytes, mime))
|
cache.set(subpath, (file_bytes, mime))
|
||||||
response = send_file(io.BytesIO(file_bytes), mimetype=mime)
|
response = send_file(io.BytesIO(file_bytes), mimetype=mime)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Failed to read file {filename}: {e}")
|
app.logger.error(f"Failed to read file {subpath}: {e}")
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
# Set Cache-Control header (browser caching for 1 day)
|
# Set Cache-Control header (browser caching for 1 day)
|
||||||
@ -459,11 +488,13 @@ def serve_file(filename):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.route("/transcript/<path:filename>")
|
@app.route("/transcript/<path:subpath>")
|
||||||
@require_secret
|
@require_secret
|
||||||
def get_transcript(filename):
|
def get_transcript(subpath):
|
||||||
fs_filename = filename.replace('/', os.sep)
|
|
||||||
full_path = os.path.join(app.config['FILE_ROOT'], fs_filename)
|
root, *relative_parts = subpath.split('/')
|
||||||
|
base_path = session['folders'][root]
|
||||||
|
full_path = os.path.join(base_path, *relative_parts)
|
||||||
|
|
||||||
if not os.path.isfile(full_path):
|
if not os.path.isfile(full_path):
|
||||||
return "Transcription not found", 404
|
return "Transcription not found", 404
|
||||||
@ -472,34 +503,27 @@ def get_transcript(filename):
|
|||||||
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:start_relative_path>")
|
@app.route("/crawl/<path:subpath>")
|
||||||
@require_secret
|
@require_secret
|
||||||
def crawl_and_cache(start_relative_path):
|
def crawl_and_cache(subpath):
|
||||||
"""
|
"""
|
||||||
Crawls through a directory (relative to app.config['FILE_ROOT']) and caches each file.
|
Crawls through a directory and caches each file.
|
||||||
For images, it creates a thumbnail (max 1200x1200) and caches the processed image.
|
For images, it creates a thumbnail (max 1200x1200) and caches the processed image.
|
||||||
For non-images, it simply reads and caches the file bytes.
|
For non-images, it simply reads and caches the file bytes.
|
||||||
|
|
||||||
:param start_relative_path: The folder (relative to FILE_ROOT) to start crawling.
|
|
||||||
"""
|
"""
|
||||||
# Compute the absolute path for the starting directory
|
root, *relative_parts = subpath.split('/')
|
||||||
base_dir = os.path.normpath(os.path.join(app.config['FILE_ROOT'], start_relative_path))
|
base_path = session['folders'][root]
|
||||||
|
full_path = os.path.join(base_path, *relative_parts)
|
||||||
# Check that base_dir is under FILE_ROOT to prevent directory traversal
|
|
||||||
if not base_dir.startswith(os.path.abspath(app.config['FILE_ROOT'])):
|
|
||||||
return jsonify({"error": "Invalid path"}), 400
|
|
||||||
|
|
||||||
cached_files = [] # List to hold cached file relative paths
|
cached_files = [] # List to hold cached file relative paths
|
||||||
|
|
||||||
# Walk through all subdirectories and files
|
# Walk through all subdirectories and files
|
||||||
for root, dirs, files in os.walk(base_dir):
|
for root, dirs, files in os.walk(full_path):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
full_path = os.path.join(root, filename)
|
full_path_file = os.path.join(root, filename)
|
||||||
# Compute the relative key used for caching
|
|
||||||
rel_key = os.path.relpath(full_path, app.config['FILE_ROOT'])
|
|
||||||
|
|
||||||
# Skip if this file is already in the cache
|
# Skip if this file is already in the cache
|
||||||
if cache.get(rel_key):
|
if cache.get(full_path_file):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Determine the MIME type
|
# Determine the MIME type
|
||||||
@ -517,19 +541,19 @@ def crawl_and_cache(start_relative_path):
|
|||||||
img.save(img_bytes_io, format='PNG', quality=85)
|
img.save(img_bytes_io, format='PNG', quality=85)
|
||||||
img_bytes = img_bytes_io.getvalue()
|
img_bytes = img_bytes_io.getvalue()
|
||||||
# Cache the processed image bytes along with its mime type
|
# Cache the processed image bytes along with its mime type
|
||||||
cache.set(rel_key, (img_bytes, mime))
|
cache.set(full_path_file, (img_bytes, mime))
|
||||||
cached_files.append(rel_key)
|
cached_files.append(full_path_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Image processing failed for {rel_key}: {e}")
|
app.logger.error(f"Image processing failed for {full_path_file}: {e}")
|
||||||
else:
|
else:
|
||||||
# Process non-image files
|
# Process non-image files
|
||||||
try:
|
try:
|
||||||
with open(full_path, 'rb') as f:
|
with open(full_path_file, 'rb') as f:
|
||||||
file_bytes = f.read()
|
file_bytes = f.read()
|
||||||
cache.set(rel_key, (file_bytes, mime))
|
cache.set(full_path_file, (file_bytes, mime))
|
||||||
cached_files.append(rel_key)
|
cached_files.append(full_path_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Failed to read file {rel_key}: {e}")
|
app.logger.error(f"Failed to read file {full_path_file}: {e}")
|
||||||
|
|
||||||
# Return the list of cached files as a JSON response
|
# Return the list of cached files as a JSON response
|
||||||
return json.dumps({"cached_files": cached_files}, indent=4), 200
|
return json.dumps({"cached_files": cached_files}, indent=4), 200
|
||||||
|
|||||||
13
folder_config.json.example.json
Normal file
13
folder_config.json.example.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"secret" : "dev_key_f83745ft0g5rg3",
|
||||||
|
"validity" : "31.12.2030",
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"foldername": "My Folder",
|
||||||
|
"folderpath": "\\\\path\\if\\using\\windows"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
x
Reference in New Issue
Block a user