This commit is contained in:
retoor 2025-11-09 01:54:31 +01:00
parent ea8af383cc
commit 5d3d0b162d
4 changed files with 158 additions and 29 deletions

View File

@ -35,6 +35,8 @@ def setup_routes(app):
app.router.add_get("/files/download/{file_path:.*}", FileBrowserView.get_download_file, name="download_file") app.router.add_get("/files/download/{file_path:.*}", FileBrowserView.get_download_file, 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")
app.router.add_post("/files/delete_multiple", FileBrowserView, name="delete_multiple_items")
app.router.add_post("/files/share_multiple", FileBrowserView, name="share_multiple_items")
app.router.add_get("/shared_file/{share_id}", FileBrowserView.shared_file_handler, name="shared_file") app.router.add_get("/shared_file/{share_id}", FileBrowserView.shared_file_handler, name="shared_file")
app.router.add_get("/shared_file/{share_id}/download", FileBrowserView.download_shared_file_handler, name="download_shared_file") app.router.add_get("/shared_file/{share_id}/download", FileBrowserView.download_shared_file_handler, name="download_shared_file")

View File

@ -107,29 +107,64 @@ document.addEventListener('DOMContentLoaded', () => {
window.location.href = `/files/download/${path}`; window.location.href = `/files/download/${path}`;
} }
async function shareFile(path, name) { async function shareFile(paths, names) {
const modal = document.getElementById('share-modal'); const modal = document.getElementById('share-modal');
const linkContainer = document.getElementById('share-link-container'); const linkContainer = document.getElementById('share-link-container');
const loading = document.getElementById('share-loading'); const loading = document.getElementById('share-loading');
const shareLinkInput = document.getElementById('share-link-input');
const shareFileName = document.getElementById('share-file-name');
const shareLinksList = document.getElementById('share-links-list'); // New element for multiple links
document.getElementById('share-file-name').textContent = `Sharing: ${name}`; // Clear previous content
shareLinkInput.value = '';
if (shareLinksList) shareLinksList.innerHTML = '';
linkContainer.style.display = 'none'; linkContainer.style.display = 'none';
loading.style.display = 'block'; loading.style.display = 'block';
modal.style.display = 'block'; modal.style.display = 'block';
if (paths.length === 1) {
shareFileName.textContent = `Sharing: ${names[0]}`;
} else {
shareFileName.textContent = `Sharing ${paths.length} items`;
}
try { try {
const response = await fetch(`/files/share/${path}`, { const response = await fetch(`/files/share_multiple`, {
method: 'POST' method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ paths: paths })
}); });
const data = await response.json(); const data = await response.json();
if (data.share_link) { if (data.share_links && data.share_links.length > 0) {
document.getElementById('share-link-input').value = data.share_link; if (data.share_links.length === 1) {
shareLinkInput.value = data.share_links[0];
linkContainer.style.display = 'block'; linkContainer.style.display = 'block';
} else {
// Display multiple links
if (!shareLinksList) {
// Create the list if it doesn't exist
const ul = document.createElement('ul');
ul.id = 'share-links-list';
linkContainer.appendChild(ul);
shareLinksList = ul;
}
data.share_links.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `<strong>${item.name}:</strong> <input type="text" value="${item.link}" readonly class="form-input share-link-item-input"> <button class="btn-primary copy-share-link-item-btn" data-link="${item.link}">Copy</button>`;
shareLinksList.appendChild(li);
});
linkContainer.style.display = 'block';
}
loading.style.display = 'none'; loading.style.display = 'none';
} else {
loading.textContent = 'Error generating share link(s)';
} }
} catch (error) { } catch (error) {
loading.textContent = 'Error generating share link'; console.error('Error sharing files:', error);
loading.textContent = 'Error generating share link(s)';
} }
} }
@ -140,10 +175,31 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Share link copied to clipboard'); alert('Share link copied to clipboard');
} }
function deleteFile(path, name) { function deleteFile(paths, names) {
document.getElementById('delete-message').textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`; const deleteForm = document.getElementById('delete-form');
document.getElementById('delete-form').action = `/files/delete/${path}`; const deleteMessage = document.getElementById('delete-message');
document.getElementById('delete-modal').style.display = 'block'; const deleteModal = document.getElementById('delete-modal');
// Clear previous hidden inputs
deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove());
if (Array.isArray(paths) && paths.length > 1) {
deleteMessage.textContent = `Are you sure you want to delete ${paths.length} items? This action cannot be undone.`;
deleteForm.action = `/files/delete_multiple`;
paths.forEach(path => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'paths[]';
input.value = path;
deleteForm.appendChild(input);
});
} else {
const path = Array.isArray(paths) ? paths[0] : paths;
const name = Array.isArray(names) ? names[0] : names;
deleteMessage.textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
deleteForm.action = `/files/delete/${path}`;
}
deleteModal.style.display = 'block';
} }
// Selection and action buttons // Selection and action buttons
@ -170,10 +226,12 @@ document.addEventListener('DOMContentLoaded', () => {
function shareSelected() { function shareSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked'); const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1) { if (checked.length > 0) {
const path = checked[0].dataset.path; const paths = Array.from(checked).map(cb => cb.dataset.path);
const name = checked[0].closest('tr').querySelector('td:nth-child(2)').textContent.trim(); const names = Array.from(checked).map(cb =>
shareFile(path, name); cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim()
);
shareFile(paths, names);
} }
} }
@ -184,14 +242,7 @@ document.addEventListener('DOMContentLoaded', () => {
const names = Array.from(checked).map(cb => const names = Array.from(checked).map(cb =>
cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim() cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim()
); );
deleteFile(paths, names);
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';
}
} }
} }
@ -255,9 +306,21 @@ document.addEventListener('DOMContentLoaded', () => {
if (event.target.classList.contains('download-file-btn')) { if (event.target.classList.contains('download-file-btn')) {
downloadFile(event.target.dataset.path); downloadFile(event.target.dataset.path);
} else if (event.target.classList.contains('share-file-btn')) { } else if (event.target.classList.contains('share-file-btn')) {
shareFile(event.target.dataset.path, event.target.dataset.name); const path = event.target.dataset.path;
const name = event.target.dataset.name;
shareFile([path], [name]);
} else if (event.target.classList.contains('delete-file-btn')) { } else if (event.target.classList.contains('delete-file-btn')) {
deleteFile(event.target.dataset.path, event.target.dataset.name); const path = event.target.dataset.path;
const name = event.target.dataset.name;
deleteFile([path], [name]);
} else if (event.target.classList.contains('copy-share-link-item-btn')) {
const linkToCopy = event.target.dataset.link;
navigator.clipboard.writeText(linkToCopy).then(() => {
alert('Share link copied to clipboard');
}).catch(err => {
console.error('Failed to copy link: ', err);
alert('Failed to copy link');
});
} }
}); });

View File

@ -130,6 +130,7 @@
<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" id="copy-share-link-btn">Copy Link</button> <button class="btn-primary" id="copy-share-link-btn">Copy Link</button>
<div id="share-links-list" class="share-links-list"></div>
</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">

View File

@ -235,14 +235,77 @@ class FileBrowserView(web.View):
) )
) )
elif route_name == "delete_multiple_items":
data = await self.request.post()
paths = data.getall("paths[]")
logger.debug(f"FileBrowserView: Delete multiple items request for paths: {paths} by user {user_email}")
if not paths:
logger.warning("FileBrowserView: Delete multiple items request missing paths")
raise web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="No items selected for deletion"
)
)
all_successful = True
for path in paths:
success = await file_service.delete_item(user_email, path)
if not success:
all_successful = False
logger.error(f"FileBrowserView: Failed to delete item '{path}' for user {user_email} during bulk delete")
if all_successful:
logger.info(f"FileBrowserView: All {len(paths)} items deleted successfully for user {user_email}")
raise web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
success=f"Successfully deleted {len(paths)} items"
)
)
else:
logger.error(f"FileBrowserView: Some items failed to delete for user {user_email} during bulk delete")
raise web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="Some items failed to delete"
)
)
elif route_name == "share_multiple_items":
data = await self.request.json()
paths = data.get("paths", [])
logger.debug(f"FileBrowserView: Share multiple items request for paths: {paths} by user {user_email}")
if not paths:
logger.warning("FileBrowserView: Share multiple items request missing paths")
return json_response({"error": "No items selected for sharing"}, status=400)
share_links = []
for path in paths:
share_id = await file_service.generate_share_link(user_email, path)
if share_id:
share_link = f"{self.request.scheme}://{self.request.host}/shared_file/{share_id}"
# Extract file name from path
file_name = os.path.basename(path)
share_links.append({"name": file_name, "link": share_link})
else:
logger.error(f"FileBrowserView: Failed to generate share link for file: {path} by user {user_email}")
if share_links:
logger.info(f"FileBrowserView: Generated {len(share_links)} share links for user {user_email}")
return json_response({"share_links": share_links})
else:
logger.error(f"FileBrowserView: Failed to generate any share links for user {user_email}")
return json_response({"error": "Failed to generate share links for any selected items"}, status=500)
logger.warning(f"FileBrowserView: Unknown file action for POST request: {route_name}") logger.warning(f"FileBrowserView: Unknown file action for POST request: {route_name}")
raise web.HTTPBadRequest(text="Unknown file action") raise web.HTTPBadRequest(text="Unknown file action")
@login_required @login_required
async def get_download_file(self): async def get_download_file(self):
user_email = self.request["user"]["email"] request = self # self is the request object here
file_service = self.request.app["file_service"] user_email = request["user"]["email"]
file_path = self.request.match_info.get("file_path") file_service = request.app["file_service"]
file_path = request.match_info.get("file_path")
logger.debug(f"FileBrowserView: Download file request for path: {file_path} by user {user_email}") logger.debug(f"FileBrowserView: Download file request for path: {file_path} by user {user_email}")
if not file_path: if not file_path:
logger.warning("FileBrowserView: Download file request missing file_path") logger.warning("FileBrowserView: Download file request missing file_path")