Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions backend/app/routers/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import io
import os
from typing import List, Optional

import zipfile
from bson import ObjectId
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Response
from fastapi import Form
from minio import Minio
from pymongo import MongoClient
Expand Down Expand Up @@ -41,6 +41,28 @@
clowder_bucket = os.getenv("MINIO_BUCKET_NAME", "clowder")


async def get_folder_hierarchy(
folder_id: str,
hierarchy: str,
db: MongoClient,
):
found = await db["folders"].find_one({"_id": ObjectId(folder_id)})
folder = FolderOut.from_mongo(found)
folder_name = folder.name
hierarchy = folder_name + "/" + hierarchy
folder_parent = folder.parent_folder
if folder_parent is not None:
parent_folder_found = await db["folders"].find_one(
{"_id": ObjectId(folder_parent)}
)
parent_folder = FolderOut.from_mongo(parent_folder_found)
hierarchy = parent_folder.name + "/" + hierarchy
parent_folder_parent = parent_folder.parent_folder
if parent_folder_parent is not None:
hierarchy = await get_folder_hierarchy(str(parent_folder.id), hierarchy, db)
return hierarchy


@router.post("", response_model=DatasetOut)
async def save_dataset(
dataset_in: DatasetIn,
Expand Down Expand Up @@ -174,7 +196,7 @@ async def delete_dataset(
await db["datasets"].delete_one({"_id": ObjectId(dataset_id)})
async for file in db["files"].find({"dataset_id": ObjectId(dataset_id)}):
fs.remove_object(clowder_bucket, str(file))
db["file_versions"].delete_many({"file_id": file['_id']})
db["file_versions"].delete_many({"file_id": file["_id"]})
files_deleted = await db.files.delete_many({"dataset_id": ObjectId(dataset_id)})
folders_delete = await db["folders"].delete_many(
{"dataset_id": ObjectId(dataset_id)}
Expand Down Expand Up @@ -263,17 +285,13 @@ async def save_file(
status_code=401, detail=f"User not found. Session might have expired."
)

dataset = await db["datasets"].find_one({"_id": ObjectId(dataset_id)})
if dataset is None:
raise HTTPException(
status_code=404, detail=f"Dataset {dataset_id} not found"
)
fileDB = FileDB(name=file.filename, creator=user, dataset_id=dataset["_id"])

if folder_id is not None:
if (
folder := await db["folders"].find_one({"_id": ObjectId(folder_id)})
) is not None:
folder = FolderOut.from_mongo(folder)
fileDB.folder_id = folder.id
else:
raise HTTPException(
Expand Down Expand Up @@ -314,3 +332,38 @@ async def save_file(
return fileDB
else:
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")


@router.get("/{dataset_id}/download", response_model=DatasetOut)
async def download_dataset(
dataset_id: str,
user=Depends(get_current_user),
db: MongoClient = Depends(dependencies.get_db),
fs: Minio = Depends(dependencies.get_fs),
):
if (
dataset := await db["datasets"].find_one({"_id": ObjectId(dataset_id)})
) is not None:
dataset = DatasetOut.from_mongo(dataset)
stream = io.BytesIO()
z = zipfile.ZipFile(stream, "w")
async for f in db["files"].find({"dataset_id": dataset.id}):
file = FileOut.from_mongo(f)
file_name = file.name
if file.folder_id is not None:
hierarchy = await get_folder_hierarchy(file.folder_id, "", db)
file_name = "/" + hierarchy + file_name
content = fs.get_object(settings.MINIO_BUCKET_NAME, str(file.id))
z.writestr(file_name, content.data)
content.close()
content.release_conn()
z.close()
return Response(
stream.getvalue(),
media_type="application/x-zip-compressed",
headers={
"Content-Disposition": f'attachment;filename={dataset.name + ".zip"}'
},
)
else:
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
4 changes: 3 additions & 1 deletion backend/app/routers/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ async def delete_file(
# TODO: Deleting individual versions may require updating version_id in mongo, or deleting entire document
fs.remove_object(settings.MINIO_BUCKET_NAME, str(file_id))
removed_file = await db["files"].delete_one({"_id": ObjectId(file_id)})
removed_vers = await db["file_versions"].delete_many({"file_id": ObjectId(file_id)})
removed_vers = await db["file_versions"].delete_many(
{"file_id": ObjectId(file_id)}
)
return {"deleted": file_id}
else:
raise HTTPException(status_code=404, detail=f"File {file_id} not found")
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/actions/dataset.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function datasetDownloaded(datasetId, filename = "") {
} else {
filename = `${datasetId}.zip`;
}
const endpoint = `${config.hostname}/datasets/${datasetId}/download?superAdmin=true`;
const endpoint = `${config.hostname}/api/v2/datasets/${datasetId}/download`;
const response = await fetch(endpoint, {method: "GET", mode: "cors", headers: await getHeader()});

if (response.status === 200) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/datasets/Dataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ export const Dataset = (): JSX.Element => {
<MenuItem sx={optionMenuItem}
onClick={() => {
handleOptionClose();
}} disabled={true}>
Download All
}}>
Download Dataset
</MenuItem>
<MenuItem sx={optionMenuItem}
onClick={() => {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/datasets/DatasetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import {useNavigate} from "react-router-dom";
import {parseDate} from "../../utils/common";
import {datasetDownloaded} from "../../actions/dataset";
import {useDispatch} from "react-redux";

type DatasetCardProps = {
id: string,
Expand All @@ -17,6 +19,10 @@ type DatasetCardProps = {

export default function DatasetCard(props: DatasetCardProps) {
const { id, name, author, created, description} = props;

const dispatch = useDispatch();
const downloadDataset = (datasetId:string|undefined, filename:string|undefined) => dispatch(datasetDownloaded(datasetId, filename))

const formattedCreated = parseDate(created);
// use history hook to redirect/navigate between routes
const history = useNavigate();
Expand All @@ -42,7 +48,7 @@ export default function DatasetCard(props: DatasetCardProps) {
</CardContent>
<CardActions>
<Button size="small" onClick={() => selectDataset(id)}>View</Button>
<Button size="small" disabled={true}>Share</Button>
<Button size="small" onClick={() => downloadDataset(id, name)}>Download</Button>
</CardActions>
</Card>
);
Expand Down
20 changes: 19 additions & 1 deletion frontend/src/openapi/v2/services/DatasetsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,22 @@ export class DatasetsService {
});
}

}
/**
* Download Dataset
* @param datasetId
* @returns DatasetOut Successful Response
* @throws ApiError
*/
public static downloadDatasetApiV2DatasetsDatasetIdDownloadGet(
datasetId: string,
): CancelablePromise<DatasetOut> {
return __request({
method: 'GET',
path: `/api/v2/datasets/${datasetId}/download`,
errors: {
422: `Validation Error`,
},
});
}

}