adding download helper and viewer
Deploy / deploy (push) Successful in 38s

This commit is contained in:
2026-05-12 10:08:17 +02:00
parent 67f31800f3
commit b69e603791
5 changed files with 139 additions and 6 deletions
+54
View File
@@ -0,0 +1,54 @@
<?php
session_start();
$loggedin = isset($_SESSION['neptune_loggedin']) && $_SESSION['neptune_loggedin'] === true;
if (!$loggedin) {
http_response_code(401);
echo 'Unauthorized';
exit;
}
$file = $_GET['file'] ?? '';
$mode = $_GET['mode'] ?? 'download';
if (!$file || preg_match('/[^a-zA-Z0-9_\.\-]/', $file)) {
http_response_code(400);
echo 'Invalid file';
exit;
}
$path = '/var/www/uploads/' . basename($file);
if (!file_exists($path)) {
http_response_code(404);
echo 'File not found';
exit;
}
require_once __DIR__ . '/config/database.php';
$db = getDbConnection();
$stmt = $db->prepare("SELECT original_name, mime_type FROM file_attachments WHERE stored_name = ?");
$stmt->execute([basename($file)]);
$att = $stmt->fetch(PDO::FETCH_ASSOC);
$originalName = $att ? $att['original_name'] : basename($file);
$mimeType = $att && $att['mime_type'] ? $att['mime_type'] : mime_content_type($path);
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$viewable = in_array($ext, ['txt', 'md', 'pdf', 'csv']);
if ($mode === 'view' && $viewable) {
header('Content-Disposition: inline; filename="' . $originalName . '"');
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . filesize($path));
header('X-File-Name: ' . $originalName);
header('X-File-Viewable: 1');
readfile($path);
exit;
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $originalName . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: no-cache');
readfile($path);
+9 -1
View File
@@ -16,7 +16,15 @@ server {
location /uploads/ { location /uploads/ {
alias /var/www/uploads/; alias /var/www/uploads/;
add_header Content-Disposition 'inline'; internal;
}
location /download/ {
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME /var/www/backend/download.php;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
include fastcgi_params;
} }
location / { location / {
+11
View File
@@ -282,4 +282,15 @@ kbd {
.attachment-item a:hover { .attachment-item a:hover {
color: #60a5fa; color: #60a5fa;
text-decoration: underline !important; text-decoration: underline !important;
}
/* File Viewer */
#fileViewerModal .modal-content {
border-color: var(--neptune-border);
}
#fileViewerBody pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
} }
+47 -5
View File
@@ -588,13 +588,20 @@ async function renderAttachments(eventId, container) {
container.innerHTML = attachments.map(a => { container.innerHTML = attachments.map(a => {
const icon = getFileIcon(a.mime_type, a.original_name); const icon = getFileIcon(a.mime_type, a.original_name);
const size = formatFileSize(a.file_size); const size = formatFileSize(a.file_size);
const ext = a.original_name.split('.').pop().toLowerCase();
const viewable = ['txt', 'md', 'csv'].includes(ext);
const href = viewable ? '#' : '/download/?file=' + encodeURIComponent(a.stored_name) + '&mode=download';
const onclick = viewable ? ` onclick="event.preventDefault();openFileViewer('${esc(a.stored_name)}','${esc(a.original_name)}')"` : '';
return `<div class="attachment-item d-flex align-items-center justify-content-between py-1"> return `<div class="attachment-item d-flex align-items-center justify-content-between py-1">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center" style="min-width:0;">
<i class="fas ${icon} me-2" style="font-size:.85rem;"></i> <i class="fas ${icon} me-2" style="font-size:.85rem;flex-shrink:0;"></i>
<a href="/uploads/${a.stored_name}" target="_blank" class="small text-decoration-none" style="word-break:break-all;">${esc(a.original_name)}</a> <a href="${href}"${onclick} target="_blank" class="small text-decoration-none text-truncate" style="color:var(--neptune-accent);">${esc(a.original_name)}</a>
<small class="text-secondary ms-2">(${size})</small> <small class="text-secondary ms-2 flex-shrink-0">(${size})</small>
</div>
<div class="d-flex align-items-center gap-1 flex-shrink-0">
<a href="/download/?file=${encodeURIComponent(a.stored_name)}&mode=download" class="btn btn-outline-primary btn-sm py-0 px-1" title="Download" style="font-size:.6rem;"><i class="fas fa-download"></i></a>
<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteAttachment(${a.id}, this)" title="Delete file" style="font-size:.6rem;"><i class="fas fa-times"></i></button>
</div> </div>
<button class="btn btn-outline-danger btn-sm py-0 px-1 ms-2" onclick="deleteAttachment(${a.id}, this)" title="Delete file" style="font-size:.65rem;"><i class="fas fa-times"></i></button>
</div>`; </div>`;
}).join(''); }).join('');
} }
@@ -618,6 +625,41 @@ function getFileIcon(mime, name) {
return 'fa-file'; return 'fa-file';
} }
function openFileViewer(storedName, originalName) {
const modalEl = document.getElementById('fileViewerModal');
document.getElementById('fileViewerName').textContent = originalName;
document.getElementById('fileViewerDownloadBtn').onclick = () => {
window.open('/download/?file=' + encodeURIComponent(storedName) + '&mode=download', '_blank');
};
const body = document.getElementById('fileViewerBody');
body.innerHTML = '<div class="text-center text-secondary py-5"><div class="spinner-border" role="status"></div><p class="mt-2 small">Loading file...</p></div>';
const modal = new bootstrap.Modal(modalEl);
modal.show();
const ext = originalName.split('.').pop().toLowerCase();
if (ext === 'pdf') {
body.innerHTML = `<iframe src="/download/?file=${encodeURIComponent(storedName)}&mode=view" style="width:100%;min-height:80vh;border:none;"></iframe>`;
return;
}
fetch('/download/?file=' + encodeURIComponent(storedName) + '&mode=view')
.then(r => {
if (!r.ok) throw new Error('Failed to load');
return r.text();
})
.then(text => {
if (ext === 'md') {
body.innerHTML = '<div class="p-3" style="white-space:pre-wrap;font-family:var(--bs-font-monospace);font-size:.85rem;line-height:1.6;color:#e2e8f0;">' + esc(text) + '</div>';
} else {
body.innerHTML = '<div class="p-3" style="white-space:pre-wrap;font-family:var(--bs-font-monospace);font-size:.85rem;line-height:1.6;color:#e2e8f0;">' + esc(text) + '</div>';
}
})
.catch(() => {
body.innerHTML = '<div class="text-center text-danger py-5"><i class="fas fa-exclamation-circle fs-2 mb-2"></i><p class="small">Failed to load file</p></div>';
});
}
async function saveEvent() { async function saveEvent() {
const tags = parseEventTags(); const tags = parseEventTags();
const data = { const data = {
+18
View File
@@ -527,6 +527,24 @@
</div> </div>
</div> </div>
<!-- ==================== FILE VIEWER MODAL ==================== -->
<div class="modal fade" id="fileViewerModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary py-2">
<h6 class="modal-title" id="fileViewerTitle"><i class="fas fa-file me-1"></i> <span id="fileViewerName"></span></h6>
<div class="d-flex align-items-center gap-1">
<button type="button" class="btn btn-outline-primary btn-sm" id="fileViewerDownloadBtn"><i class="fas fa-download me-1"></i>Download</button>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="modal-body p-0" id="fileViewerBody" style="min-height:60vh;max-height:80vh;overflow:auto;background:#0d1117;">
<div class="text-center text-secondary py-5"><div class="spinner-border" role="status"></div><p class="mt-2 small">Loading file...</p></div>
</div>
</div>
</div>
</div>
<!-- ==================== LOGIN OVERLAY ==================== --> <!-- ==================== LOGIN OVERLAY ==================== -->
<div id="loginOverlay" style="position:fixed;top:0;left:0;width:100%;height:100%;background:#0a0e1a;z-index:9999;display:flex;align-items:center;justify-content:center;"> <div id="loginOverlay" style="position:fixed;top:0;left:0;width:100%;height:100%;background:#0a0e1a;z-index:9999;display:flex;align-items:center;justify-content:center;">
<div style="text-align:center;max-width:400px;padding:2rem;"> <div style="text-align:center;max-width:400px;padding:2rem;">