Merge remote-tracking branch 'origin/development'
This commit is contained in:
commit
236b08518b
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
/filecache
|
||||
/filecache_audio
|
||||
/filecache_image
|
||||
/filecache_image_full
|
||||
/filecache_video
|
||||
/filecache_other
|
||||
/postgres_data
|
||||
|
||||
79
app.py
79
app.py
@ -40,10 +40,11 @@ from collections import OrderedDict
|
||||
app_config = auth.return_app_config()
|
||||
BASE_DIR = os.path.realpath(app_config['BASE_DIR'])
|
||||
|
||||
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)
|
||||
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_image_full = diskcache.Cache('./filecache_image_full', size_limit=app_config.get('filecache_size_limit_image_full', 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)
|
||||
|
||||
_logged_request_ids = OrderedDict()
|
||||
_logged_request_ids_lock = threading.Lock()
|
||||
@ -897,14 +898,56 @@ def serve_file(subpath):
|
||||
|
||||
# 4) Image and thumbnail handling first
|
||||
if mime.startswith('image/'):
|
||||
small = request.args.get('thumbnail') == 'true'
|
||||
small = request.args.get('thumbnail') == 'true'
|
||||
download = request.args.get('download') == 'true'
|
||||
name, ext = os.path.splitext(subpath)
|
||||
orig_key = subpath
|
||||
orig_key = subpath
|
||||
small_key = f"{name}_small{ext}"
|
||||
|
||||
# --- Download path: always serve original full-size file from cache_image_full ---
|
||||
if download:
|
||||
try:
|
||||
with cache_image_full.read(orig_key) as reader:
|
||||
full_file_path = reader.name
|
||||
cached_hit = True
|
||||
except KeyError:
|
||||
cached_hit = False
|
||||
try:
|
||||
with open(full_path, 'rb') as source:
|
||||
cache_image_full.set(orig_key, source, read=True)
|
||||
with cache_image_full.read(orig_key) as reader:
|
||||
full_file_path = reader.name
|
||||
except Exception as e:
|
||||
app.logger.error(f"Full-size image cache fill failed for {subpath}: {e}")
|
||||
abort(503, description="Service temporarily unavailable - cache population failed")
|
||||
|
||||
response = send_file(
|
||||
full_file_path,
|
||||
mimetype=mime,
|
||||
as_attachment=True,
|
||||
download_name=os.path.basename(orig_key)
|
||||
)
|
||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||
if do_log:
|
||||
if not _is_duplicate_request(req_id):
|
||||
a.log_file_access(
|
||||
orig_key,
|
||||
os.path.getsize(full_file_path),
|
||||
mime,
|
||||
ip_address,
|
||||
user_agent,
|
||||
device_id,
|
||||
cached_hit,
|
||||
request.method
|
||||
)
|
||||
_mark_request_logged(req_id)
|
||||
return response
|
||||
|
||||
# --- Display path (view / thumbnail) ---
|
||||
cache_key = small_key if small else orig_key
|
||||
|
||||
try:
|
||||
# Try to read the requested variant
|
||||
# Try to read the requested display variant
|
||||
with cache.read(cache_key) as reader:
|
||||
file_path = reader.name
|
||||
cached_hit = True
|
||||
@ -912,11 +955,10 @@ def serve_file(subpath):
|
||||
if small: # do not create when thumbnail requested
|
||||
response = make_response('', 204)
|
||||
return response
|
||||
|
||||
cached_hit = False
|
||||
# On miss: generate both full-size and small thumb, then cache
|
||||
with Image.open(full_path) as orig:
|
||||
|
||||
cached_hit = False
|
||||
# On miss: generate both display variants, then cache
|
||||
with Image.open(full_path) as orig:
|
||||
img = ImageOps.exif_transpose(orig)
|
||||
exif_bytes = None
|
||||
try:
|
||||
@ -942,16 +984,25 @@ def serve_file(subpath):
|
||||
thumb.save(bio, **save_kwargs)
|
||||
bio.seek(0)
|
||||
cache.set(key, bio, read=True)
|
||||
# Read back the variant we need
|
||||
|
||||
# Also store the original file in cache_image_full (best-effort, avoids re-reads on later downloads)
|
||||
try:
|
||||
if orig_key not in cache_image_full:
|
||||
with open(full_path, 'rb') as source:
|
||||
cache_image_full.set(orig_key, source, read=True)
|
||||
except Exception as e:
|
||||
app.logger.warning(f"Full-size image cache fill failed for {subpath}: {e}")
|
||||
|
||||
# Read back the display variant we need
|
||||
with cache.read(cache_key) as reader:
|
||||
file_path = reader.name
|
||||
|
||||
# Serve the image variant
|
||||
# Serve the display image variant
|
||||
response = send_file(
|
||||
file_path,
|
||||
mimetype=mime,
|
||||
conditional=True,
|
||||
as_attachment=(request.args.get('download') == 'true'),
|
||||
as_attachment=False,
|
||||
download_name=os.path.basename(orig_key)
|
||||
)
|
||||
response.headers['Content-Disposition'] = 'inline'
|
||||
|
||||
@ -31,10 +31,11 @@ from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
# -------- Config (override with env vars if needed) --------
|
||||
CACHE_DIRS = {
|
||||
"audio": os.environ.get("FILECACHE_AUDIO", "./filecache_audio"),
|
||||
"image": os.environ.get("FILECACHE_IMAGE", "./filecache_image"),
|
||||
"video": os.environ.get("FILECACHE_VIDEO", "./filecache_video"),
|
||||
"other": os.environ.get("FILECACHE_OTHER", "./filecache_other"),
|
||||
"audio": os.environ.get("FILECACHE_AUDIO", "./filecache_audio"),
|
||||
"image": os.environ.get("FILECACHE_IMAGE", "./filecache_image"),
|
||||
"image_full": os.environ.get("FILECACHE_IMAGE_FULL", "./filecache_image_full"),
|
||||
"video": os.environ.get("FILECACHE_VIDEO", "./filecache_video"),
|
||||
"other": os.environ.get("FILECACHE_OTHER", "./filecache_other"),
|
||||
}
|
||||
|
||||
# Column heuristics (actual names in your DB appear to include: access_time, expire_time, size, key)
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"BASE_DIR": "/mnt",
|
||||
"filecache_size_limit_audio": 16,
|
||||
"filecache_size_limit_image": 16,
|
||||
"filecache_size_limit_image_full": 16,
|
||||
"filecache_size_limit_video": 16,
|
||||
"filecache_size_limit_other": 16
|
||||
}
|
||||
|
||||
@ -1294,44 +1294,44 @@ footer .audio-player-container {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--brand-navy), var(--dark-background));
|
||||
border: 1px solid var(--brand-navy);
|
||||
color: #e5e7eb;
|
||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.18);
|
||||
background: linear-gradient(135deg, var(--brand-sky), #60a5fa);
|
||||
border: 1px solid var(--brand-sky);
|
||||
color: #f8fafc;
|
||||
box-shadow: 0 6px 14px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1b2a44, #0d1524);
|
||||
border-color: #1b2a44;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
|
||||
background: linear-gradient(135deg, #2563eb, var(--brand-sky));
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 8px 18px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active,
|
||||
.btn-primary:focus {
|
||||
background: linear-gradient(135deg, #0d1524, #0a1020);
|
||||
border-color: #0d1524;
|
||||
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.22);
|
||||
color: #e5e7eb;
|
||||
background: linear-gradient(135deg, #1d4ed8, #2563eb);
|
||||
border-color: #1d4ed8;
|
||||
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #1f2e4a, #0f172a);
|
||||
border: 1px solid #1f2e4a;
|
||||
color: #e5e7eb;
|
||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.18);
|
||||
background: linear-gradient(135deg, #60a5fa, #3b82f6);
|
||||
border: 1px solid #3b82f6;
|
||||
color: #f8fafc;
|
||||
box-shadow: 0 6px 14px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus {
|
||||
background: linear-gradient(135deg, #223456, #111a2f);
|
||||
border-color: #223456;
|
||||
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.2);
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 8px 16px rgba(59, 130, 246, 0.28);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: linear-gradient(135deg, #0c1220, #0a0f1a);
|
||||
border-color: #0c1220;
|
||||
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.22);
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
border-color: #1d4ed8;
|
||||
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.32);
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
.gallery-nav:focus,
|
||||
.gallery-nav:active,
|
||||
#gallery-modal-content,
|
||||
#gallery-close {
|
||||
#gallery-close,
|
||||
#gallery-download {
|
||||
-webkit-tap-highlight-color: transparent; /* Disable blue highlight on Android/iOS/Chrome/Safari */
|
||||
outline: none; /* Disable outline on click/focus */
|
||||
}
|
||||
@ -115,6 +116,29 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#gallery-download {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 64px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
z-index: 120;
|
||||
font-size: 1.25rem;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#gallery-download:hover {
|
||||
background: rgba(40, 40, 40, 0.85);
|
||||
}
|
||||
|
||||
#gallery-info.loading {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
@ -189,6 +189,22 @@ function updateFilenameDisplay(relUrl) {
|
||||
filenameEl.textContent = filename;
|
||||
}
|
||||
|
||||
function updateDownloadLink(relUrl) {
|
||||
const downloadBtn = document.getElementById('gallery-download');
|
||||
if (!downloadBtn) {
|
||||
return;
|
||||
}
|
||||
if (!relUrl) {
|
||||
downloadBtn.removeAttribute('href');
|
||||
downloadBtn.removeAttribute('download');
|
||||
return;
|
||||
}
|
||||
const parts = relUrl.split('/');
|
||||
const filename = parts[parts.length - 1] || relUrl;
|
||||
downloadBtn.href = '/media/' + encodeGalleryPath(relUrl) + '?download=true';
|
||||
downloadBtn.setAttribute('download', filename);
|
||||
}
|
||||
|
||||
function updateNextButtonState() {
|
||||
const nextButton = document.querySelector('.gallery-next');
|
||||
if (!nextButton) {
|
||||
@ -220,7 +236,7 @@ function showGalleryImage(relUrl) {
|
||||
const currentImage = document.getElementById('gallery-modal-content');
|
||||
const loader = document.getElementById('loader-container');
|
||||
const closeBtn = document.getElementById('gallery-close');
|
||||
|
||||
updateDownloadLink(relUrl);
|
||||
// Set a timeout for showing the loader after 500ms.
|
||||
let loaderTimeout = setTimeout(() => {
|
||||
loader.style.display = 'flex';
|
||||
@ -321,6 +337,7 @@ function closeGalleryModal() {
|
||||
document.getElementById('gallery-modal').style.display = 'none';
|
||||
isGalleryLoading = false;
|
||||
setAutoPlay(false);
|
||||
updateDownloadLink(null);
|
||||
// remove all images
|
||||
const existingImages = document.querySelectorAll('#gallery-modal img');
|
||||
existingImages.forEach(img => {
|
||||
@ -579,6 +596,13 @@ function initGalleryControls() {
|
||||
closeButton.addEventListener('click', closeGalleryModal);
|
||||
}
|
||||
|
||||
const downloadButton = document.getElementById('gallery-download');
|
||||
if (downloadButton) {
|
||||
downloadButton.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
if (infoButton) {
|
||||
infoButton.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
@ -206,6 +206,7 @@
|
||||
<div id="gallery-modal" style="display: none;">
|
||||
<button id="gallery-close">x</button>
|
||||
<button id="gallery-info" aria-expanded="false" aria-controls="gallery-exif">i</button>
|
||||
<a id="gallery-download" aria-label="Download original image" title="Download original">⇩</a>
|
||||
<img id="gallery-modal-content" src="">
|
||||
<button class="gallery-nav gallery-prev">‹</button>
|
||||
<button class="gallery-nav gallery-next">›</button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user