algolia search

Tìm thấy x bài viết trong xms.

Gin Framework: Lỗi thay đổi quyền thư mục khi upload file


Giới thiệu

Trong quá trình phát triển API upload file với Gin Framework, tôi gặp phải một vấn đề khá oái ăm: thư mục lưu trữ file ban đầu có quyền 755 (rwxr-xr-x), nhưng sau khi gọi hàm SaveUploadedFile(), quyền tự động thay đổi thành 750 (rwxr-x). Điều này gây ra lỗi permission denied khi web server cố gắng truy cập các file đã upload.

Tái hiện vấn đề

Code ban đầu khá đơn giản:

func uploadHandler(c *gin.Context) {
    var body UploadRequest
    if err := c.ShouldBind(&body); err != nil {
        c.AbortWithStatusJSON(400, gin.H{"message": err.Error()})
        return
    }

    fullPath := "/var/www/uploads/user123/index.html"

    // Thư mục /var/www/uploads/user123 có quyền 755
    // Sau khi chạy dòng này, quyền đổi thành 750 😱
    if err := c.SaveUploadedFile(body.File, fullPath); err != nil {
        c.AbortWithStatusJSON(400, gin.H{"message": err.Error()})
        return
    }

    c.JSON(200, gin.H{"message": "success"})
}

Kết quả: Thư mục đã tồn tại với quyền 755, nhưng sau khi upload file, quyền bị thay đổi thành 750.

Nguyên nhân

Sau khi đào sâu vào source code của Gin Framework phiên bản mới, tôi phát hiện ra rằng hàm SaveUploadedFile đã được thay đổi đáng kể:

// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()

    var mode os.FileMode = 0o750  // Mặc định 750 thay vì 755
    if len(perm) > 0 {
        mode = perm[0]
    }

    dir := filepath.Dir(dst)
    if err = os.MkdirAll(dir, mode); err != nil {
        return err
    }

    // Vấn đề nằm ở đây: luôn gọi Chmod ngay cả khi thư mục đã tồn tại
    if err = os.Chmod(dir, mode); err != nil {
        return err
    }

    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, src)
    return err
}

Phân tích vấn đề:

  1. Quyền mặc định thay đổi: Từ 0755 (hoặc không có default rõ ràng) sang 0750
  2. Luôn gọi os.Chmod: Dù thư mục đã tồn tại hay chưa, hàm vẫn gọi os.Chmod và ghi đè quyền cũ
  3. Không kiểm tra sự tồn tại: Không có logic để bỏ qua việc thay đổi quyền nếu thư mục đã tồn tại

Thiết kế này có ý đồ tốt (cho phép developer kiểm soát quyền), nhưng hành vi mặc định lại gây ra breaking change cho những ai đang dùng phiên bản cũ.

Giải pháp

Thay vì sử dụng SaveUploadedFile của Gin (luôn gọi os.Chmod và ghi đè quyền), tôi viết một custom function chỉ tập trung vào việc lưu file.

Tạo một function đơn giản chỉ tập trung vào việc lưu file:

package utils

import (
    "io"
    "mime/multipart"
    "os"
    "path/filepath"
    "strings"
)

// SaveUploadedFile chỉ lưu file, không can thiệp vào thư mục cha
func SaveUploadedFile(file *multipart.FileHeader, dst string) error {
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()

    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, src)
    return err
}

// FindFirstMissingDir tìm thư mục đầu tiên không tồn tại trong path
// Trả về empty string nếu tất cả thư mục đều tồn tại
func FindFirstMissingDir(fullPath string) string {
    dir := filepath.Dir(fullPath) // Lấy thư mục chứa file

    // Duyệt ngược từ thư mục cuối về root
    for dir != "" && dir != "." && dir != "/" {
        if _, err := os.Stat(dir); os.IsNotExist(err) {
            // Thư mục này không tồn tại, tiếp tục kiểm tra parent
            dir = filepath.Dir(dir)
        } else {
            // Thư mục này tồn tại, trả về thư mục con đầu tiên không tồn tại
            break
        }
    }

    // Bây giờ dir là thư mục cuối cùng tồn tại
    // Tìm thư mục con đầu tiên không tồn tại
    originalDir := filepath.Dir(fullPath)

    // Nếu dir == originalDir nghĩa là tất cả đều tồn tại
    if dir == originalDir {
        return ""
    }

    // Tìm phần path nằm giữa dir và originalDir
    rel, _ := filepath.Rel(dir, originalDir)
    firstMissing := filepath.Join(dir, strings.Split(rel, string(filepath.Separator))[0])

    return firstMissing
}

Sử dụng trong handler:

import (
    "os"
    "path/filepath"
    "yourproject/utils"
)

func uploadHandler(c *gin.Context) {
    var body UploadRequest
    if err := c.ShouldBind(&body); err != nil {
        c.AbortWithStatusJSON(400, gin.H{"message": err.Error()})
        return
    }

    fullPath := "/var/www/uploads/user123/index.html"

    // Tự quản lý thư mục - chỉ tạo nếu chưa tồn tại
    missingPath := utils.FindFirstMissingDir(fullPath)

    if missingPath != "" {
        err := os.MkdirAll(filepath.Dir(fullPath), 0755)
        if err != nil {
            c.AbortWithStatusJSON(400, gin.H{"message": err.Error()})
            return
        }
    }

    // Chỉ lưu file, không động vào quyền thư mục
    if err := utils.SaveUploadedFile(body.File, fullPath); err != nil {
        c.AbortWithStatusJSON(400, gin.H{"message": err.Error()})
        return
    }

    c.JSON(200, gin.H{"message": "Upload thành công"})
}

Ưu điểm:

  • Separation of concerns: Tách biệt việc quản lý thư mục và lưu file
  • Không overhead: Không gọi os.Chmod không cần thiết
  • Dễ test: Function đơn giản, dễ viết unit test
  • Tránh side effects: Không làm thay đổi quyền của thư mục đã tồn tại

Nhược điểm:

  • Phải maintain thêm code

Đánh giá bài viết

Thích thì like
Gin Framework: Lỗi thay đổi quyền thư mục khi upload file
0/5 0 votes

Bình luận

Hiển thị bình luận Facebook