mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Refactor uploadFile to support multiple file uploads and update related types and handlers
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,75 +330,98 @@ func (h *Handler) UploadFile() http.HandlerFunc {
|
|||||||
"clientIP", r.RemoteAddr,
|
"clientIP", r.RemoteAddr,
|
||||||
)
|
)
|
||||||
|
|
||||||
file, header, err := r.FormFile("file")
|
form := r.MultipartForm
|
||||||
if err != nil {
|
if form == nil || len(form.File) == 0 {
|
||||||
log.Error("failed to get file from form",
|
log.Debug("no files found in form")
|
||||||
"error", err.Error(),
|
respondError(w, "No files found in form", http.StatusBadRequest)
|
||||||
)
|
|
||||||
respondError(w, "Failed to get file from form", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
if err := file.Close(); err != nil {
|
|
||||||
log.Error("failed to close uploaded file",
|
|
||||||
"error", err.Error(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
filePath := r.URL.Query().Get("file_path")
|
uploadPath := r.URL.Query().Get("file_path")
|
||||||
if filePath == "" {
|
if uploadPath == "" {
|
||||||
log.Debug("missing file_path parameter")
|
log.Debug("missing file_path parameter")
|
||||||
respondError(w, "file_path is required", http.StatusBadRequest)
|
respondError(w, "file_path is required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
decodedPath, err := url.PathUnescape(filePath)
|
decodedPath, err := url.PathUnescape(uploadPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to decode file path",
|
log.Error("failed to decode file path",
|
||||||
"filePath", filePath,
|
"filePath", uploadPath,
|
||||||
"error", err.Error(),
|
"error", err.Error(),
|
||||||
)
|
)
|
||||||
respondError(w, "Invalid file path", http.StatusBadRequest)
|
respondError(w, "Invalid file path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
decodedPath = decodedPath + "/" + header.Filename
|
|
||||||
|
|
||||||
content := make([]byte, header.Size)
|
uploadedPaths := []string{}
|
||||||
_, err = file.Read(content)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
log.Error("failed to read uploaded file",
|
|
||||||
"filePath", decodedPath,
|
|
||||||
"error", err.Error(),
|
|
||||||
)
|
|
||||||
respondError(w, "Failed to read uploaded file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, decodedPath, content)
|
for _, formFile := range form.File["files"] {
|
||||||
if err != nil {
|
|
||||||
if storage.IsPathValidationError(err) {
|
if formFile.Filename == "" || formFile.Size == 0 {
|
||||||
log.Error("invalid file path attempted",
|
log.Debug("empty file uploaded",
|
||||||
"filePath", decodedPath,
|
"fileName", formFile.Filename,
|
||||||
"error", err.Error(),
|
"fileSize", formFile.Size,
|
||||||
)
|
)
|
||||||
respondError(w, "Invalid file path", http.StatusBadRequest)
|
respondError(w, "Empty file uploaded", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error("failed to save file",
|
// Open the uploaded file
|
||||||
"filePath", decodedPath,
|
file, err := formFile.Open()
|
||||||
"contentSize", len(content),
|
if err != nil {
|
||||||
"error", err.Error(),
|
log.Error("failed to get file from form",
|
||||||
)
|
"error", err.Error(),
|
||||||
respondError(w, "Failed to save file", http.StatusInternalServerError)
|
)
|
||||||
return
|
respondError(w, "Failed to get file from form", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
log.Error("failed to close uploaded file",
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
filePath := decodedPath + "/" + formFile.Filename
|
||||||
|
|
||||||
|
content := make([]byte, formFile.Size)
|
||||||
|
_, err = file.Read(content)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
log.Error("failed to read uploaded file",
|
||||||
|
"filePath", filePath,
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
respondError(w, "Failed to read uploaded file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content)
|
||||||
|
if err != nil {
|
||||||
|
if storage.IsPathValidationError(err) {
|
||||||
|
log.Error("invalid file path attempted",
|
||||||
|
"filePath", filePath,
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
respondError(w, "Invalid file path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error("failed to save file",
|
||||||
|
"filePath", filePath,
|
||||||
|
"contentSize", len(content),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
respondError(w, "Failed to save file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedPaths = append(uploadedPaths, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := SaveFileResponse{
|
response := UploadFilesResponse{
|
||||||
FilePath: decodedPath,
|
FilePaths: uploadedPaths,
|
||||||
Size: int64(len(content)),
|
|
||||||
UpdatedAt: time.Now().UTC(),
|
|
||||||
}
|
}
|
||||||
respondJSON(w, response)
|
respondJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user