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
+14
View File
@@ -6,6 +6,7 @@ import (
"errors"
"log/slog"
"net/http"
"os"
"strconv"
"strings"
"time"
@@ -1686,9 +1687,22 @@ func (s *Server) deleteGuild(ctx context.Context, actor *store.User, guildID uin
if guild.IsMain {
return humaErrorStatus(http.StatusForbidden, "guild.main_protected", "main guild cannot be deleted")
}
// Пути файлов собираем до удаления: записи уходят каскадом, а содержимое
// на диске осталось бы мусором (AGENT.md 6.4).
storagePaths, err := s.store.GuildStoragePaths(ctx, guildID)
if err != nil {
s.logger.WarnContext(ctx, "failed to list guild files",
slog.String("guild_id", formatSnowflake(guildID)), slog.Any("error", err))
}
if err := s.store.DeleteGuild(ctx, guildID); err != nil {
return humaError(err)
}
for _, path := range storagePaths {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
s.logger.WarnContext(ctx, "failed to remove guild file",
slog.String("path", path), slog.Any("error", err))
}
}
s.invalidateGuild(guildID)
s.recordAudit(ctx, actor, 0, "guild.delete", "guild", &guildID, "")
if s.gateway != nil {