fix: enhance folder download endpoint with zip file support (#4706)
* expanding download folder to zip file * run formatter * Update src/backend/base/langflow/api/v1/folders.py Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> * changing model to flowRead * run formatter --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
25a0d7674a
commit
9e8715cf4c
4 changed files with 80 additions and 30 deletions
|
|
@ -1,16 +1,22 @@
|
|||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi_pagination import Params
|
||||
from fastapi_pagination.ext.sqlmodel import paginate
|
||||
from sqlalchemy import or_, update
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlmodel import select
|
||||
|
||||
from langflow.api.utils import AsyncDbSession, CurrentActiveUser, cascade_delete_flow, custom_params
|
||||
from langflow.api.utils import AsyncDbSession, CurrentActiveUser, cascade_delete_flow, custom_params, remove_api_keys
|
||||
from langflow.api.v1.flows import create_flows
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName
|
||||
from langflow.api.v1.schemas import FlowListCreate
|
||||
from langflow.helpers.flow import generate_unique_flow_name
|
||||
from langflow.helpers.folders import generate_unique_folder_name
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
|
|
@ -248,28 +254,53 @@ async def delete_folder(
|
|||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/download/{folder_id}", response_model=FlowListReadWithFolderName, status_code=200)
|
||||
@router.get("/download/{folder_id}", status_code=200)
|
||||
async def download_file(
|
||||
*,
|
||||
session: AsyncDbSession,
|
||||
folder_id: str,
|
||||
current_user: CurrentActiveUser,
|
||||
):
|
||||
"""Download all flows from folder."""
|
||||
"""Download all flows from folder as a zip file."""
|
||||
try:
|
||||
folder = (
|
||||
await session.exec(select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id))
|
||||
).first()
|
||||
query = select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)
|
||||
result = await session.exec(query)
|
||||
folder = result.first()
|
||||
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
|
||||
flows_query = select(Flow).where(Flow.folder_id == folder_id)
|
||||
flows_result = await session.exec(flows_query)
|
||||
flows = [FlowRead.model_validate(flow, from_attributes=True) for flow in flows_result.all()]
|
||||
|
||||
if not flows:
|
||||
raise HTTPException(status_code=404, detail="No flows found in folder")
|
||||
|
||||
flows_without_api_keys = [remove_api_keys(flow.model_dump()) for flow in flows]
|
||||
zip_stream = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_stream, "w") as zip_file:
|
||||
for flow in flows_without_api_keys:
|
||||
flow_json = json.dumps(jsonable_encoder(flow))
|
||||
zip_file.writestr(f"{flow['name']}.json", flow_json)
|
||||
|
||||
zip_stream.seek(0)
|
||||
|
||||
current_time = datetime.now(tz=timezone.utc).astimezone().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{current_time}_{folder.name}_flows.zip"
|
||||
|
||||
return StreamingResponse(
|
||||
zip_stream,
|
||||
media_type="application/x-zip-compressed",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
if "No result found" in str(e):
|
||||
raise HTTPException(status_code=404, detail="Folder not found") from e
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
|
||||
return folder
|
||||
|
||||
|
||||
@router.post("/upload/", response_model=list[FlowRead], status_code=201)
|
||||
async def upload_file(
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ class FlowListRead(BaseModel):
|
|||
|
||||
class FlowListReadWithFolderName(BaseModel):
|
||||
flows: list[FlowRead]
|
||||
name: str
|
||||
folder_name: str
|
||||
description: str
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ const SideBarFoldersButtonsComponent = ({
|
|||
});
|
||||
};
|
||||
|
||||
const { mutate: mutateDownloadFolder } = useGetDownloadFolders();
|
||||
const { mutate: mutateDownloadFolder } = useGetDownloadFolders({});
|
||||
|
||||
const handleDownloadFolder = (id: string) => {
|
||||
mutateDownloadFolder(
|
||||
|
|
@ -133,20 +133,29 @@ const SideBarFoldersButtonsComponent = ({
|
|||
folderId: id,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
data.folder_name = data?.name || "folder";
|
||||
data.folder_description = data?.description || "";
|
||||
|
||||
const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(data),
|
||||
)}`;
|
||||
onSuccess: (response) => {
|
||||
// Create a blob from the response data
|
||||
const blob = new Blob([response.data], {
|
||||
type: "application/x-zip-compressed",
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = jsonString;
|
||||
link.download = `${data?.name}.json`;
|
||||
link.href = url;
|
||||
|
||||
// Get filename from header or use default
|
||||
const filename =
|
||||
response.headers?.["content-disposition"]
|
||||
?.split("filename=")[1]
|
||||
?.replace(/['"]/g, "") ?? "flows.zip";
|
||||
|
||||
link.setAttribute("download", filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
track("Folder Exported", { folderId: id! });
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
track("Folder Exported", { folderId: id });
|
||||
},
|
||||
onError: () => {
|
||||
setErrorData({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMutationFunctionType } from "@/types/api";
|
||||
import { UseMutationResult } from "@tanstack/react-query";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
|
@ -8,22 +9,31 @@ interface IGetDownloadFolders {
|
|||
}
|
||||
|
||||
export const useGetDownloadFolders: useMutationFunctionType<
|
||||
undefined,
|
||||
any, // Changed to any since we're getting the full response
|
||||
IGetDownloadFolders
|
||||
> = (options?) => {
|
||||
const { mutate } = UseRequestProcessor();
|
||||
|
||||
const downloadFoldersFn = async (
|
||||
data: IGetDownloadFolders,
|
||||
): Promise<void> => {
|
||||
const res = await api.get(`${getURL("FOLDERS")}/download/${data.folderId}`);
|
||||
return res.data;
|
||||
payload: IGetDownloadFolders,
|
||||
): Promise<any> => {
|
||||
const response = await api.get<any>(
|
||||
`${getURL("FOLDERS")}/download/${payload.folderId}`,
|
||||
{
|
||||
responseType: "blob",
|
||||
headers: {
|
||||
Accept: "application/x-zip-compressed",
|
||||
},
|
||||
},
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
const mutation = mutate(
|
||||
const mutation: UseMutationResult<any, any, IGetDownloadFolders> = mutate(
|
||||
["useGetDownloadFolders"],
|
||||
downloadFoldersFn,
|
||||
options,
|
||||
);
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue