Refactor uploadFile to support multiple file uploads and update related types and handlers

This commit is contained in:
2025-07-12 14:25:03 +02:00
parent 51c6f62c44
commit ff4d1de2b7
4 changed files with 105 additions and 56 deletions

View File

@@ -4,7 +4,9 @@ import {
API_BASE_URL, API_BASE_URL,
isLookupResponse, isLookupResponse,
isSaveFileResponse, isSaveFileResponse,
isUploadFilesResponse,
type SaveFileResponse, type SaveFileResponse,
type UploadFilesResponse,
} from '@/types/api'; } from '@/types/api';
/** /**
@@ -203,19 +205,19 @@ export const moveFile = async (
* @param workspaceName - The name of the workspace * @param workspaceName - The name of the workspace
* @param directoryPath - The directory path where files should be uploaded * @param directoryPath - The directory path where files should be uploaded
* @param files - Multiple files to upload * @param files - Multiple files to upload
* @returns {Promise<SaveFileResponse>} A promise that resolves to the upload file response * @returns {Promise<UploadFilesResponse>} A promise that resolves to the upload file response
* @throws {Error} If the API call fails or returns an invalid response * @throws {Error} If the API call fails or returns an invalid response
*/ */
export const uploadFile = async ( export const uploadFile = async (
workspaceName: string, workspaceName: string,
directoryPath: string, directoryPath: string,
files: FileList files: FileList
): Promise<SaveFileResponse> => { ): Promise<UploadFilesResponse> => {
const formData = new FormData(); const formData = new FormData();
// Add all files to the form data // Add all files to the form data
Array.from(files).forEach((file) => { Array.from(files).forEach((file) => {
formData.append('file', file); formData.append('files', file);
}); });
const response = await apiCall( const response = await apiCall(
@@ -228,7 +230,7 @@ export const uploadFile = async (
} }
); );
const data: unknown = await response.json(); const data: unknown = await response.json();
if (!isSaveFileResponse(data)) { if (!isUploadFilesResponse(data)) {
throw new Error('Invalid upload file response received from API'); throw new Error('Invalid upload file response received from API');
} }
return data; return data;

View File

@@ -122,7 +122,6 @@ export const useFileOperations = (): UseFileOperationsResult => {
if (!currentWorkspace) return false; if (!currentWorkspace) return false;
try { try {
// Use unified upload API that handles both single and multiple files
await uploadFile(currentWorkspace.name, targetPath || '', files); await uploadFile(currentWorkspace.name, targetPath || '', files);
notifications.show({ notifications.show({

View File

@@ -98,6 +98,24 @@ export function isSaveFileResponse(obj: unknown): obj is SaveFileResponse {
); );
} }
export interface UploadFilesResponse {
filePaths: string[];
}
export function isUploadFilesResponse(
obj: unknown
): obj is UploadFilesResponse {
return (
typeof obj === 'object' &&
obj !== null &&
'filePaths' in obj &&
Array.isArray((obj as UploadFilesResponse).filePaths) &&
(obj as UploadFilesResponse).filePaths.every(
(path) => typeof path === 'string'
)
);
}
export interface UpdateLastOpenedFileRequest { export interface UpdateLastOpenedFileRequest {
filePath: string; filePath: string;
} }

View File

@@ -24,6 +24,10 @@ type SaveFileResponse struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
type UploadFilesResponse struct {
FilePaths []string `json:"filePaths"`
}
// LastOpenedFileResponse represents a response to a last opened file request // LastOpenedFileResponse represents a response to a last opened file request
type LastOpenedFileResponse struct { type LastOpenedFileResponse struct {
LastOpenedFilePath string `json:"lastOpenedFilePath"` LastOpenedFilePath string `json:"lastOpenedFilePath"`
@@ -294,8 +298,8 @@ func (h *Handler) SaveFile() http.HandlerFunc {
} }
// UploadFile godoc // UploadFile godoc
// @Summary Upload file // @Summary Upload files
// @Description Uploads a file to the user's workspace // @Description Uploads one or more files to the user's workspace
// @Tags files // @Tags files
// @ID uploadFile // @ID uploadFile
// @Security CookieAuth // @Security CookieAuth
@@ -303,10 +307,13 @@ func (h *Handler) SaveFile() http.HandlerFunc {
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param file_path query string true "Directory path" // @Param file_path query string true "Directory path"
// @Param file formData file true "File to upload" // @Param files formData file true "Files to upload"
// @Success 200 {object} SaveFileResponse // @Success 200 {object} UploadFilesResponse
// @Failure 400 {object} ErrorResponse "Failed to get file from form" // @Failure 400 {object} ErrorResponse "No files found in form"
// @Failure 400 {object} ErrorResponse "file_path is required"
// @Failure 400 {object} ErrorResponse "Invalid file path" // @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 400 {object} ErrorResponse "Empty file uploaded"
// @Failure 400 {object} ErrorResponse "Failed to get file from form"
// @Failure 500 {object} ErrorResponse "Failed to read uploaded file" // @Failure 500 {object} ErrorResponse "Failed to read uploaded file"
// @Failure 500 {object} ErrorResponse "Failed to save file" // @Failure 500 {object} ErrorResponse "Failed to save file"
// @Router /workspaces/{workspace_name}/files/upload/ [post] // @Router /workspaces/{workspace_name}/files/upload/ [post]
@@ -323,7 +330,45 @@ func (h *Handler) UploadFile() http.HandlerFunc {
"clientIP", r.RemoteAddr, "clientIP", r.RemoteAddr,
) )
file, header, err := r.FormFile("file") form := r.MultipartForm
if form == nil || len(form.File) == 0 {
log.Debug("no files found in form")
respondError(w, "No files found in form", http.StatusBadRequest)
return
}
uploadPath := r.URL.Query().Get("file_path")
if uploadPath == "" {
log.Debug("missing file_path parameter")
respondError(w, "file_path is required", http.StatusBadRequest)
return
}
decodedPath, err := url.PathUnescape(uploadPath)
if err != nil {
log.Error("failed to decode file path",
"filePath", uploadPath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
uploadedPaths := []string{}
for _, formFile := range form.File["files"] {
if formFile.Filename == "" || formFile.Size == 0 {
log.Debug("empty file uploaded",
"fileName", formFile.Filename,
"fileSize", formFile.Size,
)
respondError(w, "Empty file uploaded", http.StatusBadRequest)
return
}
// Open the uploaded file
file, err := formFile.Open()
if err != nil { if err != nil {
log.Error("failed to get file from form", log.Error("failed to get file from form",
"error", err.Error(), "error", err.Error(),
@@ -339,40 +384,24 @@ func (h *Handler) UploadFile() http.HandlerFunc {
} }
}() }()
filePath := r.URL.Query().Get("file_path") filePath := decodedPath + "/" + formFile.Filename
if filePath == "" {
log.Debug("missing file_path parameter")
respondError(w, "file_path is required", http.StatusBadRequest)
return
}
decodedPath, err := url.PathUnescape(filePath) content := make([]byte, formFile.Size)
if err != nil {
log.Error("failed to decode file path",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
decodedPath = decodedPath + "/" + header.Filename
content := make([]byte, header.Size)
_, err = file.Read(content) _, err = file.Read(content)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
log.Error("failed to read uploaded file", log.Error("failed to read uploaded file",
"filePath", decodedPath, "filePath", filePath,
"error", err.Error(), "error", err.Error(),
) )
respondError(w, "Failed to read uploaded file", http.StatusInternalServerError) respondError(w, "Failed to read uploaded file", http.StatusInternalServerError)
return return
} }
err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, decodedPath, content) err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content)
if err != nil { if err != nil {
if storage.IsPathValidationError(err) { if storage.IsPathValidationError(err) {
log.Error("invalid file path attempted", log.Error("invalid file path attempted",
"filePath", decodedPath, "filePath", filePath,
"error", err.Error(), "error", err.Error(),
) )
respondError(w, "Invalid file path", http.StatusBadRequest) respondError(w, "Invalid file path", http.StatusBadRequest)
@@ -380,7 +409,7 @@ func (h *Handler) UploadFile() http.HandlerFunc {
} }
log.Error("failed to save file", log.Error("failed to save file",
"filePath", decodedPath, "filePath", filePath,
"contentSize", len(content), "contentSize", len(content),
"error", err.Error(), "error", err.Error(),
) )
@@ -388,10 +417,11 @@ func (h *Handler) UploadFile() http.HandlerFunc {
return return
} }
response := SaveFileResponse{ uploadedPaths = append(uploadedPaths, filePath)
FilePath: decodedPath, }
Size: int64(len(content)),
UpdatedAt: time.Now().UTC(), response := UploadFilesResponse{
FilePaths: uploadedPaths,
} }
respondJSON(w, response) respondJSON(w, response)
} }