feat(ops): проверка бэкапа и уборка файлов без записи

- glchat verify-backup: расшифровка age, integrity_check, foreign_key_check,
  схема и состав данных, сверка с манифестом; живые данные не трогает
- чистка удаляет файлы на диске, у которых не осталось записи в базе (так
  оставались вложения удалённых серверов), и пустые подкаталоги; работает
  через os.Root, чтобы символическая ссылка не вывела за каталог файлов
- удаление сервера теперь чистит и содержимое файлов, а не только записи
- install.sh пишет значения .env с пробелами в кавычках: файл подключается
  через `set -a && . .env`, иначе имя главного сервера выполнялось как команда
- тесты: Go на уборку файлов без записи (свежие не трогаются, пустые
  каталоги удаляются)
This commit is contained in:
2026-09-21 23:33:04 +03:00
parent ef871bcd96
commit 1cb949a7a4
8 changed files with 329 additions and 4 deletions
+46
View File
@@ -209,3 +209,49 @@ func TestCleanupKeepsFreshOrphansAndRecentAudit(t *testing.T) {
t.Fatalf("свежий файл удалён: %v", err)
}
}
func TestCleanupRemovesFilesWithoutRecords(t *testing.T) {
st, _, dataDir := newRetentionStore(t)
ctx := context.Background()
filesDir := filepath.Join(dataDir, "files")
nested := filepath.Join(filesDir, "ab")
if err := os.MkdirAll(nested, 0o700); err != nil {
t.Fatalf("prepare files dir: %v", err)
}
// Файл на диске, записи о котором нет: так остаются вложения удалённых
// серверов (запись уходит каскадом, содержимое — нет).
orphan := filepath.Join(nested, "orphan.bin")
if err := os.WriteFile(orphan, []byte("мусор"), 0o600); err != nil {
t.Fatalf("write orphan: %v", err)
}
old := time.Now().Add(-48 * time.Hour)
if err := os.Chtimes(orphan, old, old); err != nil {
t.Fatalf("touch orphan: %v", err)
}
// Свежий файл без записи не трогаем: он может ещё не получить запись.
fresh := filepath.Join(filesDir, "fresh.bin")
if err := os.WriteFile(fresh, []byte("новый"), 0o600); err != nil {
t.Fatalf("write fresh: %v", err)
}
runner := New(st, Options{DataDir: dataDir, AuditDays: 30, OrphanHours: 24})
stats, err := runner.RunOnce(ctx)
if err != nil {
t.Fatalf("cleanup: %v", err)
}
if stats.Unknown != 1 {
t.Fatalf("удалено файлов без записи: %d, ожидался 1", stats.Unknown)
}
if _, err := os.Stat(orphan); !os.IsNotExist(err) {
t.Fatalf("файл без записи остался: %v", err)
}
if _, err := os.Stat(fresh); err != nil {
t.Fatalf("свежий файл удалён: %v", err)
}
// Пустой подкаталог после уборки удалён.
if _, err := os.Stat(nested); !os.IsNotExist(err) {
t.Fatalf("пустой каталог остался: %v", err)
}
}