myxce/src/frontend/apiserver/controllers/files.go

540 lines
17 KiB
Go

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