diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts index 6558e764e8..e89f99be12 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts @@ -39,7 +39,7 @@ import { } from "../../shared/utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; -import { importFile } from "../../../../../services/api/studies/raw"; +import { uploadFile } from "../../../../../services/api/studies/raw"; import { fetchMatrixFn } from "../../../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; import usePrompt from "../../../../../hooks/usePrompt"; import { Aggregate, Column, Operation } from "../../shared/constants"; @@ -251,7 +251,8 @@ export function useMatrix( const handleUpload = async (file: File) => { try { - await importFile({ file, studyId, path: url }); + await uploadFile({ file, studyId, path: url }); + // TODO: update the API to return the uploaded file data and remove this await fetchMatrix(); } catch (e) { enqueueErrorSnackbar(t("matrix.error.import"), e as Error); diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx index fc1087a927..e4b61d8587 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx @@ -166,7 +166,7 @@ describe("useMatrix", () => { describe("File operations", () => { test("should handle file import", async () => { const mockFile = new File([""], "test.csv", { type: "text/csv" }); - vi.mocked(rawStudy.importFile).mockResolvedValue(); + vi.mocked(rawStudy.uploadFile).mockResolvedValue(); const hook = await setupHook(); @@ -174,7 +174,7 @@ describe("useMatrix", () => { await hook.result.current.handleUpload(mockFile); }); - expect(rawStudy.importFile).toHaveBeenCalledWith({ + expect(rawStudy.uploadFile).toHaveBeenCalledWith({ file: mockFile, studyId: DATA.studyId, path: DATA.url, diff --git a/webapp/src/components/common/buttons/DownloadMatrixButton.tsx b/webapp/src/components/common/buttons/DownloadMatrixButton.tsx index 55e0d029c3..0f27f75eef 100644 --- a/webapp/src/components/common/buttons/DownloadMatrixButton.tsx +++ b/webapp/src/components/common/buttons/DownloadMatrixButton.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { downloadMatrix } from "../../../services/api/studies/raw"; +import { getMatrixFile } from "../../../services/api/studies/raw"; import { downloadFile } from "../../../utils/fileUtils"; import { StudyMetadata } from "../../../common/types"; import { useTranslation } from "react-i18next"; @@ -51,7 +51,7 @@ function DownloadMatrixButton(props: DownloadMatrixButtonProps) { const isXlsx = format === "xlsx"; - const res = await downloadMatrix({ + const matrixFile = await getMatrixFile({ studyId, path, format, @@ -62,7 +62,7 @@ function DownloadMatrixButton(props: DownloadMatrixButtonProps) { const extension = format === "csv (semicolon)" ? "csv" : format; return downloadFile( - res, + matrixFile, `matrix_${studyId}_${path.replace("/", "_")}.${extension}`, ); }; diff --git a/webapp/src/components/common/buttons/UploadFileButton.tsx b/webapp/src/components/common/buttons/UploadFileButton.tsx index cc16594be0..872b05b01a 100644 --- a/webapp/src/components/common/buttons/UploadFileButton.tsx +++ b/webapp/src/components/common/buttons/UploadFileButton.tsx @@ -21,7 +21,7 @@ import { toError } from "../../../utils/fnUtils"; import { Accept, useDropzone } from "react-dropzone"; import { StudyMetadata } from "../../../common/types"; import { useSnackbar } from "notistack"; -import { importFile } from "../../../services/api/studies/raw"; +import { uploadFile } from "../../../services/api/studies/raw"; type ValidateResult = boolean | null | undefined; type Validate = (file: File) => ValidateResult | Promise; @@ -89,7 +89,7 @@ function UploadFileButton(props: UploadFileButtonProps) { const filePath = typeof path === "function" ? path(fileToUpload) : path; - await importFile({ + await uploadFile({ studyId, path: filePath, file: fileToUpload, diff --git a/webapp/src/services/api/studies/raw/index.ts b/webapp/src/services/api/studies/raw/index.ts index b334611562..673ae22013 100644 --- a/webapp/src/services/api/studies/raw/index.ts +++ b/webapp/src/services/api/studies/raw/index.ts @@ -15,29 +15,68 @@ import client from "../../client"; import type { DeleteFileParams, - DownloadMatrixParams, - ImportFileParams, + GetMatrixFileParams, RawFile, + UploadFileParams, } from "./types"; -export async function downloadMatrix(params: DownloadMatrixParams) { +/** + * Reads a matrix file from a study's raw files. + * + * @param params - Parameters for reading the matrix + * @param params.studyId - Unique identifier of the study + * @param params.path - Path to the matrix file + * @param params.format - Optional export format for the matrix + * @param params.header - Whether to include headers + * @param params.index - Whether to include indices + * @returns Promise containing the matrix data as a Blob + */ +export async function getMatrixFile( + params: GetMatrixFileParams, +): Promise { const { studyId, ...queryParams } = params; - const url = `/v1/studies/${studyId}/raw/download`; - - const { data } = await client.get(url, { - params: queryParams, - responseType: "blob", - }); + const { data } = await client.get( + `/v1/studies/${studyId}/raw/download`, + { + params: queryParams, + responseType: "blob", + }, + ); return data; } -export async function importFile(params: ImportFileParams) { +/** + * Uploads a file to a study's raw storage, creating or updating it based on existence. + * + * !Note: This method currently uses a poorly named endpoint (/raw). The endpoint structure + * should be refactored to follow REST principles: + * - PUT /raw/files/{path}/content - Upload file content (multipart/form-data, large files) `uploadFile` + * - PATCH /raw/files/{path} - Update existing file (for metadata or small content changes) `updateFile` + * - POST /raw/files - Create new file (system generates path) `createFile` + * - GET /raw/files/{path} - Retrieve file `getFile` + * - DELETE /raw/files/{path} - Delete file `deleteFile` + * + * PUT is used for upload since we're updating a resource at a known path, whether + * it exists or not (idempotent operation). + * + * TODO: + * 1. Migrate to the new REST endpoints structure + * 2. Remove createMissing param and handle directory creation automatically + * + * @param params - Parameters for the file upload + * @param params.studyId - Unique identifier of the study + * @param params.path - Destination path for the file + * @param params.file - File content to upload + * @param params.createMissing - Whether to create missing parent directories + * @param params.onUploadProgress - Callback for upload progress updates + * @returns Promise that resolves when the upload is complete + */ +export async function uploadFile(params: UploadFileParams): Promise { const { studyId, file, onUploadProgress, ...queryParams } = params; - const url = `/v1/studies/${studyId}/raw`; const body = { file }; - await client.putForm(url, body, { + await client.putForm(`/v1/studies/${studyId}/raw`, body, { params: { ...queryParams, create_missing: queryParams.createMissing, @@ -46,11 +85,17 @@ export async function importFile(params: ImportFileParams) { }); } -export async function deleteFile(params: DeleteFileParams) { +/** + * Deletes a raw file from a study. + * + * @param params - Parameters for deleting the file + * @param params.studyId - Unique identifier of the study + * @param params.path - Path to the file to delete + * @returns Promise that resolves when the deletion is complete + */ +export async function deleteFile(params: DeleteFileParams): Promise { const { studyId, path } = params; - const url = `/v1/studies/${studyId}/raw`; - - await client.delete(url, { params: { path } }); + await client.delete(`/v1/studies/${studyId}/raw`, { params: { path } }); } /** @@ -64,7 +109,7 @@ export async function getRawFile( studyId: string, filePath: string, ): Promise { - const response = await client.get( + const { data, headers } = await client.get( `/v1/studies/${studyId}/raw/original-file`, { params: { @@ -74,18 +119,20 @@ export async function getRawFile( }, ); - const contentDisposition = response.headers["content-disposition"]; + // Get the original file name from the response Headers + const contentDisposition = headers["content-disposition"]; let filename = filePath.split("/").pop() || "file"; // fallback filename if (contentDisposition) { const matches = /filename=([^;]+)/.exec(contentDisposition); + if (matches?.[1]) { filename = matches[1].replace(/"/g, "").trim(); } } return { - data: response.data, - filename: filename, + data, + filename, }; } diff --git a/webapp/src/services/api/studies/raw/types.ts b/webapp/src/services/api/studies/raw/types.ts index 43ff274183..e4fae4dd00 100644 --- a/webapp/src/services/api/studies/raw/types.ts +++ b/webapp/src/services/api/studies/raw/types.ts @@ -17,9 +17,10 @@ import type { StudyMetadata } from "../../../../common/types"; import { O } from "ts-toolbelt"; import { TableExportFormat } from "./constants"; +// Available export formats for matrix tables export type TTableExportFormat = O.UnionOf; -export interface DownloadMatrixParams { +export interface GetMatrixFileParams { studyId: StudyMetadata["id"]; path: string; format?: TTableExportFormat; @@ -27,10 +28,11 @@ export interface DownloadMatrixParams { index?: boolean; } -export interface ImportFileParams { +export interface UploadFileParams { studyId: StudyMetadata["id"]; path: string; file: File; + // Flag to indicate whether to create file and directories if missing createMissing?: boolean; onUploadProgress?: AxiosRequestConfig["onUploadProgress"]; }