mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
refactor(repo): consolidate openflare-server to root and move subprojects to internal/apps
- Merge all files inside openflare-server to the repository root directory. - Relocate agent, relay, and flared subprojects from internal/ to internal/apps/. - Combine docker-compose files and update build context paths to root. - Update GitHub workflows and Dockerfiles to refer to new directories and package names. - Rewrite Go package imports across all files. - Resolve database renew test race condition and clean up docs.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package stats maintains incremental upload statistics and aggregations.
|
||||
package stats
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/util"
|
||||
)
|
||||
|
||||
const (
|
||||
catImage = "图片"
|
||||
catVideo = "视频"
|
||||
catAudio = "音频"
|
||||
catDocument = "文档"
|
||||
catArchive = "压缩包"
|
||||
catOther = "其他"
|
||||
)
|
||||
|
||||
// GetFileCategory classifies a file by mime type and extension.
|
||||
func GetFileCategory(mimeType, ext string) string {
|
||||
mimeType = strings.ToLower(mimeType)
|
||||
ext = strings.ToLower(ext)
|
||||
|
||||
if strings.HasPrefix(mimeType, "image/") || util.IsImageExtension(ext) {
|
||||
return catImage
|
||||
}
|
||||
if strings.HasPrefix(mimeType, "video/") {
|
||||
return catVideo
|
||||
}
|
||||
if strings.HasPrefix(mimeType, "audio/") {
|
||||
return catAudio
|
||||
}
|
||||
if util.IsArchiveExtension(ext) || strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") || strings.Contains(mimeType, "gzip") {
|
||||
return catArchive
|
||||
}
|
||||
if util.IsDocumentExtension(ext) || strings.HasPrefix(mimeType, "text/") || mimeType == "application/pdf" {
|
||||
return catDocument
|
||||
}
|
||||
return catOther
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// ApplyUploadStatsAdd increments incremental stats for a newly active upload record.
|
||||
func ApplyUploadStatsAdd(ctx context.Context, upload *model.Upload) error {
|
||||
return applyUploadStatsDelta(ctx, upload, 1)
|
||||
}
|
||||
|
||||
// ApplyUploadStatsRemove decrements incremental stats for a removed active upload record.
|
||||
func ApplyUploadStatsRemove(ctx context.Context, upload *model.Upload) error {
|
||||
return applyUploadStatsDelta(ctx, upload, -1)
|
||||
}
|
||||
|
||||
// RebuildUploadStats rebuilds all incremental stats from current upload records.
|
||||
func RebuildUploadStats(ctx context.Context) error {
|
||||
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("1 = 1").Delete(&model.UploadStat{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var uploads []model.Upload
|
||||
if err := tx.Where("status != ?", model.UploadStatusDeleted).Find(&uploads).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range uploads {
|
||||
if err := applyUploadStatsDeltaTx(tx, &uploads[i], 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func applyUploadStatsDelta(ctx context.Context, upload *model.Upload, sign int64) error {
|
||||
if upload == nil || !isActiveUploadStatus(upload.Status) {
|
||||
return nil
|
||||
}
|
||||
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return applyUploadStatsDeltaTx(tx, upload, sign)
|
||||
})
|
||||
}
|
||||
|
||||
func applyUploadStatsDeltaTx(tx *gorm.DB, upload *model.Upload, sign int64) error {
|
||||
if upload == nil || !isActiveUploadStatus(upload.Status) || sign == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
countDelta := sign
|
||||
sizeDelta := sign * upload.FileSize
|
||||
typeKey := upload.Type
|
||||
if typeKey == "" {
|
||||
typeKey = "generic"
|
||||
}
|
||||
|
||||
entries := []struct {
|
||||
dimension string
|
||||
key string
|
||||
}{
|
||||
{model.UploadStatDimensionTotal, ""},
|
||||
{model.UploadStatDimensionType, typeKey},
|
||||
{model.UploadStatDimensionCategory, GetFileCategory(upload.MimeType, upload.Extension)},
|
||||
{model.UploadStatDimensionTrend, upload.CreatedAt.Format("2006-01-02")},
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if err := upsertUploadStatDelta(tx, entry.dimension, entry.key, countDelta, sizeDelta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertUploadStatDelta(tx *gorm.DB, dimension, key string, countDelta, sizeDelta int64) error {
|
||||
return tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "dimension"},
|
||||
{Name: "stat_key"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"file_count": gorm.Expr(
|
||||
"CASE WHEN w_upload_stats.file_count + ? < 0 THEN 0 ELSE w_upload_stats.file_count + ? END",
|
||||
countDelta,
|
||||
countDelta,
|
||||
),
|
||||
"file_size": gorm.Expr(
|
||||
"CASE WHEN w_upload_stats.file_size + ? < 0 THEN 0 ELSE w_upload_stats.file_size + ? END",
|
||||
sizeDelta,
|
||||
sizeDelta,
|
||||
),
|
||||
"updated_at": time.Now(),
|
||||
}),
|
||||
}).Create(&model.UploadStat{
|
||||
Dimension: dimension,
|
||||
StatKey: key,
|
||||
FileCount: countDelta,
|
||||
FileSize: sizeDelta,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// RecordUploadStatsAdd logs and applies upload stats increment.
|
||||
func RecordUploadStatsAdd(ctx context.Context, upload *model.Upload) {
|
||||
if err := ApplyUploadStatsAdd(ctx, upload); err != nil {
|
||||
logger.WarnF(ctx, "increment upload stats failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordUploadStatsRemove logs and applies upload stats decrement.
|
||||
func RecordUploadStatsRemove(ctx context.Context, upload *model.Upload) {
|
||||
if err := ApplyUploadStatsRemove(ctx, upload); err != nil {
|
||||
logger.WarnF(ctx, "decrement upload stats failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func isActiveUploadStatus(status model.UploadStatus) bool {
|
||||
return status == model.UploadStatusPending || status == model.UploadStatusUsed
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
)
|
||||
|
||||
func TestApplyUploadStatsAddAndRemove(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
upload := &model.Upload{
|
||||
ID: 42001,
|
||||
FileSize: 128,
|
||||
MimeType: "image/png",
|
||||
Extension: "png",
|
||||
Type: "avatar",
|
||||
Status: model.UploadStatusUsed,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := ApplyUploadStatsAdd(ctx, upload); err != nil {
|
||||
t.Fatalf("ApplyUploadStatsAdd returned error: %v", err)
|
||||
}
|
||||
|
||||
stats, err := loadUploadStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("loadUploadStats returned error: %v", err)
|
||||
}
|
||||
if stats.TotalCount != 1 || stats.TotalSize != 128 {
|
||||
t.Fatalf("unexpected total stats: count=%d size=%d", stats.TotalCount, stats.TotalSize)
|
||||
}
|
||||
|
||||
if err := ApplyUploadStatsRemove(ctx, upload); err != nil {
|
||||
t.Fatalf("ApplyUploadStatsRemove returned error: %v", err)
|
||||
}
|
||||
|
||||
stats, err = loadUploadStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("loadUploadStats after remove returned error: %v", err)
|
||||
}
|
||||
if stats.TotalCount != 0 || stats.TotalSize != 0 {
|
||||
t.Fatalf("expected zeroed total stats, got count=%d size=%d", stats.TotalCount, stats.TotalSize)
|
||||
}
|
||||
}
|
||||
|
||||
type uploadStatsSnapshot struct {
|
||||
TotalCount int64
|
||||
TotalSize int64
|
||||
}
|
||||
|
||||
func loadUploadStats(ctx context.Context) (uploadStatsSnapshot, error) {
|
||||
var rows []model.UploadStat
|
||||
if err := db.DB(ctx).Where("dimension = ?", model.UploadStatDimensionTotal).Find(&rows).Error; err != nil {
|
||||
return uploadStatsSnapshot{}, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return uploadStatsSnapshot{}, nil
|
||||
}
|
||||
return uploadStatsSnapshot{
|
||||
TotalCount: rows[0].FileCount,
|
||||
TotalSize: rows[0].FileSize,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user