Как избежать «сиротских» файлов (orphaned files) в S3, если не удалось сохранить запись в базе данных?
Разрабатываю сервис (NestJS + TypeORM + PostgreSQL + S3-совместимое хранилище).
Пример: создаю сущность фильма с полями name, description, poster_path. пользователь загружает постер в s3 (aws/filebase и т.п.), получает key и я сохраняю его в БД.
Минимальный пример:
// service
public async createMovie(
dto: CreateMovieDto,
): Promise<MovieResponseDtoWithRelations> {
// файл уже загружен в S3, dto.poster_path = ключ объекта в S3
const movie = await this.moviesRepository.save({
name: dto.name,
description: dto.description,
poster_path: dto.poster_path,
});
return serializationEntity(movie, MovieResponseDtoWithRelations);
}
загрузка может происходить как отдельным эндпоинтом с возвратом ключа, так и прямо перед созданием movie.
Проблема: если сохранение в БД не удалось (ошибка транзакции, падение приложения и т.д.), то файл остаётся в S3 без ссылки в БД → получается «сиротский файл» (orphaned file). со временем таких объектов может накопиться очень много.
знаю про outbox pattern для событий, но не уверен, уместно ли строить всю работу с файлами через outbox.
Вопрос: какие практики/паттерны применяются в продакшне, чтобы избежать «сиротских» файлов в S3 при связке с БД? Буду признателен за реальные схемы/подводные камни.
@eri
Я правильно понимаю, что алгоритм работы будет таким:
- В movie-controller (метод POST /movie, createMovie) мы принимаем CreateMovieDto, в котором указываем все поля для создания фильма, кроме тех, что содержат путь к файлу. Код:
public async createMovie(
dto: CreateMovieDto,
): Promise<MovieResponseDtoWithRelations> {
const posterPathKey = `poster/${uuidv4()}.mp4`;
const movie = await this.moviesRepository.create({
posterPath: posterPathKey,
name: dto.name,
description: dto.description,
});
if (!movie) {
throw new BadRequestException("Failed to create movie.");
}
const posterPathUploadUrl = await this.generatePresignedUrl(posterPathKey, "image/jpg");
return {
posterPathUploadUrl,
movie: serializationEntity(movie, MovieResponseDtoWithRelations),
};
}
- На клиенте загружаем файл в S3 по выданному presignedUrl:
const formData = new FormData(e.target);
const file = formData.get("file");
const response = await axios.put(presignedUrl, file, {
headers: { "Content-Type": file.type },
});
Всё ли я правильно понял или где-то есть нюансы, которые я упускаю?
Ответы (1 шт):
Я бы использовал presigned_URL для загрузки. Добавить в ключ uuid, подписать ссылку и сохранить её в базу. При успешном сохранении передаем ссылку на аплоад.
В этом случае будут битые ссылки в базе, но их проще валидировать потом. Можно просить загрузить ещё раз в тот же ключ.