564 lines
No EOL
20 KiB
HTML
564 lines
No EOL
20 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Upload Files - Flask Files{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/upload.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="upload-container">
|
|
<div class="upload-header">
|
|
<h2>Upload Files</h2>
|
|
<div class="upload-location">
|
|
<span>Uploading to:</span>
|
|
{% if parent_folder %}
|
|
<a href="{{ url_for('files.browser', folder_id=parent_folder.id) }}">{{ parent_folder.name }}</a>
|
|
{% else %}
|
|
<a href="{{ url_for('files.browser') }}">Root</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-dropzone" id="dropzone">
|
|
<div class="upload-icon-wrapper">
|
|
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
|
</div>
|
|
<div class="upload-text">
|
|
<p class="upload-primary-text">Drag & drop files or folders here</p>
|
|
<p class="upload-secondary-text">or</p>
|
|
<div class="upload-buttons">
|
|
<label class="btn primary">
|
|
<i class="fas fa-file"></i> Select Files
|
|
<input type="file" name="files[]" multiple id="file-input" style="display: none">
|
|
</label>
|
|
<label class="btn">
|
|
<i class="fas fa-folder"></i> Select Folder
|
|
<input type="file" name="folders[]" webkitdirectory directory id="folder-input"
|
|
style="display: none">
|
|
</label>
|
|
</div>
|
|
<p class="upload-hint">Files will upload automatically when dropped or selected</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-progress-container" id="progress-container" style="display: none;">
|
|
<h3>Upload Progress</h3>
|
|
<div class="progress-overall">
|
|
<div class="progress-header">
|
|
<span>Overall Progress</span>
|
|
<span id="progress-percentage">0%</span>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar" id="progress-bar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-stats">
|
|
<div class="stat-item">
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
<span id="upload-speed">0 KB/s</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<i class="fas fa-file-upload"></i>
|
|
<span id="uploaded-size">0 KB / 0 KB</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<i class="fas fa-clock"></i>
|
|
<span id="time-remaining">calculating...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-list" id="upload-list">
|
|
<h3>Files (<span id="file-count">0</span>)</h3>
|
|
<div id="file-items" class="file-items"></div>
|
|
<div id="empty-message" class="empty-message">
|
|
<p>No files selected yet</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-actions">
|
|
<a href="{{ url_for('files.browser', folder_id=parent_folder.id if parent_folder else None) }}" class="btn">
|
|
Cancel
|
|
</a>
|
|
<button id="clear-button" class="btn" disabled>Clear All</button>
|
|
</div>
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// DOM Elements
|
|
const dropzone = document.getElementById('dropzone');
|
|
const fileInput = document.getElementById('file-input');
|
|
const folderInput = document.getElementById('folder-input');
|
|
const uploadList = document.getElementById('file-items');
|
|
const emptyMessage = document.getElementById('empty-message');
|
|
const fileCount = document.getElementById('file-count');
|
|
const progressContainer = document.getElementById('progress-container');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const progressPercentage = document.getElementById('progress-percentage');
|
|
const uploadSpeed = document.getElementById('upload-speed');
|
|
const uploadedSize = document.getElementById('uploaded-size');
|
|
const timeRemaining = document.getElementById('time-remaining');
|
|
const clearButton = document.getElementById('clear-button');
|
|
|
|
// Upload tracking
|
|
let uploadQueue = [];
|
|
let currentUploads = 0;
|
|
let totalUploaded = 0;
|
|
let totalSize = 0;
|
|
let uploadStartTime = 0;
|
|
let lastUploadedBytes = 0;
|
|
let uploadUpdateInterval = null;
|
|
const MAX_CONCURRENT_UPLOADS = 3;
|
|
const folderId = {{ parent_folder.id if parent_folder else 'null' }
|
|
};
|
|
});
|
|
|
|
// Setup event listeners
|
|
dropzone.addEventListener('dragover', function (e) {
|
|
e.preventDefault();
|
|
this.classList.add('highlight');
|
|
});
|
|
|
|
dropzone.addEventListener('dragleave', function (e) {
|
|
e.preventDefault();
|
|
this.classList.remove('highlight');
|
|
});
|
|
|
|
dropzone.addEventListener('drop', function (e) {
|
|
e.preventDefault();
|
|
this.classList.remove('highlight');
|
|
|
|
// Handle dropped files
|
|
const items = e.dataTransfer.items;
|
|
if (items && items.length > 0) {
|
|
// Check if this is a folder drop from file explorer
|
|
const containsDirectories = Array.from(items).some(item => {
|
|
return item.webkitGetAsEntry && item.webkitGetAsEntry().isDirectory;
|
|
});
|
|
|
|
if (containsDirectories) {
|
|
processDroppedItems(items);
|
|
} else {
|
|
handleFiles(e.dataTransfer.files);
|
|
}
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener('change', function () {
|
|
handleFiles(this.files);
|
|
});
|
|
|
|
folderInput.addEventListener('change', function () {
|
|
handleFolderUpload(this.files);
|
|
});
|
|
|
|
clearButton.addEventListener('click', function () {
|
|
resetUploadState();
|
|
});
|
|
|
|
function processDroppedItems(items) {
|
|
const fileList = [];
|
|
let pendingDirectories = 0;
|
|
|
|
function traverseFileTree(entry, path = '') {
|
|
if (entry.isFile) {
|
|
entry.file(file => {
|
|
file.relativePath = path + file.name;
|
|
fileList.push(file);
|
|
if (pendingDirectories === 0 && entry.isFile) {
|
|
handleFiles(fileList);
|
|
}
|
|
});
|
|
} else if (entry.isDirectory) {
|
|
pendingDirectories++;
|
|
const dirReader = entry.createReader();
|
|
const readEntries = () => {
|
|
dirReader.readEntries(entries => {
|
|
if (entries.length > 0) {
|
|
for (let i = 0; i < entries.length; i++) {
|
|
traverseFileTree(entries[i], path + entry.name + '/');
|
|
}
|
|
readEntries(); // Continue reading if there might be more entries
|
|
} else {
|
|
pendingDirectories--;
|
|
if (pendingDirectories === 0) {
|
|
handleFiles(fileList);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
readEntries();
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
const entry = items[i].webkitGetAsEntry();
|
|
if (entry) {
|
|
traverseFileTree(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleFiles(files) {
|
|
if (!files || files.length === 0) return;
|
|
|
|
clearButton.disabled = false;
|
|
emptyMessage.style.display = 'none';
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
|
|
// Add to queue
|
|
uploadQueue.push({
|
|
file: file,
|
|
relativePath: file.relativePath || null,
|
|
status: 'queued',
|
|
progress: 0
|
|
});
|
|
|
|
totalSize += file.size;
|
|
|
|
// Add to UI
|
|
const fileItem = document.createElement('div');
|
|
fileItem.className = 'file-item';
|
|
fileItem.dataset.index = uploadQueue.length - 1;
|
|
|
|
// Determine icon based on file type
|
|
const fileIcon = getFileIcon(file.name);
|
|
|
|
fileItem.innerHTML = `
|
|
<div class="file-icon">
|
|
<i class="fas ${fileIcon}"></i>
|
|
</div>
|
|
<div class="file-info">
|
|
<div class="file-name">${file.name}</div>
|
|
<div class="file-path">${file.relativePath || 'No path'}</div>
|
|
<div class="file-size">${formatSize(file.size)}</div>
|
|
<div class="file-progress">
|
|
<div class="progress-bar-small" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="file-status">
|
|
<span class="status-indicator queued">Queued</span>
|
|
</div>
|
|
`;
|
|
|
|
uploadList.appendChild(fileItem);
|
|
}
|
|
|
|
fileCount.textContent = uploadQueue.length;
|
|
|
|
// Start upload process
|
|
startUpload();
|
|
}
|
|
|
|
function handleFolderUpload(files) {
|
|
if (!files || files.length === 0) return;
|
|
|
|
const fileArray = Array.from(files);
|
|
for (let i = 0; i < fileArray.length; i++) {
|
|
const file = fileArray[i];
|
|
file.relativePath = file.webkitRelativePath;
|
|
}
|
|
|
|
handleFiles(fileArray);
|
|
}
|
|
|
|
function startUpload() {
|
|
if (uploadQueue.length === 0 || currentUploads >= MAX_CONCURRENT_UPLOADS) return;
|
|
|
|
if (currentUploads === 0) {
|
|
// First upload - initialize tracking
|
|
uploadStartTime = Date.now();
|
|
lastUploadedBytes = 0;
|
|
progressContainer.style.display = 'block';
|
|
|
|
// Start progress update interval
|
|
uploadUpdateInterval = setInterval(updateUploadStats, 500);
|
|
}
|
|
|
|
// Find next queued file
|
|
const nextIndex = uploadQueue.findIndex(item => item.status === 'queued');
|
|
if (nextIndex === -1) return;
|
|
|
|
// Start uploading this file
|
|
uploadQueue[nextIndex].status = 'uploading';
|
|
currentUploads++;
|
|
|
|
// Update UI
|
|
const fileItem = document.querySelector(`.file-item[data-index="${nextIndex}"]`);
|
|
const statusIndicator = fileItem.querySelector('.status-indicator');
|
|
statusIndicator.className = 'status-indicator uploading';
|
|
statusIndicator.textContent = 'Uploading';
|
|
|
|
// Create FormData
|
|
const formData = new FormData();
|
|
formData.append('file', uploadQueue[nextIndex].file);
|
|
formData.append('folder_id', folderId || '');
|
|
|
|
if (uploadQueue[nextIndex].relativePath) {
|
|
formData.append('relative_path', uploadQueue[nextIndex].relativePath);
|
|
}
|
|
|
|
// Create and configure XHR
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.upload.addEventListener('progress', function (e) {
|
|
if (e.lengthComputable) {
|
|
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
|
|
|
// Update file progress
|
|
uploadQueue[nextIndex].progress = percentComplete;
|
|
|
|
// Update file UI
|
|
const progressBar = fileItem.querySelector('.progress-bar-small');
|
|
progressBar.style.width = percentComplete + '%';
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('load', function () {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
const response = JSON.parse(xhr.responseText);
|
|
|
|
if (response.success) {
|
|
// Upload successful
|
|
uploadQueue[nextIndex].status = 'complete';
|
|
statusIndicator.className = 'status-indicator complete';
|
|
statusIndicator.textContent = 'Complete';
|
|
|
|
totalUploaded += uploadQueue[nextIndex].file.size;
|
|
} else {
|
|
// Upload failed on server
|
|
uploadQueue[nextIndex].status = 'error';
|
|
statusIndicator.className = 'status-indicator error';
|
|
statusIndicator.textContent = 'Error: ' + (response.error || 'Server Error');
|
|
}
|
|
} catch (e) {
|
|
// JSON parse error
|
|
uploadQueue[nextIndex].status = 'error';
|
|
statusIndicator.className = 'status-indicator error';
|
|
statusIndicator.textContent = 'Error: Invalid response';
|
|
}
|
|
} else {
|
|
// HTTP error
|
|
uploadQueue[nextIndex].status = 'error';
|
|
statusIndicator.className = 'status-indicator error';
|
|
statusIndicator.textContent = 'Error: ' + xhr.status;
|
|
}
|
|
|
|
// One upload completed
|
|
currentUploads--;
|
|
|
|
// Check if all uploads complete
|
|
if (uploadQueue.every(item => item.status !== 'queued' && item.status !== 'uploading')) {
|
|
// All uploads complete
|
|
clearInterval(uploadUpdateInterval);
|
|
|
|
// Show completion notification
|
|
const successCount = uploadQueue.filter(item => item.status === 'complete').length;
|
|
const errorCount = uploadQueue.filter(item => item.status === 'error').length;
|
|
|
|
// Show notification message
|
|
if (errorCount === 0) {
|
|
showNotification(`Successfully uploaded ${successCount} files`, 'success');
|
|
} else {
|
|
showNotification(`Uploaded ${successCount} files, ${errorCount} failed`, 'warning');
|
|
}
|
|
} else {
|
|
// Start next upload
|
|
startUpload();
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', function () {
|
|
// Network error
|
|
uploadQueue[nextIndex].status = 'error';
|
|
statusIndicator.className = 'status-indicator error';
|
|
statusIndicator.textContent = 'Error: Network error';
|
|
|
|
currentUploads--;
|
|
startUpload(); // Try the next file
|
|
});
|
|
|
|
// Send the request
|
|
xhr.open('POST', '/files/upload_file');
|
|
xhr.send(formData);
|
|
|
|
// Try to start more uploads if possible
|
|
startUpload();
|
|
}
|
|
|
|
function updateUploadStats() {
|
|
if (currentUploads === 0) return;
|
|
|
|
// Calculate overall progress
|
|
let totalProgress = 0;
|
|
|
|
uploadQueue.forEach(item => {
|
|
if (item.status === 'complete') {
|
|
totalProgress += item.file.size;
|
|
} else if (item.status === 'uploading') {
|
|
totalProgress += (item.file.size * (item.progress / 100));
|
|
}
|
|
});
|
|
|
|
const overallPercentage = Math.round((totalProgress / totalSize) * 100);
|
|
|
|
// Update progress bar
|
|
progressBar.style.width = overallPercentage + '%';
|
|
progressPercentage.textContent = overallPercentage + '%';
|
|
|
|
// Calculate upload speed
|
|
const elapsed = (Date.now() - uploadStartTime) / 1000; // seconds
|
|
const bytesPerSecond = totalProgress / elapsed;
|
|
|
|
uploadSpeed.textContent = formatSize(bytesPerSecond) + '/s';
|
|
|
|
// Update uploaded size
|
|
uploadedSize.textContent = `${formatSize(totalProgress)} / ${formatSize(totalSize)}`;
|
|
|
|
// Calculate time remaining
|
|
const remainingBytes = totalSize - totalProgress;
|
|
if (bytesPerSecond > 0) {
|
|
const secondsRemaining = Math.round(remainingBytes / bytesPerSecond);
|
|
timeRemaining.textContent = formatTime(secondsRemaining);
|
|
} else {
|
|
timeRemaining.textContent = 'calculating...';
|
|
}
|
|
}
|
|
|
|
function resetUploadState() {
|
|
// Reset all upload tracking variables
|
|
uploadQueue = [];
|
|
currentUploads = 0;
|
|
totalUploaded = 0;
|
|
totalSize = 0;
|
|
clearInterval(uploadUpdateInterval);
|
|
|
|
// Reset UI
|
|
uploadList.innerHTML = '';
|
|
emptyMessage.style.display = 'block';
|
|
progressContainer.style.display = 'none';
|
|
fileCount.textContent = '0';
|
|
progressBar.style.width = '0%';
|
|
progressPercentage.textContent = '0%';
|
|
uploadSpeed.textContent = '0 KB/s';
|
|
uploadedSize.textContent = '0 KB / 0 KB';
|
|
timeRemaining.textContent = 'calculating...';
|
|
clearButton.disabled = true;
|
|
}
|
|
|
|
function showNotification(message, type) {
|
|
// Create alerts container if it doesn't exist
|
|
let alertsContainer = document.querySelector('.alerts');
|
|
if (!alertsContainer) {
|
|
alertsContainer = document.createElement('div');
|
|
alertsContainer.className = 'alerts';
|
|
document.body.appendChild(alertsContainer);
|
|
}
|
|
|
|
// Create alert
|
|
const alert = document.createElement('div');
|
|
alert.className = `alert ${type || 'info'}`;
|
|
alert.innerHTML = `
|
|
<div class="alert-content">${message}</div>
|
|
<button class="close" aria-label="Close">×</button>
|
|
`;
|
|
|
|
// Add to container
|
|
alertsContainer.appendChild(alert);
|
|
|
|
// Setup dismiss
|
|
const closeBtn = alert.querySelector('.close');
|
|
closeBtn.addEventListener('click', function () {
|
|
alert.classList.add('fade-out');
|
|
setTimeout(() => {
|
|
alert.remove();
|
|
}, 300);
|
|
});
|
|
|
|
// Auto dismiss
|
|
setTimeout(() => {
|
|
if (alert.parentNode) {
|
|
alert.classList.add('fade-out');
|
|
setTimeout(() => {
|
|
if (alert.parentNode) {
|
|
alert.remove();
|
|
}
|
|
}, 300);
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
if (seconds < 60) {
|
|
return seconds + ' seconds';
|
|
} else if (seconds < 3600) {
|
|
return Math.floor(seconds / 60) + ' minutes';
|
|
} else {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
return hours + ' hours ' + minutes + ' minutes';
|
|
}
|
|
}
|
|
|
|
function getFileIcon(fileName) {
|
|
const extension = fileName.split('.').pop().toLowerCase();
|
|
|
|
// Images
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(extension)) {
|
|
return 'fa-file-image';
|
|
}
|
|
// Videos
|
|
else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(extension)) {
|
|
return 'fa-file-video';
|
|
}
|
|
// Audio
|
|
else if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(extension)) {
|
|
return 'fa-file-audio';
|
|
}
|
|
// Documents
|
|
else if (['doc', 'docx', 'dot', 'dotx'].includes(extension)) {
|
|
return 'fa-file-word';
|
|
}
|
|
else if (['xls', 'xlsx', 'csv'].includes(extension)) {
|
|
return 'fa-file-excel';
|
|
}
|
|
else if (['ppt', 'pptx'].includes(extension)) {
|
|
return 'fa-file-powerpoint';
|
|
}
|
|
else if (['pdf'].includes(extension)) {
|
|
return 'fa-file-pdf';
|
|
}
|
|
// Archives
|
|
else if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) {
|
|
return 'fa-file-archive';
|
|
}
|
|
// Text
|
|
else if (['txt', 'rtf', 'md', 'log'].includes(extension)) {
|
|
return 'fa-file-alt';
|
|
}
|
|
// Code
|
|
else if (['html', 'css', 'js', 'php', 'py', 'java', 'c', 'cpp', 'h', 'xml', 'json', 'sql'].includes(extension)) {
|
|
return 'fa-file-code';
|
|
}
|
|
|
|
return 'fa-file';
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |