diff --git a/api/openapi.yaml b/api/openapi.yaml index a679548..f8da3f1 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1236,10 +1236,11 @@ components: indexConsistent: { type: boolean } SystemOverview: type: object - required: [imageCount, storedBytes, aliasCount, heapAllocBytes, heapSysBytes, rssBytes, goroutines, indexes, indexConsistent, missingImageCount, missingImageIds, lastInspection, lastRebuild, lastDaily] + required: [imageCount, storedBytes, migrationStoredBytes, aliasCount, heapAllocBytes, heapSysBytes, rssBytes, goroutines, indexes, indexConsistent, missingImageCount, missingImageIds, lastInspection, lastRebuild, lastDaily] properties: imageCount: { type: integer, format: int64, minimum: 0 } storedBytes: { type: integer, format: int64, minimum: 0 } + migrationStoredBytes: { type: integer, format: int64, minimum: 0 } aliasCount: { type: integer, format: int64, minimum: 0 } heapAllocBytes: { type: integer, format: int64, minimum: 0 } heapSysBytes: { type: integer, format: int64, minimum: 0 } diff --git a/internal/app/app.go b/internal/app/app.go index 92be6ee..65f5ce6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -92,10 +92,10 @@ func Build(ctx context.Context, cfg config.Config, db *sql.DB, logger *slog.Logg cancel() return nil, fmt.Errorf("load application settings: %w", err) } + migrationImageService := migrationimage.NewService(filesystem, cfg.MigrationMutations) maintenanceService := maintenance.NewService( - maintenance.NewRepository(db), filesystem, rebuilder, deliveryIndex, authService, tokenService, logger, + maintenance.NewRepository(db), filesystem, rebuilder, deliveryIndex, authService, tokenService, migrationImageService, logger, ) - migrationImageService := migrationimage.NewService(filesystem, cfg.MigrationMutations) unavailableIDs := append(append([]string(nil), loadResult.Delivery.MissingIDs...), loadResult.Delivery.InvalidSizeIDs...) maintenanceService.RecordStartupMissing(unavailableIDs) ui, err := webui.New() diff --git a/internal/httpapi/maintenance_handlers.go b/internal/httpapi/maintenance_handlers.go index cf036c4..2cc1797 100644 --- a/internal/httpapi/maintenance_handlers.go +++ b/internal/httpapi/maintenance_handlers.go @@ -42,20 +42,21 @@ type rebuildResponse struct { } type overviewResponse struct { - ImageCount int64 `json:"imageCount"` - StoredBytes int64 `json:"storedBytes"` - AliasCount int64 `json:"aliasCount"` - HeapAllocBytes uint64 `json:"heapAllocBytes"` - HeapSysBytes uint64 `json:"heapSysBytes"` - RSSBytes uint64 `json:"rssBytes"` - Goroutines int `json:"goroutines"` - Indexes indexStatsResponse `json:"indexes"` - IndexConsistent bool `json:"indexConsistent"` - MissingImageCount int `json:"missingImageCount"` - MissingImageIDs []string `json:"missingImageIds"` - LastInspection *inspectionResponse `json:"lastInspection"` - LastRebuild *rebuildResponse `json:"lastRebuild"` - LastDaily *dailyResponse `json:"lastDaily"` + ImageCount int64 `json:"imageCount"` + StoredBytes int64 `json:"storedBytes"` + MigrationStoredBytes int64 `json:"migrationStoredBytes"` + AliasCount int64 `json:"aliasCount"` + HeapAllocBytes uint64 `json:"heapAllocBytes"` + HeapSysBytes uint64 `json:"heapSysBytes"` + RSSBytes uint64 `json:"rssBytes"` + Goroutines int `json:"goroutines"` + Indexes indexStatsResponse `json:"indexes"` + IndexConsistent bool `json:"indexConsistent"` + MissingImageCount int `json:"missingImageCount"` + MissingImageIDs []string `json:"missingImageIds"` + LastInspection *inspectionResponse `json:"lastInspection"` + LastRebuild *rebuildResponse `json:"lastRebuild"` + LastDaily *dailyResponse `json:"lastDaily"` } type dailyResponse struct { @@ -110,7 +111,8 @@ func (h *maintenanceHandler) inspect(w http.ResponseWriter, r *http.Request) { func toOverviewResponse(value maintenance.Overview) overviewResponse { response := overviewResponse{ - ImageCount: value.Persistent.ImageCount, StoredBytes: value.Persistent.StoredBytes, AliasCount: value.Persistent.AliasCount, + ImageCount: value.Persistent.ImageCount, StoredBytes: value.Persistent.StoredBytes, + MigrationStoredBytes: value.MigrationStoredBytes, AliasCount: value.Persistent.AliasCount, HeapAllocBytes: value.Runtime.HeapAllocBytes, HeapSysBytes: value.Runtime.HeapSysBytes, RSSBytes: value.Runtime.RSSBytes, Goroutines: value.Runtime.Goroutines, Indexes: indexStatsResponse{ diff --git a/internal/httpapi/phase_five_test.go b/internal/httpapi/phase_five_test.go index 466437a..849012f 100644 --- a/internal/httpapi/phase_five_test.go +++ b/internal/httpapi/phase_five_test.go @@ -91,11 +91,16 @@ func TestPhaseFiveOverviewInspectionAndManualRebuild(t *testing.T) { fixture := newPhaseTwoFixture(t) cookies, csrfToken, _ := fixture.login(nil, phaseTwoPassword) image := fixture.uploadBytes(cookies, csrfToken, "public", "", "overview.jpg", phaseTwoJPEG(t)) + migrationBytes := phaseTwoJPEG(t) + if err := os.WriteFile(filepath.Join(fixture.dataDirectory, "migrations", "overview.jpg"), migrationBytes, 0o640); err != nil { + t.Fatalf("write migration image: %v", err) + } overview := fixture.request(http.MethodGet, "/api/v1/overview", nil, cookies, "", "") var firstOverview overviewResponse if err := json.Unmarshal(overview.Body.Bytes(), &firstOverview); overview.Code != http.StatusOK || err != nil || - firstOverview.ImageCount != 1 || firstOverview.Indexes.Images != 1 || !firstOverview.IndexConsistent { + firstOverview.ImageCount != 1 || firstOverview.MigrationStoredBytes != int64(len(migrationBytes)) || + firstOverview.Indexes.Images != 1 || !firstOverview.IndexConsistent { t.Fatalf("overview status = %d, value = %+v, error = %v", overview.Code, firstOverview, err) } if err := os.Remove(filepath.Join(fixture.dataDirectory, "images", image.ID)); err != nil { diff --git a/internal/httpapi/phase_two_test.go b/internal/httpapi/phase_two_test.go index ceaff31..86e76ce 100644 --- a/internal/httpapi/phase_two_test.go +++ b/internal/httpapi/phase_two_test.go @@ -253,8 +253,10 @@ func newHTTPFixture(t *testing.T, engine processor.Engine, gate *processor.Gate, settingsService := settings.NewService(settings.NewRepository(db)) var logs bytes.Buffer logger := slog.New(slog.NewJSONHandler(&logs, nil)) - maintenanceService := maintenance.NewService(maintenance.NewRepository(db), filesystem, rebuilder, deliveryIndex, authService, tokenService, logger) migrationImageService := migrationimage.NewService(filesystem, true) + maintenanceService := maintenance.NewService( + maintenance.NewRepository(db), filesystem, rebuilder, deliveryIndex, authService, tokenService, migrationImageService, logger, + ) router := NewRouter(Dependencies{ DB: db, Logger: logger, Auth: authService, APITokens: tokenService, Aliases: aliasService, Images: imageService, Importer: importService, Settings: settingsService, DeliveryIndex: deliveryIndex, Storage: filesystem, diff --git a/internal/maintenance/service.go b/internal/maintenance/service.go index d6d807e..f262a88 100644 --- a/internal/maintenance/service.go +++ b/internal/maintenance/service.go @@ -23,15 +23,16 @@ type IndexStats struct { } type Overview struct { - Persistent PersistentStats - Runtime RuntimeSnapshot - Indexes IndexStats - IndexConsistent bool - MissingImageCount int - MissingImageIDs []string - LastInspection *InspectionResult - LastRebuild *RebuildResult - LastDaily *DailyResult + Persistent PersistentStats + MigrationStoredBytes int64 + Runtime RuntimeSnapshot + Indexes IndexStats + IndexConsistent bool + MissingImageCount int + MissingImageIDs []string + LastInspection *InspectionResult + LastRebuild *RebuildResult + LastDaily *DailyResult } type InspectionResult struct { @@ -72,6 +73,7 @@ type DailyResult struct { type Service struct { repository *Repository storage *storage.Filesystem + migrationStorage migrationStorage rebuilder *indexstate.Rebuilder delivery *delivery.Index sessions *auth.Service @@ -85,6 +87,10 @@ type Service struct { lastDaily *DailyResult } +type migrationStorage interface { + StoredBytes(context.Context) (int64, error) +} + func NewService( repository *Repository, filesystem *storage.Filesystem, @@ -92,11 +98,12 @@ func NewService( deliveryIndex *delivery.Index, sessions *auth.Service, tokens *apitoken.Service, + migrationStorage migrationStorage, logger *slog.Logger, ) *Service { return &Service{ repository: repository, storage: filesystem, rebuilder: rebuilder, - delivery: deliveryIndex, sessions: sessions, tokens: tokens, logger: logger, + delivery: deliveryIndex, sessions: sessions, tokens: tokens, migrationStorage: migrationStorage, logger: logger, } } @@ -113,6 +120,10 @@ func (s *Service) Overview(ctx context.Context) (Overview, error) { if err != nil { return Overview{}, err } + migrationBytes, err := s.migrationStorage.StoredBytes(ctx) + if err != nil { + return Overview{}, err + } s.mu.RLock() missingCount := s.missingImageCount missingIDs := append([]string(nil), s.missingImageIDs...) @@ -121,7 +132,7 @@ func (s *Service) Overview(ctx context.Context) (Overview, error) { lastDaily := cloneDaily(s.lastDaily) s.mu.RUnlock() return Overview{ - Persistent: persistent, Runtime: CaptureRuntime(), Indexes: indexes, IndexConsistent: consistent, + Persistent: persistent, MigrationStoredBytes: migrationBytes, Runtime: CaptureRuntime(), Indexes: indexes, IndexConsistent: consistent, MissingImageCount: missingCount, MissingImageIDs: missingIDs, LastInspection: lastInspection, LastRebuild: lastRebuild, LastDaily: lastDaily, }, nil diff --git a/internal/maintenance/service_test.go b/internal/maintenance/service_test.go index d220fee..f68f941 100644 --- a/internal/maintenance/service_test.go +++ b/internal/maintenance/service_test.go @@ -18,6 +18,7 @@ import ( images "github.com/Willxup/imagesilo/internal/image" "github.com/Willxup/imagesilo/internal/indexbarrier" "github.com/Willxup/imagesilo/internal/indexstate" + "github.com/Willxup/imagesilo/internal/migrationimage" "github.com/Willxup/imagesilo/internal/platform/database" "github.com/Willxup/imagesilo/internal/platform/storage" ) @@ -140,10 +141,24 @@ func TestSizeMismatchIsReportedUnavailableAndNeverCleanedAsOrphan(t *testing.T) } } +func TestOverviewIncludesMigrationDirectoryStorage(t *testing.T) { + directory, service, db := prepareMaintenanceTest(t) + defer db.Close() + content := []byte("migration directory bytes") + if err := os.WriteFile(filepath.Join(directory, "migrations", "notes.txt"), content, 0o600); err != nil { + t.Fatal(err) + } + + overview, err := service.Overview(context.Background()) + if err != nil || overview.MigrationStoredBytes != int64(len(content)) { + t.Fatalf("Overview() migration storage = %d, %v", overview.MigrationStoredBytes, err) + } +} + func prepareMaintenanceTest(t *testing.T) (string, *Service, *sql.DB) { t.Helper() directory := t.TempDir() - for _, path := range []string{"db", "images", filepath.Join("cache", "thumbnails"), "tmp"} { + for _, path := range []string{"db", "images", "migrations", filepath.Join("cache", "thumbnails"), "tmp"} { if err := os.MkdirAll(filepath.Join(directory, path), 0o750); err != nil { t.Fatal(err) } @@ -171,7 +186,10 @@ func prepareMaintenanceTest(t *testing.T) (string, *Service, *sql.DB) { deliveryIndex := delivery.NewIndex() rebuilder := indexstate.NewRebuilder(db, filesystem, authRepository, tokenRepository, deliveryIndex, sessionIndex, tokenIndex, barrier) var logs bytes.Buffer - service := NewService(NewRepository(db), filesystem, rebuilder, deliveryIndex, authService, tokenService, slog.New(slog.NewJSONHandler(&logs, nil))) + service := NewService( + NewRepository(db), filesystem, rebuilder, deliveryIndex, authService, tokenService, + migrationimage.NewService(filesystem, false), slog.New(slog.NewJSONHandler(&logs, nil)), + ) return directory, service, db } diff --git a/internal/migrationimage/service.go b/internal/migrationimage/service.go index 45d3281..b36afeb 100644 --- a/internal/migrationimage/service.go +++ b/internal/migrationimage/service.go @@ -73,6 +73,7 @@ type Service struct { type migrationSnapshot struct { items []Image skippedFiles int + storedBytes int64 scannedAt time.Time } @@ -94,6 +95,14 @@ func (s *Service) Refresh(ctx context.Context) error { return err } +func (s *Service) StoredBytes(ctx context.Context) (int64, error) { + snapshot, err := s.loadSnapshot(ctx, false) + if err != nil { + return 0, err + } + return snapshot.storedBytes, nil +} + func (s *Service) Search(ctx context.Context, filter ListFilter) (Page, error) { if err := validateListFilter(filter); err != nil { return Page{}, err @@ -245,7 +254,9 @@ func (s *Service) scanSnapshot(ctx context.Context) (*migrationSnapshot, error) } return items[left].ModifiedAt.After(items[right].ModifiedAt) }) - return &migrationSnapshot{items: items, skippedFiles: skipped, scannedAt: s.now().UTC()}, nil + return &migrationSnapshot{ + items: items, skippedFiles: skipped, storedBytes: scanned.StoredBytes, scannedAt: s.now().UTC(), + }, nil } func (s *Service) removeCachedPath(canonicalPath string) { @@ -255,12 +266,17 @@ func (s *Service) removeCachedPath(canonicalPath string) { return } items := make([]Image, 0, len(s.snapshot.items)) + storedBytes := s.snapshot.storedBytes for _, item := range s.snapshot.items { if item.Path != canonicalPath { items = append(items, item) + } else { + storedBytes -= item.StoredSize } } - s.snapshot = &migrationSnapshot{items: items, skippedFiles: s.snapshot.skippedFiles, scannedAt: s.snapshot.scannedAt} + s.snapshot = &migrationSnapshot{ + items: items, skippedFiles: s.snapshot.skippedFiles, storedBytes: storedBytes, scannedAt: s.snapshot.scannedAt, + } } func validateListFilter(filter ListFilter) error { diff --git a/internal/migrationimage/service_test.go b/internal/migrationimage/service_test.go index dfee3b9..60c2e9e 100644 --- a/internal/migrationimage/service_test.go +++ b/internal/migrationimage/service_test.go @@ -86,6 +86,10 @@ func TestSearchCachesUntilRefreshAndDeleteUpdatesSnapshot(t *testing.T) { if err != nil || len(initial.Items) != 1 || initial.Items[0].Path != "/i/first.jpg" { t.Fatalf("initial Search() = %+v, %v", initial, err) } + initialBytes, err := service.StoredBytes(context.Background()) + if err != nil || initialBytes != int64(len(migrationTestJPEG(t))) { + t.Fatalf("initial StoredBytes() = %d, %v", initialBytes, err) + } writeMigrationTestImage(t, dataDirectory, "images/second.jpg", migrationTestJPEG(t), base.Add(time.Hour)) cached, err := service.Search(context.Background(), ListFilter{}) if err != nil || len(cached.Items) != 1 { @@ -98,6 +102,10 @@ func TestSearchCachesUntilRefreshAndDeleteUpdatesSnapshot(t *testing.T) { if err != nil || len(refreshed.Items) != 2 || refreshed.Items[0].Path != "/images/second.jpg" { t.Fatalf("refreshed Search() = %+v, %v", refreshed, err) } + refreshedBytes, err := service.StoredBytes(context.Background()) + if err != nil || refreshedBytes != 2*initialBytes { + t.Fatalf("refreshed StoredBytes() = %d, %v", refreshedBytes, err) + } if _, err := service.Delete(context.Background(), "/images/second.jpg"); err != nil { t.Fatalf("Delete() error = %v", err) @@ -106,6 +114,10 @@ func TestSearchCachesUntilRefreshAndDeleteUpdatesSnapshot(t *testing.T) { if err != nil || len(afterDelete.Items) != 1 || afterDelete.Items[0].Path != "/i/first.jpg" { t.Fatalf("Search() after deletion = %+v, %v", afterDelete, err) } + afterDeleteBytes, err := service.StoredBytes(context.Background()) + if err != nil || afterDeleteBytes != initialBytes { + t.Fatalf("StoredBytes() after deletion = %d, %v", afterDeleteBytes, err) + } } func TestDeleteEvictsExternallyRemovedImageFromSnapshot(t *testing.T) { diff --git a/internal/platform/storage/filesystem.go b/internal/platform/storage/filesystem.go index 93dc5b3..f3d172b 100644 --- a/internal/platform/storage/filesystem.go +++ b/internal/platform/storage/filesystem.go @@ -42,6 +42,7 @@ type MigrationFile struct { type MigrationList struct { Files []MigrationFile SkippedFiles int + StoredBytes int64 } type MigrationDeleteResult struct { @@ -97,6 +98,7 @@ func (f *Filesystem) ListMigrationImages(ctx context.Context) (MigrationList, er result.SkippedFiles++ return nil } + result.StoredBytes += info.Size() file, mimeType, err := openMigrationImage(root, relativePath) if err != nil { result.SkippedFiles++ diff --git a/internal/platform/storage/filesystem_test.go b/internal/platform/storage/filesystem_test.go index 849bf5a..7d4b86e 100644 --- a/internal/platform/storage/filesystem_test.go +++ b/internal/platform/storage/filesystem_test.go @@ -75,10 +75,12 @@ func TestListMigrationImagesSkipsInvalidContentAndSymlinks(t *testing.T) { if err := os.WriteFile(validPath, jpegBytes, 0o640); err != nil { t.Fatalf("WriteFile(valid): %v", err) } - if err := os.WriteFile(filepath.Join(migrationsDirectory, "disguised.jpg"), []byte("not an image"), 0o640); err != nil { + disguisedBytes := []byte("not an image") + if err := os.WriteFile(filepath.Join(migrationsDirectory, "disguised.jpg"), disguisedBytes, 0o640); err != nil { t.Fatalf("WriteFile(disguised): %v", err) } - if err := os.WriteFile(filepath.Join(migrationsDirectory, "notes.txt"), []byte("not public"), 0o640); err != nil { + notesBytes := []byte("not public") + if err := os.WriteFile(filepath.Join(migrationsDirectory, "notes.txt"), notesBytes, 0o640); err != nil { t.Fatalf("WriteFile(notes): %v", err) } if err := os.Symlink(validPath, filepath.Join(migrationsDirectory, "linked.jpg")); err != nil { @@ -95,6 +97,9 @@ func TestListMigrationImagesSkipsInvalidContentAndSymlinks(t *testing.T) { if listed.SkippedFiles < 2 { t.Fatalf("SkippedFiles = %d, want at least invalid content and unsupported extension", listed.SkippedFiles) } + if want := int64(len(jpegBytes) + len(disguisedBytes) + len(notesBytes)); listed.StoredBytes != want { + t.Fatalf("StoredBytes = %d, want %d for all regular files", listed.StoredBytes, want) + } } func TestListMigrationImagesHonorsCancellation(t *testing.T) { diff --git a/web/src/features/system/system-overview-panel.tsx b/web/src/features/system/system-overview-panel.tsx index 42c665f..a21dcf2 100644 --- a/web/src/features/system/system-overview-panel.tsx +++ b/web/src/features/system/system-overview-panel.tsx @@ -7,6 +7,7 @@ import { Button } from '../../components/ui/button' import { Card } from '../../components/ui/card' import { ConfirmDialog } from '../../components/ui/confirm-dialog' import { Icon } from '../../components/ui/icon' +import type { IconName } from '../../components/ui/icon' import { apiRequest } from '../../lib/api-client' import { formatBytes } from '../../lib/image-links' import type { InspectionResult, RebuildResult, SystemOverview } from '../../lib/api-types' @@ -47,21 +48,38 @@ export function SystemOverviewPanel() { function Overview({ value }: { value: SystemOverview }) { const { t } = useTranslation() - const cards = [ - [t('settings.imageCount'), String(value.imageCount)], [t('settings.storageUsed'), formatBytes(value.storedBytes)], - [t('settings.aliasCount'), String(value.aliasCount)], [t('settings.rss'), formatBytes(value.rssBytes)], - [t('settings.heap'), formatBytes(value.heapAllocBytes)], [t('settings.goroutines'), String(value.goroutines)], + const rows: Array> = [ + [ + { icon: 'images', label: t('settings.imageCount'), value: String(value.imageCount) }, + { icon: 'history', label: t('settings.aliasCount'), value: String(value.aliasCount) }, + { icon: 'server', label: t('settings.storageUsed'), value: formatBytes(value.storedBytes) }, + { icon: 'refresh', label: t('settings.migrationStorageUsed'), value: formatBytes(value.migrationStoredBytes) }, + ], + [ + { icon: 'server', label: t('settings.rss'), value: formatBytes(value.rssBytes) }, + { icon: 'activity', label: t('settings.heap'), value: formatBytes(value.heapAllocBytes) }, + { icon: 'server', label: t('settings.heapSys'), value: formatBytes(value.heapSysBytes) }, + { icon: 'activity', label: t('settings.goroutines'), value: String(value.goroutines) }, + ], ] return (
-
- {cards.map(([label, number], index) => ( - - - - -

{label}

-

{number}

+
+ {rows.map((metrics) => ( + +
+ {metrics.map((metric) => ( +
+ + + +
+

{metric.label}

+

{metric.value}

+
+
+ ))} +
))}
diff --git a/web/src/features/system/system-page.test.tsx b/web/src/features/system/system-page.test.tsx index 5d96078..80b3666 100644 --- a/web/src/features/system/system-page.test.tsx +++ b/web/src/features/system/system-page.test.tsx @@ -12,6 +12,7 @@ vi.mock('../../lib/api-client', () => ({ apiRequest: vi.fn() })) const overview = { imageCount: 12, storedBytes: 1024, + migrationStoredBytes: 3 * 1024, aliasCount: 3, heapAllocBytes: 2 * 1024, heapSysBytes: 4 * 1024, @@ -55,6 +56,8 @@ describe('SystemPage', () => { renderPage() expect(await screen.findByText('系统状态')).toBeInTheDocument() expect(await screen.findByText('12')).toBeInTheDocument() + expect(screen.getByText('迁移用量')).toBeInTheDocument() + expect(screen.getByText('Go Heap Sys')).toBeInTheDocument() expect(screen.getByText('数据库与图片/别名索引数量一致。')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: '立即巡检' })) diff --git a/web/src/generated/openapi.d.ts b/web/src/generated/openapi.d.ts index 1b287f4..387d794 100644 --- a/web/src/generated/openapi.d.ts +++ b/web/src/generated/openapi.d.ts @@ -809,6 +809,8 @@ export interface components { /** Format: int64 */ storedBytes: number; /** Format: int64 */ + migrationStoredBytes: number; + /** Format: int64 */ aliasCount: number; /** Format: int64 */ heapAllocBytes: number; diff --git a/web/src/i18n/en-US.json b/web/src/i18n/en-US.json index 7cf5919..0ac4ea7 100644 --- a/web/src/i18n/en-US.json +++ b/web/src/i18n/en-US.json @@ -292,9 +292,11 @@ "maintenanceFailed": "The maintenance operation failed.", "imageCount": "Images", "storageUsed": "Storage used", + "migrationStorageUsed": "Migration storage", "aliasCount": "Historical paths", "rss": "Process RSS", "heap": "Go heap", + "heapSys": "Go heap sys", "goroutines": "Goroutines", "indexConsistent": "Database and image/alias index counts agree.", "indexDifferent": "Database and in-memory index counts differ. Check missing files or rebuild.", diff --git a/web/src/i18n/zh-CN.json b/web/src/i18n/zh-CN.json index 759c42d..7a766dc 100644 --- a/web/src/i18n/zh-CN.json +++ b/web/src/i18n/zh-CN.json @@ -292,9 +292,11 @@ "maintenanceFailed": "维护操作失败。", "imageCount": "图片数量", "storageUsed": "存储用量", + "migrationStorageUsed": "迁移用量", "aliasCount": "历史路径", "rss": "进程 RSS", "heap": "Go Heap", + "heapSys": "Go Heap Sys", "goroutines": "Goroutine", "indexConsistent": "数据库与图片/别名索引数量一致。", "indexDifferent": "数据库与内存索引数量不一致,请检查缺失文件或执行重建。",