From df8f7e89220aa1859a23f5a82aa3c02a3b29a025 Mon Sep 17 00:00:00 2001 From: loveuer Date: Fri, 11 Sep 2026 01:44:21 -0700 Subject: [PATCH] feat: require advanced permission for large uploads --- README.md | 2 +- app.go | 5 +++++ app_test.go | 33 +++++++++++++++++++++++++++++++++ frontend/src/App.jsx | 14 +++++++++----- 4 files changed, 48 insertions(+), 6 deletions(-) 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
{message}
@@ -116,6 +118,7 @@ function UploadPanel() { const [error, setError] = useState('') const [result, setResult] = useState(null) const allowed = user.canUpload || user.canAdvancedUpload + const fileTooLarge = Boolean(file && !user.canAdvancedUpload && file.size > basicUploadMaxSize) const generatePickupCode = () => { const value = new Uint32Array(1) crypto.getRandomValues(value) @@ -123,6 +126,7 @@ function UploadPanel() { } const submit = async (event) => { event.preventDefault(); if (!file) return + if (fileTooLarge) return setError('超过 10 MB 的文件需要高级上传权限') setBusy(true); setError('') const data = new FormData(); data.set('file', file); data.set('retention', retention) data.set('downloads', downloads === 'custom' ? customDownloads : downloads); data.set('pickupCode', pickupCode) @@ -133,9 +137,9 @@ function UploadPanel() { if (!allowed) return

当前账号没有上传权限

请联系用户管理员调整权限。

if (result) return { setResult(null); setPickupCode('') }} /> return
-

上传新文件

{user.canAdvancedUpload ? '高级上传 · 不限有效文件数量' : '普通上传 · 最多 10 个有效文件'}

+

上传新文件

{user.canAdvancedUpload ? '高级上传 · 不限有效文件数量和文件大小' : '普通上传 · 最多 10 个有效文件 · 单文件最大 10 MB'}

@@ -144,8 +148,8 @@ function UploadPanel() { {downloads === 'custom' &&
setCustomDownloads(e.target.value)} />
}
setPickupCode(e.target.value.replace(/\D/g, ''))} />

{pickupCode ? '已启用取件码保护' : '强烈建议生成取件码;仅在确认文件无需保护时留空。'}

- {error &&

{error}

} - + {(error || fileTooLarge) &&

{error || '超过 10 MB 的文件需要高级上传权限'}

} +
} @@ -177,7 +181,7 @@ function UserModal({ value, onClose, onSaved }) { catch (err) { setError(err.message) } } const checkbox = (key, title, hint, disabled = false) => - return

{value ? '编辑用户' : '创建用户'}

setForm({ ...form, username: e.target.value })} />

只能分配你当前拥有的权限。

{checkbox('canManageUsers', '用户管理', '创建、编辑、重置或删除用户')}{checkbox('canManageFiles', '文件管理', '查看并删除所有用户上传的文件', !actor.canManageFiles)}{checkbox('canUpload', '普通上传', '最多保留 10 个有效文件', !canGrantUpload)}{checkbox('canAdvancedUpload', '高级上传', '涵盖普通上传,并允许永久、无限下载', !actor.canAdvancedUpload)}
{error &&

{error}

}
+ return

{value ? '编辑用户' : '创建用户'}

setForm({ ...form, username: e.target.value })} />

只能分配你当前拥有的权限。

{checkbox('canManageUsers', '用户管理', '创建、编辑、重置或删除用户')}{checkbox('canManageFiles', '文件管理', '查看并删除所有用户上传的文件', !actor.canManageFiles)}{checkbox('canUpload', '普通上传', '最多保留 10 个有效文件,单文件最大 10 MB', !canGrantUpload)}{checkbox('canAdvancedUpload', '高级上传', '涵盖普通上传,并允许大文件、永久、无限下载', !actor.canAdvancedUpload)}
{error &&

{error}

}
} function UsersPanel({ notify }) {