This commit is contained in:
retoor 2025-11-09 00:52:16 +01:00
parent 81f1cfd200
commit c6fb77c89d
6 changed files with 328 additions and 206 deletions

View File

@ -1,5 +1,6 @@
from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView
from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
from .views.upload import UploadView
from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team
@ -30,7 +31,7 @@ def setup_routes(app):
app.router.add_post("/users/{email}/delete", UserManagementView, name="delete_user_page")
app.router.add_view("/files", FileBrowserView, name="file_browser")
app.router.add_post("/files/new_folder", FileBrowserView, name="new_folder")
app.router.add_post("/files/upload", FileBrowserView, name="upload_file")
app.router.add_post("/files/upload", UploadView, name="upload_file")
app.router.add_get("/files/download/{file_path:.*}", FileBrowserView, name="download_file")
app.router.add_post("/files/share/{file_path:.*}", FileBrowserView, name="share_file")
app.router.add_post("/files/delete/{file_path:.*}", FileBrowserView, name="delete_item")

View File

@ -236,3 +236,107 @@
min-width: 120px;
}
}
/* Styles for the new upload functionality */
.upload-area {
border: 2px dashed var(--border-color);
border-radius: 8px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
background-color: var(--background-color);
}
.upload-button {
display: inline-block;
padding: 10px 20px;
cursor: pointer;
margin-top: 10px;
}
.selected-files-preview {
margin-top: 20px;
text-align: left;
max-height: 200px;
overflow-y: auto;
border-top: 1px solid var(--border-color);
padding-top: 10px;
}
.file-entry {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
}
.file-entry:last-child {
border-bottom: none;
}
.file-entry .file-name {
flex-grow: 1;
margin-right: 10px;
color: var(--text-color);
}
.file-entry .file-size {
color: var(--light-text-color);
font-size: 0.85rem;
}
.file-entry .thumbnail-preview {
width: 40px;
height: 40px;
margin-left: 10px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--background-color);
border: 1px solid var(--border-color);
}
.file-entry .thumbnail-preview img {
max-width: 100%;
max-height: 100%;
display: block;
object-fit: contain;
}
.progress-bar-container {
margin-top: 15px;
text-align: left;
border-top: 1px solid var(--border-color);
padding-top: 10px;
}
.progress-bar-container .file-name {
font-weight: bold;
margin-bottom: 5px;
color: var(--text-color);
}
.progress-bar-wrapper {
width: 100%;
background-color: var(--background-color);
border-radius: 5px;
overflow: hidden;
height: 10px;
margin-bottom: 5px;
}
.progress-bar {
height: 100%;
width: 0%;
background-color: var(--accent-color);
border-radius: 5px;
transition: width 0.3s ease-in-out;
}
.progress-text {
font-size: 0.85rem;
color: var(--light-text-color);
text-align: right;
}

View File

@ -1,5 +1,6 @@
import './components/slider.js';
import './components/navigation.js'; // Assuming navigation.js might be needed globally
import { showUploadModal } from './components/upload.js'; // Import showUploadModal
document.addEventListener('DOMContentLoaded', () => {
// Logic for custom-slider on order page
@ -82,4 +83,199 @@ document.addEventListener('DOMContentLoaded', () => {
// Initial display based on active button (default to monthly)
updatePricingDisplay('monthly');
}
// --- File Browser Specific Logic ---
// Helper functions for modals
function showNewFolderModal() {
document.getElementById('new-folder-modal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
window.closeModal = closeModal; // Make it globally accessible
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
// File actions
function downloadFile(path) {
window.location.href = `/files/download/${path}`;
}
async function shareFile(path, name) {
const modal = document.getElementById('share-modal');
const linkContainer = document.getElementById('share-link-container');
const loading = document.getElementById('share-loading');
document.getElementById('share-file-name').textContent = `Sharing: ${name}`;
linkContainer.style.display = 'none';
loading.style.display = 'block';
modal.style.display = 'block';
try {
const response = await fetch(`/files/share/${path}`, {
method: 'POST'
});
const data = await response.json();
if (data.share_link) {
document.getElementById('share-link-input').value = data.share_link;
linkContainer.style.display = 'block';
loading.style.display = 'none';
}
} catch (error) {
loading.textContent = 'Error generating share link';
}
}
function copyShareLink() {
const input = document.getElementById('share-link-input');
input.select();
document.execCommand('copy');
alert('Share link copied to clipboard');
}
function deleteFile(path, name) {
document.getElementById('delete-message').textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
document.getElementById('delete-form').action = `/files/delete/${path}`;
document.getElementById('delete-modal').style.display = 'block';
}
// Selection and action buttons
function updateActionButtons() {
const checked = document.querySelectorAll('.file-checkbox:checked');
const downloadBtn = document.getElementById('download-selected-btn');
const shareBtn = document.getElementById('share-selected-btn');
const deleteBtn = document.getElementById('delete-selected-btn');
const hasSelection = checked.length > 0;
const hasFiles = Array.from(checked).some(cb => cb.dataset.isDir === 'False');
if (downloadBtn) downloadBtn.disabled = !hasFiles;
if (shareBtn) shareBtn.disabled = !hasSelection;
if (deleteBtn) deleteBtn.disabled = !hasSelection;
}
function downloadSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1 && checked[0].dataset.isDir === 'False') {
downloadFile(checked[0].dataset.path);
}
}
function shareSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1) {
const path = checked[0].dataset.path;
const name = checked[0].closest('tr').querySelector('td:nth-child(2)').textContent.trim();
shareFile(path, name);
}
}
function deleteSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length > 0) {
const paths = Array.from(checked).map(cb => cb.dataset.path);
const names = Array.from(checked).map(cb =>
cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim()
);
if (checked.length === 1) {
deleteFile(paths[0], names[0]);
} else {
document.getElementById('delete-message').textContent =
`Are you sure you want to delete ${checked.length} items? This action cannot be undone.`;
document.getElementById('delete-modal').style.display = 'block';
}
}
}
// Event Listeners for File Browser
const newFolderBtn = document.getElementById('new-folder-btn');
if (newFolderBtn) {
console.log('Attaching event listener to new-folder-btn');
newFolderBtn.addEventListener('click', showNewFolderModal);
}
const uploadBtn = document.getElementById('upload-btn');
if (uploadBtn) {
console.log('Attaching event listener to upload-btn');
uploadBtn.addEventListener('click', showUploadModal);
}
const createFirstFolderBtn = document.getElementById('create-first-folder-btn');
if (createFirstFolderBtn) {
console.log('Attaching event listener to create-first-folder-btn');
createFirstFolderBtn.addEventListener('click', showNewFolderModal);
}
const uploadFirstFileBtn = document.getElementById('upload-first-file-btn');
if (uploadFirstFileBtn) {
console.log('Attaching event listener to upload-first-file-btn');
uploadFirstFileBtn.addEventListener('click', showUploadModal);
}
const downloadSelectedBtn = document.getElementById('download-selected-btn');
if (downloadSelectedBtn) {
downloadSelectedBtn.addEventListener('click', downloadSelected);
}
const shareSelectedBtn = document.getElementById('share-selected-btn');
if (shareSelectedBtn) {
shareSelectedBtn.addEventListener('click', shareSelected);
}
const deleteSelectedBtn = document.getElementById('delete-selected-btn');
if (deleteSelectedBtn) {
deleteSelectedBtn.addEventListener('click', deleteSelected);
}
const copyShareLinkBtn = document.getElementById('copy-share-link-btn');
if (copyShareLinkBtn) {
copyShareLinkBtn.addEventListener('click', copyShareLink);
}
document.getElementById('select-all')?.addEventListener('change', function(e) {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateActionButtons();
});
document.querySelectorAll('.file-checkbox').forEach(cb => {
cb.addEventListener('change', updateActionButtons);
});
// Event listeners for dynamically created download/share/delete buttons
document.addEventListener('click', (event) => {
if (event.target.classList.contains('download-file-btn')) {
downloadFile(event.target.dataset.path);
} else if (event.target.classList.contains('share-file-btn')) {
shareFile(event.target.dataset.path, event.target.dataset.name);
} else if (event.target.classList.contains('delete-file-btn')) {
deleteFile(event.target.dataset.path, event.target.dataset.name);
}
});
document.getElementById('search-bar')?.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#file-list-body tr');
rows.forEach(row => {
const name = row.querySelector('td:nth-child(2)')?.textContent.toLowerCase();
if (name && name.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Initial update of action buttons
updateActionButtons();
});

View File

@ -9,11 +9,11 @@
{% block page_title %}My Files{% endblock %}
{% block dashboard_actions %}
<button class="btn-primary" onclick="showNewFolderModal()">+ New</button>
<button class="btn-outline" onclick="showUploadModal()">Upload</button>
<button class="btn-outline" onclick="downloadSelected()" id="download-btn" disabled>Download</button>
<button class="btn-outline" onclick="shareSelected()" id="share-btn" disabled>Share</button>
<button class="btn-outline" onclick="deleteSelected()" id="delete-btn" disabled>Delete</button>
<button class="btn-primary" id="new-folder-btn">+ New</button>
<button class="btn-outline" id="upload-btn">Upload</button>
<button class="btn-outline" id="download-selected-btn" disabled>Download</button>
<button class="btn-outline" id="share-selected-btn" disabled>Share</button>
<button class="btn-outline" id="delete-selected-btn" disabled>Delete</button>
{% endblock %}
{% block dashboard_content %}
@ -70,10 +70,10 @@
<td>
<div class="action-buttons">
{% if not item.is_dir %}
<button class="btn-small" onclick="downloadFile('{{ item.path }}')">Download</button>
<button class="btn-small download-file-btn" data-path="{{ item.path }}">Download</button>
{% endif %}
<button class="btn-small" onclick="shareFile('{{ item.path }}', '{{ item.name }}')">Share</button>
<button class="btn-small btn-danger" onclick="deleteFile('{{ item.path }}', '{{ item.name }}')">Delete</button>
<button class="btn-small share-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Share</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Delete</button>
</div>
</td>
</tr>
@ -82,8 +82,8 @@
<tr>
<td colspan="6" style="text-align: center; padding: 40px;">
<p>No files found in this directory.</p>
<button class="btn-primary" onclick="showNewFolderModal()" style="margin-top: 10px;">Create your first folder</button>
<button class="btn-outline" onclick="showUploadModal()" style="margin-top: 10px;">Upload a file</button>
<button class="btn-primary" id="create-first-folder-btn" style="margin-top: 10px;">Create your first folder</button>
<button class="btn-outline" id="upload-first-file-btn" style="margin-top: 10px;">Upload a file</button>
</td>
</tr>
{% endif %}
@ -108,15 +108,17 @@
<div id="upload-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('upload-modal')">&times;</span>
<h3>Upload File</h3>
<form action="/files/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" required class="form-input" id="file-input">
<div class="file-info" id="file-info"></div>
<div class="modal-actions">
<button type="submit" class="btn-primary">Upload</button>
<button type="button" class="btn-outline" onclick="closeModal('upload-modal')">Cancel</button>
</div>
</form>
<h3>Upload Files</h3>
<div class="upload-area">
<input type="file" name="file" multiple class="form-input" id="file-input-multiple" style="display: none;">
<label for="file-input-multiple" class="btn-outline upload-button">Select Files</label>
<div id="selected-files-preview" class="selected-files-preview"></div>
<div id="upload-progress-container" class="upload-progress-container"></div>
</div>
<div class="modal-actions">
<button type="button" class="btn-primary" id="start-upload-btn" disabled>Upload</button>
<button type="button" class="btn-outline" onclick="closeModal('upload-modal')">Cancel</button>
</div>
</div>
</div>
@ -127,7 +129,7 @@
<p id="share-file-name"></p>
<div id="share-link-container" style="display: none;">
<input type="text" id="share-link-input" readonly class="form-input">
<button class="btn-primary" onclick="copyShareLink()">Copy Link</button>
<button class="btn-primary" id="copy-share-link-btn">Copy Link</button>
</div>
<div id="share-loading">Generating share link...</div>
<div class="modal-actions">
@ -150,146 +152,6 @@
</div>
</div>
<script>
function showNewFolderModal() {
document.getElementById('new-folder-modal').style.display = 'block';
}
function showUploadModal() {
document.getElementById('upload-modal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
document.getElementById('file-input').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const size = (file.size / 1024 / 1024).toFixed(2);
document.getElementById('file-info').innerHTML = `Selected: ${file.name} (${size} MB)`;
}
});
function downloadFile(path) {
window.location.href = `/files/download/${path}`;
}
async function shareFile(path, name) {
const modal = document.getElementById('share-modal');
const linkContainer = document.getElementById('share-link-container');
const loading = document.getElementById('share-loading');
document.getElementById('share-file-name').textContent = `Sharing: ${name}`;
linkContainer.style.display = 'none';
loading.style.display = 'block';
modal.style.display = 'block';
try {
const response = await fetch(`/files/share/${path}`, {
method: 'POST'
});
const data = await response.json();
if (data.share_link) {
document.getElementById('share-link-input').value = data.share_link;
linkContainer.style.display = 'block';
loading.style.display = 'none';
}
} catch (error) {
loading.textContent = 'Error generating share link';
}
}
function copyShareLink() {
const input = document.getElementById('share-link-input');
input.select();
document.execCommand('copy');
alert('Share link copied to clipboard');
}
function deleteFile(path, name) {
document.getElementById('delete-message').textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
document.getElementById('delete-form').action = `/files/delete/${path}`;
document.getElementById('delete-modal').style.display = 'block';
}
document.getElementById('select-all').addEventListener('change', function(e) {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateActionButtons();
});
document.querySelectorAll('.file-checkbox').forEach(cb => {
cb.addEventListener('change', updateActionButtons);
});
function updateActionButtons() {
const checked = document.querySelectorAll('.file-checkbox:checked');
const downloadBtn = document.getElementById('download-btn');
const shareBtn = document.getElementById('share-btn');
const deleteBtn = document.getElementById('delete-btn');
const hasSelection = checked.length > 0;
const hasFiles = Array.from(checked).some(cb => cb.dataset.isDir === 'False');
downloadBtn.disabled = !hasFiles;
shareBtn.disabled = !hasSelection;
deleteBtn.disabled = !hasSelection;
}
function downloadSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1 && checked[0].dataset.isDir === 'False') {
downloadFile(checked[0].dataset.path);
}
}
function shareSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1) {
const path = checked[0].dataset.path;
const name = checked[0].closest('tr').querySelector('td:nth-child(2)').textContent.trim();
shareFile(path, name);
}
}
function deleteSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length > 0) {
const paths = Array.from(checked).map(cb => cb.dataset.path);
const names = Array.from(checked).map(cb =>
cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim()
);
if (checked.length === 1) {
deleteFile(paths[0], names[0]);
} else {
document.getElementById('delete-message').textContent =
`Are you sure you want to delete ${checked.length} items? This action cannot be undone.`;
document.getElementById('delete-modal').style.display = 'block';
}
}
}
document.getElementById('search-bar').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#file-list-body tr');
rows.forEach(row => {
const name = row.querySelector('td:nth-child(2)')?.textContent.toLowerCase();
if (name && name.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
</script>
<script type="module" src="/static/js/components/upload.js"></script>
<script type="module" src="/static/js/main.js"></script>
{% endblock %}

View File

@ -182,46 +182,6 @@ class FileBrowserView(web.View):
)
)
elif route_name == "upload_file":
try:
reader = await self.request.multipart()
field = await reader.next()
if not field or field.name != "file":
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="No file selected for upload"
)
)
filename = field.filename
if not filename:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="Filename is required"
)
)
content = await field.read()
success = await file_service.upload_file(user_email, filename, content)
if success:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
success=f"File '{filename}' uploaded successfully"
)
)
else:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error=f"Failed to upload file '{filename}'"
)
)
except Exception as e:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error=f"Upload error: {str(e)}"
)
)
elif route_name == "share_file":
file_path = self.request.match_info.get("file_path")
if not file_path:

View File

@ -369,8 +369,7 @@ async def test_file_browser_upload_file(logged_in_client: TestClient, file_servi
content_type='text/plain')
resp = await logged_in_client.post("/files/upload", data=data, allow_redirects=False)
assert resp.status == 302 # Redirect
assert resp.headers["Location"].startswith("/files")
assert resp.status == 200
expected_path = temp_user_files_dir / user_email / "uploaded.txt"
assert expected_path.is_file()