diff --git a/README.md b/README.md index 25e24dc..2b0f0ab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ufile -一个单二进制运行的轻量文件快传服务。支持多用户权限、限时保存、限次访问、6 位取件码,以及无需登录的下载和预览链接。文件管理员可以查看并删除全部当前有效文件。 +一个单二进制运行的轻量文件快传服务。支持多用户权限、限时保存、限次访问、6 位取件码,以及无需登录的下载和预览链接。普通上传单文件最大 10 MB,高级上传不受此限制。文件管理员可以查看并删除全部当前有效文件。 ## 构建 diff --git a/app.go b/app.go index 0682256..3402c95 100644 --- a/app.go +++ b/app.go @@ -27,6 +27,7 @@ import ( ) const sessionCookie = "ufile_session" +const basicUploadMaxSize = 10 * 1024 * 1024 var usernamePattern = regexp.MustCompile(`^[A-Za-z0-9_.-]{3,32}$`) var pickupCodePattern = regexp.MustCompile(`^[0-9]{6}$`) @@ -397,6 +398,10 @@ func (a *App) uploadFile(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"}) return } + if header.Size > basicUploadMaxSize && !user.CanAdvancedUpload { + c.JSON(http.StatusForbidden, gin.H{"error": "超过 10 MB 的文件需要高级上传权限"}) + return + } a.uploadMu.Lock() defer a.uploadMu.Unlock() diff --git a/app_test.go b/app_test.go index cd54b0d..158cea7 100644 --- a/app_test.go +++ b/app_test.go @@ -401,6 +401,39 @@ func TestBasicUploadLimitAndAdvancedInheritance(t *testing.T) { } } +func TestLargeFileRequiresAdvancedUpload(t *testing.T) { + app, _ := testApp(t) + passwordHash, _ := bcrypt.GenerateFromPassword([]byte("password-123"), bcrypt.DefaultCost) + basic := User{Username: "basic-size", PasswordHash: string(passwordHash), CanUpload: true} + advanced := User{Username: "advanced-size", PasswordHash: string(passwordHash), CanAdvancedUpload: true} + for _, user := range []*User{&basic, &advanced} { + if err := app.db.Create(user).Error; err != nil { + t.Fatal(err) + } + } + router := app.router() + fields := map[string]string{"retention": "1h", "downloads": "1"} + + basicCookie := loginAs(t, router, basic.Username, "password-123") + body, contentType := uploadBody(t, fields, "allowed.bin", make([]byte, 10*1024*1024)) + response := request(t, router, http.MethodPost, "/api/files", body, contentType, basicCookie) + if response.Code != http.StatusCreated { + t.Fatalf("10 MB basic upload status = %d, body = %s", response.Code, response.Body.String()) + } + body, contentType = uploadBody(t, fields, "blocked.bin", make([]byte, 10*1024*1024+1)) + response = request(t, router, http.MethodPost, "/api/files", body, contentType, basicCookie) + if response.Code != http.StatusForbidden || !strings.Contains(response.Body.String(), "高级上传") { + t.Fatalf("large basic upload status = %d, body = %s", response.Code, response.Body.String()) + } + + advancedCookie := loginAs(t, router, advanced.Username, "password-123") + body, contentType = uploadBody(t, fields, "allowed-large.bin", make([]byte, 10*1024*1024+1)) + response = request(t, router, http.MethodPost, "/api/files", body, contentType, advancedCookie) + if response.Code != http.StatusCreated { + t.Fatalf("large advanced upload status = %d, body = %s", response.Code, response.Body.String()) + } +} + func TestDeleteUserRemovesOwnedFilesAndProtectsIDOne(t *testing.T) { app, password := testApp(t) var admin User diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 90d7e9b..0afe93a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,8 @@ const formatDate = (value) => value ? new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).format(new Date(value)) : '永久' +const basicUploadMaxSize = 10 * 1024 * 1024 + function Toast({ message, onClose }) { useEffect(() => { const timer = setTimeout(onClose, 3600); return () => clearTimeout(timer) }, [message, onClose]) return
请联系用户管理员调整权限。