mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 06:36:38 +08:00
fix(pages): 收紧部署包与 Agent 同步边界
完成 V2 Phase 0 安全与一致性前置:统一真实归档限额、流式拉取、候选裁剪、保留上传删除语义及 Pages 路由引用锁。
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -96,6 +97,7 @@ func ListFiles(c *gin.Context) {
|
||||
// @Success 200 {object} response.Any "删除成功"
|
||||
// @Failure 403 {object} response.Any "无权操作"
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Failure 409 {object} response.Any "系统保留类型或存储只读"
|
||||
// @Router /api/v1/admin/uploads/{id} [delete]
|
||||
func DeleteFile(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
@@ -111,6 +113,10 @@ func DeleteFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if _, err := softDeleteUpload(ctx, uploadID); err != nil {
|
||||
if errors.Is(err, ingest.ErrReservedUploadType) {
|
||||
response.AbortConflict(c, shared.ErrReservedUploadType)
|
||||
return
|
||||
}
|
||||
if isRecordNotFound(err) {
|
||||
response.AbortNotFound(c, "文件记录未找到")
|
||||
return
|
||||
@@ -216,6 +222,7 @@ func ListMyFiles(c *gin.Context) {
|
||||
// @Success 200 {object} response.Any "删除成功"
|
||||
// @Failure 403 {object} response.Any "无权操作"
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Failure 409 {object} response.Any "系统保留类型或存储只读"
|
||||
// @Router /api/v1/upload/{id} [delete]
|
||||
func DeleteMyFile(c *gin.Context) {
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
@@ -232,11 +239,15 @@ func DeleteMyFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if _, err := softDeleteOwnedUpload(ctx, currUser.ID, uploadID); err != nil {
|
||||
if errors.Is(err, ingest.ErrReservedUploadType) {
|
||||
response.AbortConflict(c, shared.ErrReservedUploadType)
|
||||
return
|
||||
}
|
||||
if isRecordNotFound(err) {
|
||||
response.AbortNotFound(c, "文件记录未找到")
|
||||
return
|
||||
}
|
||||
if err == ingest.ErrForbidden {
|
||||
if errors.Is(err, ingest.ErrForbidden) {
|
||||
response.AbortForbidden(c, "无权操作")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ type batchDownloadRequest struct {
|
||||
// @Success 200 {object} response.Any{data=model.Upload} "上传成功"
|
||||
// @Failure 400 {object} response.Any "请求参数错误或文件受限"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 409 {object} response.Any "系统保留类型或存储只读"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/upload [post]
|
||||
//
|
||||
@@ -107,6 +108,10 @@ func UploadFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
uploadType := c.DefaultPostForm("type", "generic")
|
||||
if uploadType == shared.ReservedPagesDeploymentType {
|
||||
response.AbortConflict(c, shared.ErrReservedUploadType)
|
||||
return
|
||||
}
|
||||
|
||||
accessMode, errMsg := resolveUploadAccessMode(c, uploadType)
|
||||
if errMsg != "" {
|
||||
|
||||
@@ -226,6 +226,42 @@ func TestUploadFile(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("upload rejects Pages reserved type", func(t *testing.T) {
|
||||
putCountBefore := putCount
|
||||
imgContent := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01")
|
||||
contentType, body := createMultipartRequest(t, "file", "pages.png", imgContent, map[string]string{
|
||||
"type": shared.ReservedPagesDeploymentType,
|
||||
})
|
||||
req, _ := http.NewRequest("POST", "/api/v1/upload", body)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("expected status 409, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp testResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal reserved type response: %v", err)
|
||||
}
|
||||
if resp.ErrorMsg != shared.ErrReservedUploadType {
|
||||
t.Fatalf("reserved type error = %q, want %q", resp.ErrorMsg, shared.ErrReservedUploadType)
|
||||
}
|
||||
if putCount != putCountBefore {
|
||||
t.Fatalf("reserved upload wrote storage object: put count %d -> %d", putCountBefore, putCount)
|
||||
}
|
||||
var count int64
|
||||
if err := dbConn.Model(&model.Upload{}).
|
||||
Where("type = ?", shared.ReservedPagesDeploymentType).
|
||||
Count(&count).Error; err != nil {
|
||||
t.Fatalf("count reserved uploads: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("reserved upload record count = %d, want 0", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("instant upload deduplication (秒传)", func(t *testing.T) {
|
||||
putCount = 0
|
||||
imgContent := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01")
|
||||
@@ -993,6 +1029,62 @@ func TestUserUploadManagement(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteReservedUploadType(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
authUser := &model.User{ID: 1001, Username: "reserved_owner"}
|
||||
router := setupTestRouter(authUser)
|
||||
reserved := model.Upload{
|
||||
ID: 4101,
|
||||
UserID: authUser.ID,
|
||||
FileName: "pages.zip",
|
||||
FilePath: "uploads/pages.zip",
|
||||
FileSize: 128,
|
||||
MimeType: "application/zip",
|
||||
Extension: "zip",
|
||||
Hash: "pages-reserved-hash",
|
||||
Type: shared.ReservedPagesDeploymentType,
|
||||
Status: model.UploadStatusUsed,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := dbConn.Create(&reserved).Error; err != nil {
|
||||
t.Fatalf("seed reserved upload: %v", err)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{name: "admin delete", path: "/api/v1/admin/uploads/4101"},
|
||||
{name: "owner delete", path: "/api/v1/upload/4101"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, tc.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("expected status 409, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp testResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal delete response: %v", err)
|
||||
}
|
||||
if resp.ErrorMsg != shared.ErrReservedUploadType {
|
||||
t.Fatalf("reserved delete error = %q, want %q", resp.ErrorMsg, shared.ErrReservedUploadType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var persisted model.Upload
|
||||
if err := dbConn.First(&persisted, reserved.ID).Error; err != nil {
|
||||
t.Fatalf("reload reserved upload: %v", err)
|
||||
}
|
||||
if persisted.Status != model.UploadStatusUsed {
|
||||
t.Fatalf("reserved upload status = %s, want used", persisted.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func configureLocalStorageRoot(t *testing.T, dbConn *gorm.DB, tempDir string) {
|
||||
var sc model.SystemConfig
|
||||
if err := dbConn.Where("key = ?", model.ConfigKeyStorageConfig).First(&sc).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user