nas-py/index.html
2026-05-15 18:00:40 +03:00

1041 lines
30 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Secure HTTPS File Transfer Server">
<title>📁 File Transfer - Secure & Fast</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #667eea;
--primary-dark: #764ba2;
--success: #27ae60;
--danger: #e74c3c;
--warning: #f39c12;
--info: #3498db;
--light: #ecf0f1;
--dark: #2c3e50;
--gray: #95a5a6;
--white: #ffffff;
--shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
}
html, body {
height: 100%;
width: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
color: var(--dark);
}
/* ============================
Main Container
============================ */
.container {
background: var(--white);
border-radius: 16px;
box-shadow: var(--shadow);
max-width: 850px;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ============================
Header
============================ */
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: var(--white);
padding: 30px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header-icon {
font-size: 40px;
margin-bottom: 10px;
}
.header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 5px;
letter-spacing: -0.5px;
}
.header p {
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
}
.status-badge {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
color: var(--white);
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
backdrop-filter: blur(10px);
}
.status-badge.online {
background: rgba(39, 174, 96, 0.3);
}
.status-badge.online::before {
content: "🔒 ";
}
/* ============================
Content Area
============================ */
.content {
flex: 1;
overflow-y: auto;
padding: 30px;
}
/* ============================
Alert Messages
============================ */
.alert {
padding: 14px 16px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
animation: slideDown 0.3s ease;
font-size: 14px;
border-left: 4px solid;
}
.alert.show {
display: block;
}
.alert.success {
background: #d4edda;
color: #155724;
border-left-color: var(--success);
}
.alert.error {
background: #f8d7da;
color: #721c24;
border-left-color: var(--danger);
}
.alert.info {
background: #d1ecf1;
color: #0c5460;
border-left-color: var(--info);
}
.alert-icon {
margin-right: 8px;
}
/* ============================
Sections
============================ */
.section {
margin-bottom: 30px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--primary);
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary);
}
.section-title-icon {
font-size: 18px;
}
/* ============================
Upload Area
============================ */
.upload-area {
border: 2px dashed var(--primary);
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
position: relative;
overflow: hidden;
}
.upload-area:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border-color: var(--primary-dark);
transform: translateY(-2px);
}
.upload-area.dragover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border-color: var(--primary-dark);
transform: scale(1.02);
}
.upload-area-icon {
font-size: 48px;
margin-bottom: 12px;
animation: float 3s ease-in-out infinite;
}
.upload-area p {
color: var(--dark);
margin: 8px 0;
font-weight: 500;
}
.upload-area-hint {
color: var(--gray);
font-size: 12px;
margin-top: 8px;
}
#fileInput {
display: none;
}
/* ============================
Buttons
============================ */
.btn {
display: inline-block;
padding: 10px 24px;
border-radius: 8px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
text-align: center;
white-space: nowrap;
user-select: none;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: var(--primary);
color: var(--white);
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--gray);
color: var(--white);
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-danger {
background: var(--danger);
color: var(--white);
}
.btn-danger:hover {
background: #c0392b;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
/* ============================
Progress Bar
============================ */
.progress {
width: 100%;
height: 6px;
background: var(--light);
border-radius: 3px;
margin-top: 15px;
overflow: hidden;
display: none;
}
.progress.active {
display: block;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-dark));
width: 0%;
transition: width 0.3s ease;
border-radius: 3px;
}
.progress-text {
font-size: 12px;
color: var(--gray);
margin-top: 8px;
text-align: right;
}
/* ============================
File List
============================ */
.file-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.file-item {
background: var(--light);
border-radius: 10px;
padding: 14px;
border-left: 4px solid var(--primary);
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 12px;
}
.file-item:hover {
box-shadow: var(--shadow-sm);
transform: translateX(4px);
}
.file-icon {
font-size: 24px;
min-width: 24px;
text-align: center;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
color: var(--dark);
word-break: break-all;
font-size: 14px;
margin-bottom: 4px;
}
.file-meta {
font-size: 12px;
color: var(--gray);
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.file-meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.no-files {
text-align: center;
padding: 30px 20px;
color: var(--gray);
font-size: 14px;
}
.no-files-icon {
font-size: 32px;
margin-bottom: 10px;
opacity: 0.5;
}
/* ============================
Stats
============================ */
.stats {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
border-radius: 10px;
padding: 16px;
margin-top: 20px;
border: 1px solid var(--primary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
text-align: center;
padding: 10px;
}
.stat-label {
font-size: 12px;
color: var(--gray);
margin-bottom: 4px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: var(--primary);
}
/* ============================
Footer
============================ */
.footer {
background: var(--light);
padding: 15px 20px;
text-align: center;
font-size: 12px;
color: var(--gray);
border-top: 1px solid #ddd;
}
.footer a {
color: var(--primary);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* ============================
Loading State
============================ */
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--light);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* ============================
Animations
============================ */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ============================
Responsive Design
============================ */
@media (max-width: 768px) {
.container {
max-height: 100vh;
border-radius: 0;
}
.header {
padding: 20px 15px;
}
.header h1 {
font-size: 24px;
}
.header-icon {
font-size: 32px;
}
.content {
padding: 20px;
}
.upload-area {
padding: 30px 15px;
}
.upload-area-icon {
font-size: 40px;
}
.file-item {
flex-wrap: wrap;
}
.file-actions {
width: 100%;
margin-top: 10px;
}
.file-actions .btn {
flex: 1;
min-width: 80px;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
.section-title {
font-size: 14px;
}
}
@media (max-width: 480px) {
body {
padding: 0;
}
.header {
padding: 15px 12px;
}
.header h1 {
font-size: 20px;
}
.content {
padding: 15px;
}
.upload-area {
padding: 25px 10px;
}
.upload-area p {
font-size: 14px;
}
.btn {
padding: 8px 16px;
font-size: 13px;
}
.file-name {
font-size: 13px;
}
.file-meta {
font-size: 11px;
gap: 8px;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
/* ============================
Print Styles
============================ */
@media print {
body {
background: white;
}
.container {
box-shadow: none;
max-height: none;
}
.upload-area {
border-style: solid;
}
.file-actions {
display: none;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-icon">📁</div>
<h1>File Transfer Server</h1>
<p>Secure HTTPS file upload and download</p>
<div class="status-badge online">Encrypted connection</div>
</div>
<!-- Main Content -->
<div class="content">
<!-- Alert Messages -->
<div id="alert" class="alert"></div>
<!-- Upload Section -->
<div class="section">
<div class="section-title">
<span class="section-title-icon">📤</span>
<span>Upload Files</span>
</div>
<div class="upload-area" id="uploadArea">
<div class="upload-area-icon">⬆️</div>
<p>Drag and drop files here</p>
<p class="upload-area-hint">or click to browse your files</p>
<button class="btn btn-primary btn-small" style="margin-top: 10px;">Choose Files</button>
</div>
<input type="file" id="fileInput" multiple accept="*/*">
<div class="progress" id="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="progress-text" id="progressText"></div>
</div>
<!-- Download Section -->
<div class="section">
<div class="section-title">
<span class="section-title-icon">📥</span>
<span>Download Files</span>
</div>
<div class="file-list" id="fileList">
<div class="no-files">
<div class="no-files-icon">📭</div>
<p>Loading files...</p>
</div>
</div>
<div class="stats" id="stats" style="display: none;">
<div class="stats-grid" id="statsGrid"></div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<span id="status">Ready</span>
<a href="#" onclick="refreshFiles(); return false;">Refresh</a>
<span id="lastUpdate">Never</span>
</div>
</div>
<script>
// ====================================================================
// State Management
// ====================================================================
const state = {
uploading: false,
files: [],
lastUpdate: null,
stats: {
totalFiles: 0,
totalSize: 0
}
};
// ====================================================================
// DOM Elements
// ====================================================================
const elements = {
uploadArea: document.getElementById('uploadArea'),
fileInput: document.getElementById('fileInput'),
fileList: document.getElementById('fileList'),
alert: document.getElementById('alert'),
progress: document.getElementById('progress'),
progressBar: document.getElementById('progressBar'),
progressText: document.getElementById('progressText'),
stats: document.getElementById('stats'),
statsGrid: document.getElementById('statsGrid'),
status: document.getElementById('status'),
lastUpdate: document.getElementById('lastUpdate')
};
// ====================================================================
// Alert System
// ====================================================================
function showAlert(message, type = 'info') {
const icons = {
success: '✅',
error: '❌',
info: '',
warning: '⚠️'
};
elements.alert.innerHTML = `
<span class="alert-icon">${icons[type]}</span>
${escapeHtml(message)}
`;
elements.alert.className = `alert show ${type}`;
setTimeout(() => {
elements.alert.classList.remove('show');
}, 4000);
}
// ====================================================================
// File Management
// ====================================================================
async function refreshFiles() {
try {
elements.status.textContent = 'Loading...';
const response = await fetch('/api/files');
const data = await response.json();
state.files = data.files;
state.stats = {
totalFiles: data.total_files,
totalSize: data.files.reduce((sum, f) => sum + f.size, 0)
};
renderFileList();
renderStats();
updateLastUpdate();
elements.status.textContent = 'Ready';
} catch (error) {
console.error('Error fetching files:', error);
elements.status.textContent = 'Error loading files';
showAlert('Failed to load files', 'error');
}
}
function renderFileList() {
if (state.files.length === 0) {
elements.fileList.innerHTML = `
<div class="no-files">
<div class="no-files-icon">📭</div>
<p>No files uploaded yet</p>
</div>
`;
elements.stats.style.display = 'none';
return;
}
elements.fileList.innerHTML = state.files.map(file => {
const icon = getFileIcon(file.name);
const date = new Date(file.modified);
const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return `
<div class="file-item">
<div class="file-icon">${icon}</div>
<div class="file-info">
<div class="file-name">${escapeHtml(file.name)}</div>
<div class="file-meta">
<div class="file-meta-item">📊 ${file.size_human}</div>
<div class="file-meta-item">📅 ${dateStr}</div>
</div>
</div>
<div class="file-actions">
<a href="/download?file=${encodeURIComponent(file.name)}"
class="btn btn-primary btn-small"
download="${file.name}">
⬇️ Download
</a>
<button class="btn btn-danger btn-small"
onclick="deleteFile('${escapeHtml(file.name)}')">
🗑️ Delete
</button>
</div>
</div>
`;
}).join('');
elements.stats.style.display = 'block';
}
function renderStats() {
elements.statsGrid.innerHTML = `
<div class="stat-item">
<div class="stat-label">Total Files</div>
<div class="stat-value">${state.stats.totalFiles}</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Size</div>
<div class="stat-value">${formatBytes(state.stats.totalSize)}</div>
</div>
<div class="stat-item">
<div class="stat-label">Avg File Size</div>
<div class="stat-value">${state.stats.totalFiles > 0 ? formatBytes(state.stats.totalSize / state.stats.totalFiles) : '—'}</div>
</div>
`;
}
function updateLastUpdate() {
const now = new Date();
elements.lastUpdate.textContent = `${now.toLocaleTimeString()}`;
state.lastUpdate = now;
}
async function deleteFile(filename) {
if (!confirm(`Delete "${filename}"?`)) return;
try {
elements.status.textContent = 'Deleting...';
const response = await fetch(`/delete?file=${encodeURIComponent(filename)}`, {
method: 'POST'
});
if (response.ok) {
showAlert(`File deleted: ${filename}`, 'success');
await refreshFiles();
} else {
showAlert(`Error deleting ${filename}`, 'error');
}
} catch (error) {
console.error('Delete error:', error);
showAlert('Delete failed', 'error');
} finally {
elements.status.textContent = 'Ready';
}
}
// ====================================================================
// File Upload
// ====================================================================
function setupUploadArea() {
elements.uploadArea.addEventListener('click', () => {
elements.fileInput.click();
});
elements.uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
elements.uploadArea.classList.add('dragover');
});
elements.uploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
elements.uploadArea.classList.remove('dragover');
});
elements.uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
elements.uploadArea.classList.remove('dragover');
uploadFiles(e.dataTransfer.files);
});
elements.fileInput.addEventListener('change', (e) => {
uploadFiles(e.target.files);
});
}
async function uploadFiles(files) {
if (files.length === 0 || state.uploading) return;
state.uploading = true;
elements.progress.classList.add('active');
let uploaded = 0;
let failed = 0;
for (const file of files) {
try {
await uploadFile(file);
uploaded++;
} catch (error) {
console.error('Upload error:', error);
failed++;
}
}
state.uploading = false;
elements.progress.classList.remove('active');
elements.progressText.textContent = '';
elements.fileInput.value = '';
if (failed === 0) {
showAlert(`${uploaded} file${uploaded !== 1 ? 's' : ''} uploaded successfully`, 'success');
} else {
showAlert(`${uploaded} succeeded, ${failed} failed`, 'warning');
}
await refreshFiles();
}
function uploadFile(file) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
elements.progressBar.style.width = percentComplete + '%';
elements.progressText.textContent = `${Math.round(percentComplete)}% • ${formatBytes(e.loaded)} / ${formatBytes(e.total)}`;
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload error'));
});
xhr.addEventListener('abort', () => {
reject(new Error('Upload aborted'));
});
xhr.open('POST', '/upload');
xhr.send(formData);
});
}
// ====================================================================
// Utility Functions
// ====================================================================
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 10) / 10 + ' ' + sizes[i];
}
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const icons = {
'pdf': '📄',
'doc': '📄', 'docx': '📄',
'xls': '📊', 'xlsx': '📊',
'ppt': '📊', 'pptx': '📊',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
'mp4': '🎬', 'avi': '🎬', 'mov': '🎬', 'mkv': '🎬',
'mp3': '🎵', 'wav': '🎵', 'flac': '🎵', 'aac': '🎵',
'zip': '🗜️', 'rar': '🗜️', '7z': '🗜️', 'tar': '🗜️', 'gz': '🗜️',
'txt': '📝', 'md': '📝', 'log': '📝',
'exe': '⚙️', 'app': '⚙️', 'jar': '⚙️',
'html': '🌐', 'css': '🌐', 'js': '🌐', 'json': '🌐',
'py': '🐍', 'java': '☕', 'cpp': '⚙️', 'c': '⚙️',
};
return icons[ext] || '📦';
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// ====================================================================
// Initialization
// ====================================================================
function init() {
setupUploadArea();
refreshFiles();
// Refresh file list every 5 seconds
setInterval(refreshFiles, 5000);
// Handle visibility change
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
refreshFiles();
}
});
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>