feat: media-browser: preview for local files
This commit is contained in:
@@ -143,7 +143,7 @@ in {
|
|||||||
|
|
||||||
maxUploadSize = lib.mkOption {
|
maxUploadSize = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "100M";
|
default = "2G";
|
||||||
description = ''
|
description = ''
|
||||||
Maximum file upload size accepted by Synapse and nginx.
|
Maximum file upload size accepted by Synapse and nginx.
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -207,11 +207,30 @@ def api_sync_status():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
def get_media_type_from_path(filepath):
|
||||||
|
try:
|
||||||
|
parts = filepath.split('/')
|
||||||
|
if len(parts) >= 4 and parts[0] == 'local_content':
|
||||||
|
media_id = parts[1] + parts[2] + parts[3]
|
||||||
|
conn = get_db_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT media_type FROM local_media_repository WHERE media_id = %s",
|
||||||
|
(media_id,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return row[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
@app.route('/view/local/<path:filepath>')
|
@app.route('/view/local/<path:filepath>')
|
||||||
def view_local(filepath):
|
def view_local(filepath):
|
||||||
try:
|
try:
|
||||||
safe_path = os.path.join(MEDIA_STORE_PATH, filepath)
|
safe_path = os.path.join(MEDIA_STORE_PATH, filepath)
|
||||||
# Security: ensure path is within MEDIA_STORE_PATH
|
|
||||||
real_path = os.path.realpath(safe_path)
|
real_path = os.path.realpath(safe_path)
|
||||||
real_base = os.path.realpath(MEDIA_STORE_PATH)
|
real_base = os.path.realpath(MEDIA_STORE_PATH)
|
||||||
if not real_path.startswith(real_base):
|
if not real_path.startswith(real_base):
|
||||||
@@ -220,7 +239,8 @@ def view_local(filepath):
|
|||||||
if not os.path.exists(real_path):
|
if not os.path.exists(real_path):
|
||||||
return 'Not found', 404
|
return 'Not found', 404
|
||||||
|
|
||||||
return send_file(real_path)
|
mimetype = get_media_type_from_path(filepath)
|
||||||
|
return send_file(real_path, mimetype=mimetype)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return str(e), 500
|
return str(e), 500
|
||||||
|
|
||||||
@@ -230,8 +250,16 @@ def view_s3(key):
|
|||||||
s3 = get_s3_client()
|
s3 = get_s3_client()
|
||||||
full_key = f"{S3_PREFIX}/{key}" if S3_PREFIX else key
|
full_key = f"{S3_PREFIX}/{key}" if S3_PREFIX else key
|
||||||
|
|
||||||
|
mimetype = None
|
||||||
|
if key.startswith('local_content/'):
|
||||||
|
mimetype = get_media_type_from_path(key)
|
||||||
|
|
||||||
|
params = {'Bucket': S3_BUCKET, 'Key': full_key}
|
||||||
|
if mimetype:
|
||||||
|
params['ResponseContentType'] = mimetype
|
||||||
|
|
||||||
url = s3.generate_presigned_url('get_object',
|
url = s3.generate_presigned_url('get_object',
|
||||||
Params={'Bucket': S3_BUCKET, 'Key': full_key},
|
Params=params,
|
||||||
ExpiresIn=3600)
|
ExpiresIn=3600)
|
||||||
|
|
||||||
return jsonify({'url': url})
|
return jsonify({'url': url})
|
||||||
@@ -338,6 +366,34 @@ HTML_TEMPLATE = '''
|
|||||||
.size { font-family: monospace; color: #8b949e; }
|
.size { font-family: monospace; color: #8b949e; }
|
||||||
.path { font-family: monospace; font-size: 0.85em; }
|
.path { font-family: monospace; font-size: 0.85em; }
|
||||||
#content { max-height: 70vh; overflow-y: auto; }
|
#content { max-height: 70vh; overflow-y: auto; }
|
||||||
|
.preview-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.preview-modal.active { display: flex; }
|
||||||
|
.preview-modal img, .preview-modal video {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.preview-modal audio { width: 500px; }
|
||||||
|
.preview-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px; right: 30px;
|
||||||
|
font-size: 2em; color: white;
|
||||||
|
cursor: pointer; background: none; border: none;
|
||||||
|
}
|
||||||
|
.preview-info {
|
||||||
|
color: #8b949e;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -376,6 +432,12 @@ HTML_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="preview-modal" class="preview-modal">
|
||||||
|
<button class="preview-close">×</button>
|
||||||
|
<div id="preview-container"></div>
|
||||||
|
<div id="preview-info" class="preview-info"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return "0 B";
|
||||||
@@ -436,7 +498,7 @@ HTML_TEMPLATE = '''
|
|||||||
<td>${row.last_access ? row.last_access.slice(0, 19).replace("T", " ") : "-"}</td>
|
<td>${row.last_access ? row.last_access.slice(0, 19).replace("T", " ") : "-"}</td>
|
||||||
<td>${row.upload_name || "-"}</td>
|
<td>${row.upload_name || "-"}</td>
|
||||||
<td>${status}${quarantined}</td>
|
<td>${status}${quarantined}</td>
|
||||||
<td><a href="/view/local/${row.local_path}" target="_blank">View</a></td>
|
<td><a href="#" onclick="showPreview('/view/local/${row.local_path}', '${row.media_type || ""}', '${row.upload_name || row.media_id}'); return false;">View</a></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += "</tbody></table>";
|
html += "</tbody></table>";
|
||||||
@@ -496,7 +558,7 @@ HTML_TEMPLATE = '''
|
|||||||
<td class="path">${f.path}</td>
|
<td class="path">${f.path}</td>
|
||||||
<td class="size">${formatBytes(f.size)}</td>
|
<td class="size">${formatBytes(f.size)}</td>
|
||||||
<td>${f.modified.slice(0, 19).replace("T", " ")}</td>
|
<td>${f.modified.slice(0, 19).replace("T", " ")}</td>
|
||||||
<td><a href="/view/local/${f.path}" target="_blank">View</a></td>
|
<td><a href="#" onclick="showPreview('/view/local/${f.path}', '', '${f.path}'); return false;">View</a></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += "</tbody></table>";
|
html += "</tbody></table>";
|
||||||
@@ -535,10 +597,36 @@ HTML_TEMPLATE = '''
|
|||||||
const r = await fetch("/view/s3/" + encodeURIComponent(key));
|
const r = await fetch("/view/s3/" + encodeURIComponent(key));
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
window.open(data.url, "_blank");
|
showPreview(data.url, '', key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPreview(url, mimetype, name) {
|
||||||
|
const modal = document.getElementById("preview-modal");
|
||||||
|
const container = document.getElementById("preview-container");
|
||||||
|
const info = document.getElementById("preview-info");
|
||||||
|
container.innerHTML = "";
|
||||||
|
info.textContent = name + (mimetype ? " (" + mimetype + ")" : "");
|
||||||
|
if (mimetype && mimetype.startsWith("image/")) {
|
||||||
|
container.innerHTML = `<img src="${url}" alt="${name}">`;
|
||||||
|
} else if (mimetype && mimetype.startsWith("video/")) {
|
||||||
|
container.innerHTML = `<video controls autoplay><source src="${url}" type="${mimetype}"></video>`;
|
||||||
|
} else if (mimetype && mimetype.startsWith("audio/")) {
|
||||||
|
container.innerHTML = `<audio controls autoplay src="${url}"></audio>`;
|
||||||
|
} else {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("preview-modal").addEventListener("click", function(e) {
|
||||||
|
if (e.target === this || e.target.classList.contains("preview-close")) {
|
||||||
|
this.classList.remove("active");
|
||||||
|
document.getElementById("preview-container").innerHTML = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loadStats();
|
loadStats();
|
||||||
loadMediaDb();
|
loadMediaDb();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user