package controllers import ( "apiserver/common" "apiserver/utils" logger "apiserver/utils" "archive/zip" "crypto/md5" "fmt" "io" "mime/multipart" "net/http" "os" "path/filepath" "strconv" "strings" "time" "github.com/google/uuid" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type FilesController struct{} func (fc *FilesController) UploadFiles(ctx *gin.Context) { logger.Debug("Enter UploadFiles() function from ", zap.String("client_ip", ctx.ClientIP())) // use form-data format to upload files // check token token := ctx.GetHeader("Authorization") submitUser, err := GetUsernameFromToken(token) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } // Get the home directory of the submitUser userHomeDir := filepath.Join("/home", submitUser) // Check if the directory exists if _, err := os.Stat(userHomeDir); os.IsNotExist(err) { ReturnError(ctx, common.ERROR_CODE, fmt.Sprintf("User home directory does not exist: %s", userHomeDir), nil) return } // Check Content-Type if it contains "multipart/form-data" contentType := ctx.ContentType() if !strings.Contains(contentType, "form-data") { ReturnError(ctx, common.ERROR_CODE, "Invalid Content-Type, expected multipart/form-data", nil) return } // get all uploaded files form, err := ctx.MultipartForm() if err != nil { ReturnError(ctx, common.ERROR_CODE, "Failed to parse multipart form", nil) return } files := form.File["files"] if len(files) == 0 { ReturnError(ctx, common.ERROR_CODE, "No files uploaded", nil) return } // get configuration of file upload fileUploadConfig := utils.FileUploadYamlConfig if fileUploadConfig == nil { ReturnError(ctx, common.ERROR_CODE, "File upload configuration is missing", nil) return } webUrl := utils.GetServerHostPort(ctx) // Initialize structures to hold results successFiles := []map[string]interface{}{} failedFiles := []map[string]interface{}{} for _, file := range files { // check disallowed file types if err := validateFileType(file, fileUploadConfig); err != nil { logger.Error("File validation failed", zap.String("filename", file.Filename), zap.Error(err)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "File validation failed", }) continue } // check uploaded file size maxSizeMB, err := utils.ParseMaxSize(fileUploadConfig.MaxSize) if err != nil { logger.Error("Invalid MaxSize configuration", zap.String("max_size", fileUploadConfig.MaxSize), zap.Error(err)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "Invalid MaxSize configuration", }) continue } fileSizeMB := file.Size / (1024 * 1024) if fileSizeMB > maxSizeMB { logger.Error("File size exceeds limit", zap.String("filename", file.Filename), zap.Int64("size", file.Size), zap.Int64("max_size", maxSizeMB)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "File size exceeds limit", }) continue } // Open the uploaded file srcFile, err := file.Open() if err != nil { logger.Error("Failed to open uploaded file", zap.String("filename", file.Filename), zap.Error(err)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "Failed to open file", }) continue } defer srcFile.Close() // Generate MD5 hash for the file content hasher := md5.New() if _, err := io.Copy(hasher, srcFile); err != nil { logger.Error("Failed to hash file content", zap.String("filename", file.Filename), zap.Error(err)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "Failed to hash file content", }) continue } contentHash := hasher.Sum(nil) // Generate UUID based on the MD5 hash contentUUID := uuid.NewMD5(uuid.NameSpaceOID, contentHash[:]).String() // Add submission date (YYYYMMDD) as a prefix to the UUID submissionDate := time.Now().Format("20060102") // Using Go's standard library to get the current date contentUUID = submissionDate + "_" + contentUUID // Create a directory based on the UUID uuidDir := filepath.Join(userHomeDir, "./apiserver_uploads", submissionDate) if err := os.MkdirAll(uuidDir, os.ModePerm); err != nil { logger.Error("Failed to create UUID directory", zap.String("uuid_dir", uuidDir), zap.Error(err)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "Failed to create directory", }) continue } // Save file to the UUID directory savePath := filepath.Join(uuidDir, contentUUID) // Check if the file already exists if _, err := os.Stat(savePath); err == nil { logger.Warn("File already exists, skipping upload", zap.String("filename", contentUUID)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "File already exists", }) continue } if err := ctx.SaveUploadedFile(file, savePath); err != nil { logger.Error("Failed to save file", zap.String("filename", contentUUID), zap.Error(err)) failedFiles = append(failedFiles, map[string]interface{}{ "file_name": file.Filename, "msg": "Failed to save file", }) continue } // Append source, destination, web URL, and encoded path to the result successFiles = append(successFiles, map[string]interface{}{ "src_file": file.Filename, "dest_file": savePath, "web_url": webUrl + "/" + contentUUID + "/" + file.Filename, }) } response, successMsg := BuildBatchOperationResponse("Upload", "File", successFiles, failedFiles) overallStatus := response["status"].(string) logger.Info("Finished upload files", zap.String("overallStatus", overallStatus), zap.String("client_ip", ctx.ClientIP())) ReturnSuccess(ctx, common.ACCEPTED, successMsg, response) } func validateFileType(file *multipart.FileHeader, config *utils.FileUploadConfig) error { // check content type fileType := file.Header.Get("Content-Type") for _, disallowedType := range config.DisallowedTypes { if fileType == disallowedType { return fmt.Errorf("File type is not allowed: %s (type: %s)", file.Filename, fileType) } } // check file extension fileExt := strings.ToLower(filepath.Ext(file.Filename)) if fileExt == ".exe" { return fmt.Errorf("File extension is not allowed: %s (extension: %s)", file.Filename, fileExt) } return nil } func (fc *FilesController) DeleteFiles(ctx *gin.Context) { logger.Debug("Enter DeleteFiles() function from ", zap.String("client_ip", ctx.ClientIP())) // check token token := ctx.GetHeader("Authorization") submitUser, err := GetUsernameFromToken(token) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } // Get the home directory of the submitUser userHomeDir := filepath.Join("/home", submitUser) // Check if the directory exists if _, err := os.Stat(userHomeDir); os.IsNotExist(err) { ReturnError(ctx, common.ERROR_CODE, fmt.Sprintf("User home directory does not exist: %s", userHomeDir), nil) return } filePathStr := ctx.Query("path") if filePathStr == "" { ReturnError(ctx, common.INVALID_PARAMS_CODE, "File path is required", nil) return } fileList := strings.Split(filePathStr, ",") var successfulDeletes []string var failedDeletes []string webUrl := utils.GetServerHostPort(ctx) for _, path := range fileList { trimmedPath := strings.TrimSpace(path) // Check if the path start with web_url if strings.HasPrefix(trimmedPath, webUrl) { trimmedPath = strings.TrimPrefix(trimmedPath, webUrl+"/") } // Check if the path is absolute, if not, prepend userHomeDir if !filepath.IsAbs(trimmedPath) { trimmedPath = filepath.Join(userHomeDir, "./apiserver_uploads", trimmedPath) } // Check if the path exists fileInfo, err := os.Stat(trimmedPath) if os.IsNotExist(err) { logger.Error("The file doesn't exist", zap.Error(err)) failedDeletes = append(failedDeletes, trimmedPath) continue } // Delete file or directory if fileInfo.IsDir() { // Delete directory and its contents if err := os.RemoveAll(trimmedPath); err != nil { logger.Error("Failed to delete directory", zap.String("path", trimmedPath), zap.Error(err)) failedDeletes = append(failedDeletes, trimmedPath) continue } successfulDeletes = append(successfulDeletes, trimmedPath) } else { // Delete file if err := os.Remove(trimmedPath); err != nil { logger.Error("Failed to delete file", zap.String("path", trimmedPath), zap.Error(err)) failedDeletes = append(failedDeletes, trimmedPath) continue } successfulDeletes = append(successfulDeletes, trimmedPath) } } // Prepare response var response gin.H if len(failedDeletes) > 0 { if len(successfulDeletes) > 0 { response = gin.H{ "successful_deletes": successfulDeletes, "failed_deletes": failedDeletes, } } else { response = gin.H{ "failed_deletes": failedDeletes, } } ReturnError(ctx, common.StatusPartialContent, common.PARTIAL_DELETE_FILES_MSG, response) } else { response = gin.H{ "successful_deletes": successfulDeletes, } ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, response) } } func (fc *FilesController) GetFileLists(ctx *gin.Context) { logger.Debug("Enter GetFileLists() function from ", zap.String("client_ip", ctx.ClientIP())) // check token token := ctx.GetHeader("Authorization") submitUser, err := GetUsernameFromToken(token) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } // Get the home directory of the submitUser userHomeDir := filepath.Join("/home", submitUser) // Check if the directory exists if _, err := os.Stat(userHomeDir); os.IsNotExist(err) { ReturnError(ctx, common.ERROR_CODE, fmt.Sprintf("User home directory does not exist: %s", userHomeDir), nil) return } _, err = GetRequireResourcePermission(submitUser, "file:view") if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil) return } filePathStr := ctx.Query("path") if filePathStr == "" { ReturnError(ctx, common.INVALID_PARAMS_CODE, "File path is required", nil) return } webUrl := utils.GetServerHostPort(ctx) if strings.HasPrefix(filePathStr, webUrl) { filePathStr = strings.TrimPrefix(filePathStr, webUrl+"/") } // if only filename, it will join the real path if !strings.HasPrefix(filePathStr, "/") && !strings.Contains(filePathStr, string(os.PathSeparator)) { filePathStr = filepath.Join(userHomeDir, "./apiserver_uploads", filePathStr) } // Check if the path belongs to the current submitUser if !strings.HasPrefix(filePathStr, userHomeDir) { logger.Error("Access denied: path does not belong to the user", zap.String("path", filePathStr), zap.String("user_home", userHomeDir)) ReturnError(ctx, common.ERROR_CODE, "Access denied: path does not belong to the user", nil) return } // Check if the path is a file or directory fileInfo, err := os.Stat(filePathStr) if err != nil { logger.Error("Failed to get file or directory info", zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, "Invalid file or directory path", nil) return } var fileInfos []gin.H if fileInfo.IsDir() { // Read the directory files, err := os.ReadDir(filePathStr) if err != nil { logger.Error("Can't read the directory", zap.Error(err)) ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Can't read the directory"}) return } // Collect file information for _, file := range files { info, err := file.Info() if err != nil { logger.Error("Failed to get file info", zap.String("file", file.Name()), zap.Error(err)) continue } // Ensure the name is correct name := file.Name() fileInfos = append(fileInfos, gin.H{ "name": name, "size": info.Size(), "is_dir": file.IsDir(), "mod_time": info.ModTime().Format("2006-01-02 15:04:05"), "perm": fmt.Sprintf("%#o", info.Mode().Perm()), }) } } else { fileInfos = append(fileInfos, gin.H{ "name": filepath.Base(filePathStr), // Ensure the name is correct "size": fileInfo.Size(), "is_dir": fileInfo.IsDir(), "mod_time": fileInfo.ModTime().Format("2006-01-02 15:04:05"), "perm": fmt.Sprintf("%#o", fileInfo.Mode().Perm()), }) } // Return file list as JSON ctx.JSON(http.StatusOK, gin.H{ "files": fileInfos, }) } func (fc *FilesController) DownloadFile(ctx *gin.Context) { logger.Debug("Enter DownloadFile() function from ", zap.String("client_ip", ctx.ClientIP())) // Validate token and get submitUser token := ctx.GetHeader("Authorization") submitUser, err := GetUsernameFromToken(token) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } // Get the home directory of the submitUser userHomeDir := filepath.Join("/home", submitUser) if _, err := os.Stat(userHomeDir); os.IsNotExist(err) { ReturnError(ctx, common.ERROR_CODE, fmt.Sprintf("User home directory does not exist: %s", userHomeDir), nil) return } // Get UUID from URL parameter uuid := ctx.Param("uuid") if uuid == "" { ReturnError(ctx, common.INVALID_PARAMS_CODE, "UUID is required", nil) return } // Get filename from URL parameter filename := ctx.Param("filename") if filename == "" { ReturnError(ctx, common.INVALID_PARAMS_CODE, "Filename is required", nil) return } // Extract the date part from the UUID (assuming format YYYYMMDD_UUID) parts := strings.SplitN(uuid, "_", 2) if len(parts) != 2 { ReturnError(ctx, common.INVALID_PARAMS_CODE, "Invalid UUID format", nil) return } datePart := parts[0] // Construct the path based on the extracted date and UUID uuidPath := filepath.Join(userHomeDir, "./apiserver_uploads", datePart, uuid) // Check if the path exists fileInfo, err := os.Stat(uuidPath) if os.IsNotExist(err) { ReturnError(ctx, common.ERROR_NOT_FOUND_CODE, fmt.Sprintf("Path not found: %s", uuidPath), nil) return } if fileInfo.IsDir() { // If it's a directory, check its contents files, err := os.ReadDir(uuidPath) if err != nil { logger.Error("Failed to read directory", zap.String("path", uuidPath), zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, "Failed to read directory", nil) return } if len(files) == 1 && !files[0].IsDir() { // If the directory contains a single file, download the file directly filePath := filepath.Join(uuidPath, files[0].Name()) ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", strconv.Quote(filename))) ctx.Header("Content-Type", "application/octet-stream") http.ServeFile(ctx.Writer, ctx.Request, filePath) // Log the download event logger.Info("File downloaded successfully", zap.String("filename", files[0].Name()), zap.String("user", submitUser)) } else { // If the directory contains multiple files or subdirectories, compress and download zipFileName := strconv.Quote(filename) + ".zip" ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipFileName)) ctx.Header("Content-Type", "application/zip") zipWriter := zip.NewWriter(ctx.Writer) defer zipWriter.Close() err := filepath.Walk(uuidPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Create a header for the file or directory header, err := zip.FileInfoHeader(info) if err != nil { return err } header.Name, _ = filepath.Rel(uuidPath, path) // Relative path inside the ZIP if info.IsDir() { header.Name += "/" } else { header.Method = zip.Deflate } // Write the header to the ZIP writer, err := zipWriter.CreateHeader(header) if err != nil { return err } // If it's a file, write its content if !info.IsDir() { file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(writer, file) if err != nil { return err } } return nil }) if err != nil { logger.Error("Failed to create ZIP file", zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, "Failed to create ZIP file", nil) return } // Log the download event logger.Info("Directory downloaded successfully as ZIP", zap.String("directory", uuidPath), zap.String("user", submitUser)) } } else { // If it's a single file, download it directly ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", strconv.Quote(filename))) ctx.Header("Content-Type", "application/octet-stream") http.ServeFile(ctx.Writer, ctx.Request, uuidPath) // Log the download event logger.Info("File downloaded successfully", zap.String("filename", filename), zap.String("user", submitUser)) } } // End of DownloadFile function