feat: Phase 2 — Git 核心功能,go-git 克隆、SSE 进度、D3.js 图形、diff2html 查看器
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CacheCleanupResult holds the outcome of a cleanup operation.
|
||||
type CacheCleanupResult struct {
|
||||
RemovedCount int `json:"removed_count"`
|
||||
FreedBytes int64 `json:"freed_bytes"`
|
||||
RemovedPaths []string `json:"removed_paths"`
|
||||
}
|
||||
|
||||
type repoEntry struct {
|
||||
name string
|
||||
path string
|
||||
modTime time.Time
|
||||
size int64
|
||||
}
|
||||
|
||||
// CleanupExpiredRepos removes repos older than maxAge days from reposDir.
|
||||
func CleanupExpiredRepos(reposDir string, maxAgeDays int) (*CacheCleanupResult, error) {
|
||||
result := &CacheCleanupResult{}
|
||||
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
|
||||
|
||||
entries, err := os.ReadDir(reposDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read repos dir: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
repoPath := filepath.Join(reposDir, entry.Name())
|
||||
size := dirSize(repoPath)
|
||||
if err := os.RemoveAll(repoPath); err != nil {
|
||||
continue
|
||||
}
|
||||
result.RemovedCount++
|
||||
result.FreedBytes += size
|
||||
result.RemovedPaths = append(result.RemovedPaths, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CleanupBySize removes oldest repos until total size is under maxMB.
|
||||
func CleanupBySize(reposDir string, maxMB int) (*CacheCleanupResult, error) {
|
||||
result := &CacheCleanupResult{}
|
||||
maxBytes := int64(maxMB) * 1024 * 1024
|
||||
|
||||
entries, err := os.ReadDir(reposDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return result, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect repo info with mod time
|
||||
var repos []repoEntry
|
||||
var totalSize int64
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
repoPath := filepath.Join(reposDir, entry.Name())
|
||||
size := dirSize(repoPath)
|
||||
repos = append(repos, repoEntry{
|
||||
name: entry.Name(),
|
||||
path: repoPath,
|
||||
modTime: info.ModTime(),
|
||||
size: size,
|
||||
})
|
||||
totalSize += size
|
||||
}
|
||||
|
||||
if totalSize <= maxBytes {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Sort oldest first
|
||||
sortReposByModTime(repos)
|
||||
|
||||
for _, repo := range repos {
|
||||
if totalSize <= maxBytes {
|
||||
break
|
||||
}
|
||||
if err := os.RemoveAll(repo.path); err != nil {
|
||||
continue
|
||||
}
|
||||
totalSize -= repo.size
|
||||
result.RemovedCount++
|
||||
result.FreedBytes += repo.size
|
||||
result.RemovedPaths = append(result.RemovedPaths, repo.name)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCacheStats returns current cache statistics.
|
||||
func GetCacheStats(reposDir string) (count int, totalBytes int64) {
|
||||
entries, err := os.ReadDir(reposDir)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
totalBytes += dirSize(filepath.Join(reposDir, entry.Name()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func sortReposByModTime(repos []repoEntry) {
|
||||
for i := 0; i < len(repos); i++ {
|
||||
for j := i + 1; j < len(repos); j++ {
|
||||
if repos[j].modTime.Before(repos[i].modTime) {
|
||||
repos[i], repos[j] = repos[j], repos[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user