feat: add bulk file actions (#7827)
* fixed styling * Enabled header checkbox selection * Changed styling of selection * Implemented bulk downloading and deleting * Added delete bulk hook * Added download bulk hook * Fix backend to send extension in download single file * Fix hook to download single file directly * Added header and selection handling * Added delete confirmation * [autofix.ci] apply automated fixes * Fixed selection with shift * Show disabled files * Show disabled files as not clickable * Changed color of icon when disabled * Implemented pressed shift handling * Fixed shift selection and disabled text selection when holding shift * Created test for bulk selection on files modal * add test of disabled components in file component * Fixed files page test to include bulk editing test * removed ring on focus visible * Changed delete files having the right select --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
596a219de3
commit
a72995c408
11 changed files with 1244 additions and 235 deletions
|
|
@ -1,9 +1,13 @@
|
|||
import io
|
||||
import re
|
||||
import uuid
|
||||
import zipfile
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
|
@ -159,6 +163,92 @@ async def list_files(
|
|||
raise HTTPException(status_code=500, detail=f"Error listing files: {e}") from e
|
||||
|
||||
|
||||
@router.delete("/batch/", status_code=HTTPStatus.OK)
|
||||
async def delete_files_batch(
|
||||
file_ids: list[uuid.UUID],
|
||||
current_user: CurrentActiveUser,
|
||||
session: DbSession,
|
||||
storage_service: Annotated[StorageService, Depends(get_storage_service)],
|
||||
):
|
||||
"""Delete multiple files by their IDs."""
|
||||
try:
|
||||
# Fetch all files from the DB
|
||||
stmt = select(UserFile).where(UserFile.id in file_ids, UserFile.user_id == current_user.id)
|
||||
results = await session.exec(stmt)
|
||||
files = results.all()
|
||||
|
||||
if not files:
|
||||
raise HTTPException(status_code=404, detail="No files found")
|
||||
|
||||
# Delete all files from the storage service
|
||||
for file in files:
|
||||
await storage_service.delete_file(flow_id=str(current_user.id), file_name=file.path)
|
||||
await session.delete(file)
|
||||
|
||||
# Delete all files from the database
|
||||
await session.flush() # Ensures delete is staged
|
||||
await session.commit() # Commit deletion
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback() # Rollback on failure
|
||||
raise HTTPException(status_code=500, detail=f"Error deleting files: {e}") from e
|
||||
|
||||
return {"message": f"{len(files)} files deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/batch/", status_code=HTTPStatus.OK)
|
||||
async def download_files_batch(
|
||||
file_ids: list[uuid.UUID],
|
||||
current_user: CurrentActiveUser,
|
||||
session: DbSession,
|
||||
storage_service: Annotated[StorageService, Depends(get_storage_service)],
|
||||
):
|
||||
"""Download multiple files as a zip file by their IDs."""
|
||||
try:
|
||||
# Fetch all files from the DB
|
||||
stmt = select(UserFile).where(UserFile.id in file_ids, UserFile.user_id == current_user.id)
|
||||
results = await session.exec(stmt)
|
||||
files = results.all()
|
||||
|
||||
if not files:
|
||||
raise HTTPException(status_code=404, detail="No files found")
|
||||
|
||||
# Create a byte stream to hold the ZIP file
|
||||
zip_stream = io.BytesIO()
|
||||
|
||||
# Create a ZIP file
|
||||
with zipfile.ZipFile(zip_stream, "w") as zip_file:
|
||||
for file in files:
|
||||
# Get the file content from storage
|
||||
file_content = await storage_service.get_file(
|
||||
flow_id=str(current_user.id), file_name=file.path.split("/")[-1]
|
||||
)
|
||||
|
||||
# Get the file extension from the original filename
|
||||
file_extension = Path(file.path).suffix
|
||||
# Create the filename with extension
|
||||
filename_with_extension = f"{file.name}{file_extension}"
|
||||
|
||||
# Write the file to the ZIP with the proper extension
|
||||
zip_file.writestr(filename_with_extension, file_content)
|
||||
|
||||
# Seek to the beginning of the byte stream
|
||||
zip_stream.seek(0)
|
||||
|
||||
# Generate the filename with the current datetime
|
||||
current_time = datetime.now(tz=ZoneInfo("UTC")).astimezone().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{current_time}_langflow_files.zip"
|
||||
|
||||
return StreamingResponse(
|
||||
zip_stream,
|
||||
media_type="application/x-zip-compressed",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error downloading files: {e}") from e
|
||||
|
||||
|
||||
@router.get("/{file_id}")
|
||||
async def download_file(
|
||||
file_id: uuid.UUID,
|
||||
|
|
@ -177,6 +267,10 @@ async def download_file(
|
|||
# Get file stream
|
||||
file_stream = await storage_service.get_file(flow_id=str(current_user.id), file_name=file_name)
|
||||
|
||||
file_extension = Path(file.path).suffix
|
||||
# Create the filename with extension
|
||||
filename_with_extension = f"{file.name}{file_extension}"
|
||||
|
||||
# Ensure file_stream is an async iterator returning bytes
|
||||
byte_stream = byte_stream_generator(file_stream)
|
||||
except Exception as e:
|
||||
|
|
@ -186,7 +280,7 @@ async def download_file(
|
|||
return StreamingResponse(
|
||||
byte_stream,
|
||||
media_type="application/octet-stream",
|
||||
headers={"Content-Disposition": f'attachment; filename="{file.name}"'},
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename_with_extension}"'},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue