540 lines
17 KiB
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
|