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:
Cristhian Zanforlin Lousa 2024-11-19 15:41:37 -03:00 committed by GitHub
commit 9e8715cf4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 80 additions and 30 deletions

View file

@ -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(

View file

@ -162,7 +162,7 @@ class FlowListRead(BaseModel):
class FlowListReadWithFolderName(BaseModel):
flows: list[FlowRead]
name: str
folder_name: str
description: str

View file

@ -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({

View file

@ -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;
};