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 đề:
- Quyền mặc định thay đổi: Từ
0755
(hoặc không có default rõ ràng) sang0750
- Luôn gọi
os.Chmod
: Dù thư mục đã tồn tại hay chưa, hàm vẫn gọios.Chmod
và ghi đè quyền cũ - 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
Ủng hộ Chung Nguyễn Blog
Chung Nguyễn Blog sử dụng FlashPanel - Dịch vụ quản trị máy chủ chuyên nghiệp để quản lý VPS
#FlashPanel là dịch vụ cloud panel trên nền tảng web hỗ trợ khách hàng:
- * Quản lý máy chủ số lượng nhiều
- * Không có kinh nghiệm quản lý máy chủ
- * Thích sử dụng giao diện web đơn giản, trực quan hơn terminal
- * Quá nhàm chán với việc ghi nhớ và lặp lại việc gõ các câu lệnh
- * Muốn tự động hóa mọi thao tác
- * Muốn tiết kiệm thời gian quản trị máy chủ
- * Muốn tiết kiệm tiền bạc, nhân lực quản trị máy chủ 👉 https://flashpanel.io
Các bài viết trên website thường xuyên được đăng tải và cập nhật trên trang Facebook Chung Nguyễn Blog hãy tặng cho Chung một LIKE nhé! Mãi yêu các bạn!
813 👍Đánh giá bài viết

Bình luận