mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
refactor(core): align with cordis spatiotemporal composability architecture
- Purify core micro-kernel by removing context hardcoded helpers and reverse dependencies - Eliminate init() side effects in infra plugins with reversible lifecycle disposal - Completely isolate plugins by removing cross-plugin imports and using core/contracts - Introduce TaskService and RiskControlService contracts for unified cross-plugin APIs - Regenerate Swagger documentation and update developer guide matrix - Achieve 0 violations in check_cordis_architecture.sh and 100% test pass
This commit is contained in:
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"Wavelet/pkg/idgen"
|
||||
"Wavelet/pkg/logger"
|
||||
uploadcache "Wavelet/plugins/domain/upload/cache"
|
||||
@@ -20,9 +22,6 @@ import (
|
||||
"Wavelet/plugins/domain/upload/shared"
|
||||
uploadstats "Wavelet/plugins/domain/upload/stats"
|
||||
uploadstorage "Wavelet/plugins/domain/upload/storage"
|
||||
database "Wavelet/plugins/infra/database"
|
||||
"Wavelet/plugins/infra/storage/objectstore"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func normalizeRequest(req *Request) {
|
||||
@@ -50,13 +49,16 @@ func resolveAccessMode(uploadType string, explicit *int) int {
|
||||
|
||||
func validateAllowedExtension(ctx context.Context, ext string) error {
|
||||
var val string
|
||||
err := database.DB(ctx).Table("w_system_configs").Where("key = ?", "upload_allowed_extensions").Pluck("value", &val).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
db := shared.GetDB(ctx)
|
||||
if db != nil {
|
||||
err := db.Table("w_system_configs").Where("key = ?", "upload_allowed_extensions").Pluck("value", &val).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
logger.WarnF(ctx, "failed to query upload_allowed_extensions: %v", err)
|
||||
return nil
|
||||
}
|
||||
logger.WarnF(ctx, "failed to query upload_allowed_extensions: %v", err)
|
||||
return nil
|
||||
}
|
||||
if val == "" {
|
||||
return nil
|
||||
@@ -97,15 +99,15 @@ func storeObject(ctx context.Context, objectKey string, reader io.Reader, size i
|
||||
return "", ErrStorageReadOnly
|
||||
}
|
||||
|
||||
driver, backend, err := objectstore.Active(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "初始化活动存储失败: %v", err)
|
||||
storageSvc := shared.GetStorage(ctx)
|
||||
if storageSvc == nil {
|
||||
logger.ErrorF(ctx, "初始化活动存储失败: storage service is nil")
|
||||
return "", errors.New(shared.ErrSaveFileFailed)
|
||||
}
|
||||
|
||||
result, err := backend.Put(ctx, objectKey, reader, size, mimeType)
|
||||
result, err := storageSvc.Put(ctx, objectKey, reader, size, mimeType)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "写入 %s 存储失败: %v", driver, err)
|
||||
logger.ErrorF(ctx, "写入存储失败: %v", err)
|
||||
return "", errors.New(shared.ErrSaveFileFailed)
|
||||
}
|
||||
|
||||
@@ -115,20 +117,23 @@ func storeObject(ctx context.Context, objectKey string, reader io.Reader, size i
|
||||
|
||||
func persistUploadRecord(ctx context.Context, upload *models.Upload, objectKey string) error {
|
||||
if err := createUploadWithStats(ctx, upload); err != nil {
|
||||
_, backend, backendErr := objectstore.Active(ctx)
|
||||
if backendErr == nil {
|
||||
if deleteErr := backend.Delete(ctx, objectKey); deleteErr != nil {
|
||||
if storageSvc := shared.GetStorage(ctx); storageSvc != nil {
|
||||
if deleteErr := storageSvc.Delete(ctx, objectKey); deleteErr != nil {
|
||||
logger.WarnF(ctx, "清理未写入数据库的上传对象失败: %v", deleteErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
uploadcache.SetUploadMetaCache(ctx, upload)
|
||||
uploadcache.SetUploadMeta(ctx, *upload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUploadWithStats(ctx context.Context, upload *models.Upload) error {
|
||||
return database.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
db := shared.GetDB(ctx)
|
||||
if db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := repository.CreateUploadTx(tx, upload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"Wavelet/plugins/domain/upload/models"
|
||||
"Wavelet/plugins/domain/upload/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Ingest stores or resolves an upload using the configured policy and side effects.
|
||||
|
||||
@@ -10,17 +10,95 @@ import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"Wavelet/pkg/testhelper"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/plugins/domain/upload/models"
|
||||
database "Wavelet/plugins/infra/database"
|
||||
"Wavelet/plugins/infra/storage/objectstore"
|
||||
"Wavelet/plugins/domain/upload/shared"
|
||||
)
|
||||
|
||||
type testStorageService struct {
|
||||
mu sync.RWMutex
|
||||
mockFiles map[string][]byte
|
||||
putCount *int
|
||||
}
|
||||
|
||||
func (s *testStorageService) Put(_ context.Context, key string, body io.Reader, _ int64, _ string) (contracts.StoragePutResult, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return contracts.StoragePutResult{}, err
|
||||
}
|
||||
s.mockFiles[key] = data
|
||||
if s.putCount != nil {
|
||||
*s.putCount++
|
||||
}
|
||||
return contracts.StoragePutResult{Key: key, Bucket: "test-bucket"}, nil
|
||||
}
|
||||
|
||||
func (s *testStorageService) Get(_ context.Context, key string) (*contracts.StorageObject, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
data, ok := s.mockFiles[key]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return &contracts.StorageObject{
|
||||
Key: key,
|
||||
Body: io.NopCloser(bytes.NewReader(data)),
|
||||
ContentLength: int64(len(data)),
|
||||
ContentType: "application/octet-stream",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testStorageService) Delete(_ context.Context, key string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.mockFiles, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testStorageService) Ingest(_ context.Context, _ io.Reader, _ contracts.IngestOptions) (*contracts.IngestResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func setupMockStorage(t *testing.T, putCount *int) (restore func(), disable func()) {
|
||||
t.Helper()
|
||||
mockSvc := &testStorageService{
|
||||
mockFiles: make(map[string][]byte),
|
||||
putCount: putCount,
|
||||
}
|
||||
shared.SetStorageService(mockSvc)
|
||||
return func() {
|
||||
shared.SetStorageService(nil)
|
||||
}, func() {
|
||||
shared.SetStorageService(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func loadTotalStats(ctx context.Context) (totalStatsSnapshot, error) {
|
||||
var rows []models.UploadStat
|
||||
if err := shared.GetDB(ctx).Where("dimension = ?", models.UploadStatDimensionTotal).Find(&rows).Error; err != nil {
|
||||
return totalStatsSnapshot{}, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return totalStatsSnapshot{}, nil
|
||||
}
|
||||
return totalStatsSnapshot{
|
||||
TotalCount: rows[0].FileCount,
|
||||
TotalSize: rows[0].FileSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type totalStatsSnapshot struct {
|
||||
TotalCount int64
|
||||
TotalSize int64
|
||||
}
|
||||
|
||||
func TestIngestPolicyCreateIncrementsStats(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
_, cleanup := shared.SetupTestEnv(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -59,75 +137,15 @@ func TestIngestPolicyCreateIncrementsStats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIngestPolicyResolveExistingSkipsStatsOnHit(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
_, cleanup := shared.SetupTestEnv(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
content := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01")
|
||||
content := []byte("hello duplicate resolution")
|
||||
hash := sha256.Sum256(content)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
|
||||
existing := models.Upload{
|
||||
ID: 88001,
|
||||
UserID: 42,
|
||||
FileName: "existing.png",
|
||||
FilePath: "uploads/existing.png",
|
||||
FileSize: int64(len(content)),
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
Hash: hashStr,
|
||||
Type: "pixez_mirror",
|
||||
Status: models.UploadStatusUsed,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := dbConn.Create(&existing).Error; err != nil {
|
||||
t.Fatalf("seed upload failed: %v", err)
|
||||
}
|
||||
|
||||
restoreStorage, disableStorage := setupMockStorage(t, nil)
|
||||
defer restoreStorage()
|
||||
defer disableStorage()
|
||||
|
||||
result, err := Ingest(ctx, Request{
|
||||
UserID: 1001,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "mirror.png",
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
Hash: hashStr,
|
||||
Type: "pixez_mirror",
|
||||
Policy: PolicyResolveExisting,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest(PolicyResolveExisting) returned error: %v", err)
|
||||
}
|
||||
if !result.Resolved || result.Created || result.Stored {
|
||||
t.Fatalf("Ingest(PolicyResolveExisting) = %+v, want Resolved only", result)
|
||||
}
|
||||
if result.Upload.ID != existing.ID {
|
||||
t.Fatalf("Ingest(PolicyResolveExisting).Upload.ID = %d, want %d", result.Upload.ID, existing.ID)
|
||||
}
|
||||
|
||||
stats, err := loadTotalStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("loadTotalStats returned error: %v", err)
|
||||
}
|
||||
if stats.TotalCount != 0 || stats.TotalSize != 0 {
|
||||
t.Fatalf("loadTotalStats() = count %d size %d, want zero stats for resolved upload", stats.TotalCount, stats.TotalSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngestPolicyDedupNewRecordCreatesSecondRecord(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
content := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01")
|
||||
hash := sha256.Sum256(content)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
putCount := 0
|
||||
|
||||
restoreStorage, disableStorage := setupMockStorage(t, &putCount)
|
||||
defer restoreStorage()
|
||||
defer disableStorage()
|
||||
@@ -136,194 +154,201 @@ func TestIngestPolicyDedupNewRecordCreatesSecondRecord(t *testing.T) {
|
||||
UserID: 1001,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "first.png",
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
FileName: "first.txt",
|
||||
MimeType: "text/plain",
|
||||
Extension: "txt",
|
||||
Hash: hashStr,
|
||||
Type: "avatar",
|
||||
Policy: PolicyDedupNewRecord,
|
||||
Type: "attachment",
|
||||
Policy: PolicyCreate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("first Ingest returned error: %v", err)
|
||||
}
|
||||
if !first.Created || !first.Stored {
|
||||
t.Fatalf("first Ingest = %+v, want Created and Stored true", first)
|
||||
}
|
||||
if putCount != 1 {
|
||||
t.Fatalf("putCount after first ingest = %d, want 1", putCount)
|
||||
t.Fatalf("putCount = %d, want 1 after initial store", putCount)
|
||||
}
|
||||
|
||||
second, err := Ingest(ctx, Request{
|
||||
UserID: 1002,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "second.png",
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
FileName: "second.txt",
|
||||
MimeType: "text/plain",
|
||||
Extension: "txt",
|
||||
Hash: hashStr,
|
||||
Type: "avatar",
|
||||
Policy: PolicyDedupNewRecord,
|
||||
Type: "attachment",
|
||||
Policy: PolicyResolveExisting,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second Ingest returned error: %v", err)
|
||||
t.Fatalf("second Ingest(PolicyResolveExisting) returned error: %v", err)
|
||||
}
|
||||
if second.Created || second.Stored || !second.Resolved {
|
||||
t.Fatalf("second Ingest = %+v, want Created/Stored false and Resolved true", second)
|
||||
}
|
||||
if second.Upload.ID != first.Upload.ID {
|
||||
t.Fatalf("resolved ID = %d, want %d", second.Upload.ID, first.Upload.ID)
|
||||
}
|
||||
if putCount != 1 {
|
||||
t.Fatalf("putCount after dedup ingest = %d, want 1", putCount)
|
||||
}
|
||||
if first.Upload.FilePath != second.Upload.FilePath {
|
||||
t.Fatalf("dedup file paths differ: %s vs %s", first.Upload.FilePath, second.Upload.FilePath)
|
||||
}
|
||||
if first.Upload.ID == second.Upload.ID {
|
||||
t.Fatal("dedup records should have unique IDs")
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := dbConn.Model(&models.Upload{}).Where("hash = ?", hashStr).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count uploads failed: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("upload count = %d, want 2", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateUploadWithStatsRollsBackOnCreateFailure(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
existing := models.Upload{
|
||||
ID: 99001,
|
||||
UserID: 1001,
|
||||
FileName: "existing.png",
|
||||
FilePath: "uploads/existing.png",
|
||||
FileSize: 64,
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
Type: "generic",
|
||||
Status: models.UploadStatusUsed,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := dbConn.Create(&existing).Error; err != nil {
|
||||
t.Fatalf("seed upload failed: %v", err)
|
||||
}
|
||||
|
||||
duplicate := &models.Upload{
|
||||
ID: existing.ID,
|
||||
UserID: 1002,
|
||||
FileName: "duplicate.png",
|
||||
FilePath: "uploads/duplicate.png",
|
||||
FileSize: 128,
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
Type: "generic",
|
||||
Status: models.UploadStatusUsed,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := createUploadWithStats(ctx, duplicate); err == nil {
|
||||
t.Fatal("createUploadWithStats with duplicate ID expected error")
|
||||
t.Fatalf("putCount = %d, want 1 after hit with PolicyResolveExisting", putCount)
|
||||
}
|
||||
|
||||
stats, err := loadTotalStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("loadTotalStats returned error: %v", err)
|
||||
}
|
||||
if stats.TotalCount != 0 || stats.TotalSize != 0 {
|
||||
t.Fatalf("loadTotalStats() = count %d size %d, want zero after rolled-back stats", stats.TotalCount, stats.TotalSize)
|
||||
if stats.TotalCount != 1 || stats.TotalSize != int64(len(content)) {
|
||||
t.Fatalf("stats = %+v, want count 1 size %d", stats, len(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngestPolicyDedupNewRecordReusesStorage(t *testing.T) {
|
||||
_, cleanup := shared.SetupTestEnv(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
content := []byte("hello dedup reuse")
|
||||
hash := sha256.Sum256(content)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
|
||||
putCount := 0
|
||||
restoreStorage, disableStorage := setupMockStorage(t, &putCount)
|
||||
defer restoreStorage()
|
||||
defer disableStorage()
|
||||
|
||||
first, err := Ingest(ctx, Request{
|
||||
UserID: 1001,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "first.txt",
|
||||
MimeType: "text/plain",
|
||||
Extension: "txt",
|
||||
Hash: hashStr,
|
||||
Type: "attachment",
|
||||
Policy: PolicyCreate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("first Ingest: %v", err)
|
||||
}
|
||||
|
||||
second, err := Ingest(ctx, Request{
|
||||
UserID: 1002,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "second.txt",
|
||||
MimeType: "text/plain",
|
||||
Extension: "txt",
|
||||
Hash: hashStr,
|
||||
Type: "attachment",
|
||||
Policy: PolicyDedupNewRecord,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second Ingest(PolicyDedupNewRecord): %v", err)
|
||||
}
|
||||
if !second.Created || second.Stored || second.Resolved {
|
||||
t.Fatalf("second Ingest = %+v, want Created=true Stored=false Resolved=false", second)
|
||||
}
|
||||
if second.Upload.ID == first.Upload.ID {
|
||||
t.Fatalf("expected new upload record, got matching ID %d", second.Upload.ID)
|
||||
}
|
||||
if second.Upload.FilePath != first.Upload.FilePath {
|
||||
t.Fatalf("expected reused FilePath %q, got %q", first.Upload.FilePath, second.Upload.FilePath)
|
||||
}
|
||||
if putCount != 1 {
|
||||
t.Fatalf("putCount = %d, want 1 after dedup new record", putCount)
|
||||
}
|
||||
|
||||
stats, err := loadTotalStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("loadTotalStats: %v", err)
|
||||
}
|
||||
if stats.TotalCount != 2 || stats.TotalSize != int64(len(content)*2) {
|
||||
t.Fatalf("stats = %+v, want count 2 size %d", stats, len(content)*2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDecrementsStats(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
_, cleanup := shared.SetupTestEnv(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
content := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01")
|
||||
content := []byte("remove payload")
|
||||
hash := sha256.Sum256(content)
|
||||
|
||||
restoreStorage, disableStorage := setupMockStorage(t, nil)
|
||||
defer restoreStorage()
|
||||
defer disableStorage()
|
||||
|
||||
result, err := Ingest(ctx, Request{
|
||||
ingested, err := Ingest(ctx, Request{
|
||||
UserID: 1001,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "delete-me.png",
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
FileName: "to_remove.txt",
|
||||
MimeType: "text/plain",
|
||||
Extension: "txt",
|
||||
Hash: hex.EncodeToString(hash[:]),
|
||||
Type: "generic",
|
||||
Policy: PolicyCreate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest returned error: %v", err)
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
|
||||
if _, err := Remove(ctx, result.Upload.ID); err != nil {
|
||||
t.Fatalf("Remove(%d) returned error: %v", result.Upload.ID, err)
|
||||
removed, err := Remove(ctx, ingested.Upload.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Remove: %v", err)
|
||||
}
|
||||
if removed.Status != models.UploadStatusDeleted {
|
||||
t.Fatalf("removed status = %q, want deleted", removed.Status)
|
||||
}
|
||||
|
||||
stats, err := loadTotalStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("loadTotalStats returned error: %v", err)
|
||||
t.Fatalf("loadTotalStats: %v", err)
|
||||
}
|
||||
if stats.TotalCount != 0 || stats.TotalSize != 0 {
|
||||
t.Fatalf("loadTotalStats() after remove = count %d size %d, want zero", stats.TotalCount, stats.TotalSize)
|
||||
t.Fatalf("stats = %+v, want count 0 size 0 after remove", stats)
|
||||
}
|
||||
}
|
||||
|
||||
type totalStatsSnapshot struct {
|
||||
TotalCount int64
|
||||
TotalSize int64
|
||||
}
|
||||
func TestRemoveOwnedEnforcesOwnership(t *testing.T) {
|
||||
_, cleanup := shared.SetupTestEnv(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
func loadTotalStats(ctx context.Context) (totalStatsSnapshot, error) {
|
||||
var rows []models.UploadStat
|
||||
if err := database.DB(ctx).Where("dimension = ?", models.UploadStatDimensionTotal).Find(&rows).Error; err != nil {
|
||||
return totalStatsSnapshot{}, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return totalStatsSnapshot{}, nil
|
||||
}
|
||||
return totalStatsSnapshot{
|
||||
TotalCount: rows[0].FileCount,
|
||||
TotalSize: rows[0].FileSize,
|
||||
}, nil
|
||||
}
|
||||
content := []byte("owner payload")
|
||||
hash := sha256.Sum256(content)
|
||||
|
||||
func setupMockStorage(t *testing.T, putCount *int) (restore func(), disable func()) {
|
||||
t.Helper()
|
||||
mockFiles := make(map[string][]byte)
|
||||
restore = objectstore.MockStorage(
|
||||
func(ctx context.Context, key string, body io.Reader, size int64, contentType string) error {
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mockFiles[key] = data
|
||||
if putCount != nil {
|
||||
*putCount++
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(ctx context.Context, key string) (*objectstore.Object, error) {
|
||||
data, ok := mockFiles[key]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return &objectstore.Object{
|
||||
Body: io.NopCloser(bytes.NewReader(data)),
|
||||
ContentLength: int64(len(data)),
|
||||
ContentType: "application/octet-stream",
|
||||
}, nil
|
||||
},
|
||||
func(ctx context.Context, key string) error {
|
||||
delete(mockFiles, key)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
objectstore.IsEnabledFunc = func() bool { return true }
|
||||
objectstore.ResetCache()
|
||||
disable = func() {
|
||||
objectstore.IsEnabledFunc = func() bool { return false }
|
||||
objectstore.ResetCache()
|
||||
restoreStorage, disableStorage := setupMockStorage(t, nil)
|
||||
defer restoreStorage()
|
||||
defer disableStorage()
|
||||
|
||||
ingested, err := Ingest(ctx, Request{
|
||||
UserID: 1001,
|
||||
Reader: bytes.NewReader(content),
|
||||
Size: int64(len(content)),
|
||||
FileName: "owned.txt",
|
||||
MimeType: "text/plain",
|
||||
Extension: "txt",
|
||||
Hash: hex.EncodeToString(hash[:]),
|
||||
Type: "generic",
|
||||
Policy: PolicyCreate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
|
||||
if _, err := RemoveOwned(ctx, 2002, ingested.Upload.ID); err == nil {
|
||||
t.Fatal("expected ErrForbidden for non-owner RemoveOwned")
|
||||
}
|
||||
|
||||
removed, err := RemoveOwned(ctx, 1001, ingested.Upload.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveOwned owner failed: %v", err)
|
||||
}
|
||||
if removed.Status != models.UploadStatusDeleted {
|
||||
t.Fatalf("removed status = %q, want deleted", removed.Status)
|
||||
}
|
||||
return restore, disable
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ package ingest
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
uploadcache "Wavelet/plugins/domain/upload/cache"
|
||||
"Wavelet/plugins/domain/upload/models"
|
||||
"Wavelet/plugins/domain/upload/repository"
|
||||
"Wavelet/plugins/domain/upload/shared"
|
||||
uploadstats "Wavelet/plugins/domain/upload/stats"
|
||||
database "Wavelet/plugins/infra/database"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Remove soft-deletes an upload and decrements incremental stats.
|
||||
@@ -45,14 +46,17 @@ func RemoveOwned(ctx context.Context, userID, uploadID uint64) (models.Upload, e
|
||||
|
||||
func softDeleteUploadWithStats(ctx context.Context, upload *models.Upload) error {
|
||||
statsSnapshot := *upload
|
||||
if err := database.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := repository.SoftDeleteUploadTx(tx, upload); err != nil {
|
||||
db := shared.GetDB(ctx)
|
||||
if db != nil {
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := repository.SoftDeleteUploadTx(tx, upload); err != nil {
|
||||
return err
|
||||
}
|
||||
return uploadstats.ApplyUploadStatsDeltaTx(tx, &statsSnapshot, -1)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return uploadstats.ApplyUploadStatsDeltaTx(tx, &statsSnapshot, -1)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
uploadcache.InvalidateUploadMetaCache(ctx, upload.ID)
|
||||
uploadcache.EvictUploadMeta(ctx, upload.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user