基本能力编写完成
This commit is contained in:
178
internal/cache/file_cache.go
vendored
Normal file
178
internal/cache/file_cache.go
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/logger"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/models"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/storage"
|
||||
)
|
||||
|
||||
// FileCache 基于文件+DB的缓存实现
|
||||
type FileCache struct {
|
||||
store storage.Store
|
||||
statsDir string
|
||||
}
|
||||
|
||||
// NewFileCache 创建文件缓存
|
||||
func NewFileCache(store storage.Store, statsDir string) *FileCache {
|
||||
return &FileCache{
|
||||
store: store,
|
||||
statsDir: statsDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 获取缓存
|
||||
func (c *FileCache) Get(ctx context.Context, cacheKey string) (*models.StatsResult, error) {
|
||||
// 从DB查询缓存元数据
|
||||
cache, err := c.store.StatsCache().GetByCacheKey(ctx, cacheKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cache == nil {
|
||||
return nil, nil // 缓存不存在
|
||||
}
|
||||
|
||||
// 读取结果文件
|
||||
stats, err := c.loadStatsFromFile(cache.ResultPath)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("cache_key", cacheKey).Msg("failed to load stats from file")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新命中次数
|
||||
if err := c.store.StatsCache().UpdateHitCount(ctx, cache.ID); err != nil {
|
||||
logger.Logger.Warn().Err(err).Int64("cache_id", cache.ID).Msg("failed to update hit count")
|
||||
}
|
||||
|
||||
result := &models.StatsResult{
|
||||
CacheHit: true,
|
||||
CachedAt: &cache.CreatedAt,
|
||||
CommitHash: cache.CommitHash,
|
||||
Statistics: stats,
|
||||
}
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("cache_key", cacheKey).
|
||||
Int64("cache_id", cache.ID).
|
||||
Msg("cache hit")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Set 设置缓存
|
||||
func (c *FileCache) Set(ctx context.Context, repoID int64, branch string, constraint *models.StatsConstraint,
|
||||
commitHash string, stats *models.Statistics) error {
|
||||
|
||||
// 生成缓存键
|
||||
cacheKey := GenerateCacheKey(repoID, branch, constraint, commitHash)
|
||||
|
||||
// 保存统计结果到文件
|
||||
resultPath := filepath.Join(c.statsDir, cacheKey+".json.gz")
|
||||
if err := c.saveStatsToFile(stats, resultPath); err != nil {
|
||||
return fmt.Errorf("failed to save stats to file: %w", err)
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
fileInfo, err := os.Stat(resultPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat result file: %w", err)
|
||||
}
|
||||
|
||||
// 创建缓存记录
|
||||
cache := &models.StatsCache{
|
||||
RepoID: repoID,
|
||||
Branch: branch,
|
||||
ConstraintType: constraint.Type,
|
||||
ConstraintValue: SerializeConstraint(constraint),
|
||||
CommitHash: commitHash,
|
||||
ResultPath: resultPath,
|
||||
ResultSize: fileInfo.Size(),
|
||||
CacheKey: cacheKey,
|
||||
}
|
||||
|
||||
if err := c.store.StatsCache().Create(ctx, cache); err != nil {
|
||||
// 如果创建失败,删除已保存的文件
|
||||
os.Remove(resultPath)
|
||||
return fmt.Errorf("failed to create cache record: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("cache_key", cacheKey).
|
||||
Int64("cache_id", cache.ID).
|
||||
Int64("file_size", fileInfo.Size()).
|
||||
Msg("cache saved")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidateByRepoID 使指定仓库的所有缓存失效
|
||||
func (c *FileCache) InvalidateByRepoID(ctx context.Context, repoID int64) error {
|
||||
// 查询该仓库的所有缓存
|
||||
// 注意:这里简化实现,实际应该先查询再删除文件
|
||||
if err := c.store.StatsCache().DeleteByRepoID(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("failed to delete cache records: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.Info().Int64("repo_id", repoID).Msg("cache invalidated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveStatsToFile 保存统计结果到文件(gzip压缩)
|
||||
func (c *FileCache) saveStatsToFile(stats *models.Statistics, filePath string) error {
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 创建gzip writer
|
||||
gzipWriter := gzip.NewWriter(file)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
// 编码JSON
|
||||
encoder := json.NewEncoder(gzipWriter)
|
||||
if err := encoder.Encode(stats); err != nil {
|
||||
return fmt.Errorf("failed to encode stats: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadStatsFromFile 从文件加载统计结果
|
||||
func (c *FileCache) loadStatsFromFile(filePath string) (*models.Statistics, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 创建gzip reader
|
||||
gzipReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
// 解码JSON
|
||||
var stats models.Statistics
|
||||
decoder := json.NewDecoder(gzipReader)
|
||||
if err := decoder.Decode(&stats); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode stats: %w", err)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
44
internal/cache/key.go
vendored
Normal file
44
internal/cache/key.go
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/models"
|
||||
)
|
||||
|
||||
// GenerateCacheKey 生成缓存键
|
||||
func GenerateCacheKey(repoID int64, branch string, constraint *models.StatsConstraint, commitHash string) string {
|
||||
var constraintStr string
|
||||
|
||||
if constraint != nil {
|
||||
if constraint.Type == models.ConstraintTypeDateRange {
|
||||
constraintStr = fmt.Sprintf("dr_%s_%s", constraint.From, constraint.To)
|
||||
} else if constraint.Type == models.ConstraintTypeCommitLimit {
|
||||
constraintStr = fmt.Sprintf("cl_%d", constraint.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("repo:%d|branch:%s|constraint:%s|commit:%s",
|
||||
repoID, branch, constraintStr, commitHash)
|
||||
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// SerializeConstraint 序列化约束为JSON字符串
|
||||
func SerializeConstraint(constraint *models.StatsConstraint) string {
|
||||
if constraint == nil {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
if constraint.Type == models.ConstraintTypeDateRange {
|
||||
return fmt.Sprintf(`{"type":"date_range","from":"%s","to":"%s"}`,
|
||||
constraint.From, constraint.To)
|
||||
} else if constraint.Type == models.ConstraintTypeCommitLimit {
|
||||
return fmt.Sprintf(`{"type":"commit_limit","limit":%d}`, constraint.Limit)
|
||||
}
|
||||
|
||||
return "{}"
|
||||
}
|
||||
Reference in New Issue
Block a user