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.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView
from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView 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 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_post("/users/{email}/delete", UserManagementView, name="delete_user_page")
app.router.add_view("/files", FileBrowserView, name="file_browser") 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/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_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/share/{file_path:.*}", FileBrowserView, name="share_file")
app.router.add_post("/files/delete/{file_path:.*}", FileBrowserView, name="delete_item") app.router.add_post("/files/delete/{file_path:.*}", FileBrowserView, name="delete_item")

View File

@ -236,3 +236,107 @@
min-width: 120px; 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/slider.js';
import './components/navigation.js'; // Assuming navigation.js might be needed globally import './components/navigation.js'; // Assuming navigation.js might be needed globally
import { showUploadModal } from './components/upload.js'; // Import showUploadModal
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Logic for custom-slider on order page // Logic for custom-slider on order page
@ -82,4 +83,199 @@ document.addEventListener('DOMContentLoaded', () => {
// Initial display based on active button (default to monthly) // Initial display based on active button (default to monthly)
updatePricingDisplay('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 page_title %}My Files{% endblock %}
{% block dashboard_actions %} {% block dashboard_actions %}
<button class="btn-primary" onclick="showNewFolderModal()">+ New</button> <button class="btn-primary" id="new-folder-btn">+ New</button>
<button class="btn-outline" onclick="showUploadModal()">Upload</button> <button class="btn-outline" id="upload-btn">Upload</button>
<button class="btn-outline" onclick="downloadSelected()" id="download-btn" disabled>Download</button> <button class="btn-outline" id="download-selected-btn" disabled>Download</button>
<button class="btn-outline" onclick="shareSelected()" id="share-btn" disabled>Share</button> <button class="btn-outline" id="share-selected-btn" disabled>Share</button>
<button class="btn-outline" onclick="deleteSelected()" id="delete-btn" disabled>Delete</button> <button class="btn-outline" id="delete-selected-btn" disabled>Delete</button>
{% endblock %} {% endblock %}
{% block dashboard_content %} {% block dashboard_content %}
@ -70,10 +70,10 @@
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
{% if not item.is_dir %} {% 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 %} {% endif %}
<button class="btn-small" onclick="shareFile('{{ item.path }}', '{{ item.name }}')">Share</button> <button class="btn-small share-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Share</button>
<button class="btn-small btn-danger" onclick="deleteFile('{{ item.path }}', '{{ item.name }}')">Delete</button> <button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Delete</button>
</div> </div>
</td> </td>
</tr> </tr>
@ -82,8 +82,8 @@
<tr> <tr>
<td colspan="6" style="text-align: center; padding: 40px;"> <td colspan="6" style="text-align: center; padding: 40px;">
<p>No files found in this directory.</p> <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-primary" id="create-first-folder-btn" 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-outline" id="upload-first-file-btn" style="margin-top: 10px;">Upload a file</button>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -108,15 +108,17 @@
<div id="upload-modal" class="modal"> <div id="upload-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeModal('upload-modal')">&times;</span> <span class="close" onclick="closeModal('upload-modal')">&times;</span>
<h3>Upload File</h3> <h3>Upload Files</h3>
<form action="/files/upload" method="post" enctype="multipart/form-data"> <div class="upload-area">
<input type="file" name="file" required class="form-input" id="file-input"> <input type="file" name="file" multiple class="form-input" id="file-input-multiple" style="display: none;">
<div class="file-info" id="file-info"></div> <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"> <div class="modal-actions">
<button type="submit" class="btn-primary">Upload</button> <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> <button type="button" class="btn-outline" onclick="closeModal('upload-modal')">Cancel</button>
</div> </div>
</form>
</div> </div>
</div> </div>
@ -127,7 +129,7 @@
<p id="share-file-name"></p> <p id="share-file-name"></p>
<div id="share-link-container" style="display: none;"> <div id="share-link-container" style="display: none;">
<input type="text" id="share-link-input" readonly class="form-input"> <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>
<div id="share-loading">Generating share link...</div> <div id="share-loading">Generating share link...</div>
<div class="modal-actions"> <div class="modal-actions">
@ -150,146 +152,6 @@
</div> </div>
</div> </div>
<script> <script type="module" src="/static/js/components/upload.js"></script>
function showNewFolderModal() { <script type="module" src="/static/js/main.js"></script>
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>
{% endblock %} {% 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": elif route_name == "share_file":
file_path = self.request.match_info.get("file_path") file_path = self.request.match_info.get("file_path")
if not 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') content_type='text/plain')
resp = await logged_in_client.post("/files/upload", data=data, allow_redirects=False) resp = await logged_in_client.post("/files/upload", data=data, allow_redirects=False)
assert resp.status == 302 # Redirect assert resp.status == 200
assert resp.headers["Location"].startswith("/files")
expected_path = temp_user_files_dir / user_email / "uploaded.txt" expected_path = temp_user_files_dir / user_email / "uploaded.txt"
assert expected_path.is_file() assert expected_path.is_file()