This commit is contained in:
2026-10-03 13:22:11 +07:00
parent 05090722a0
commit 2caa2c1a88
74 changed files with 44 additions and 676 deletions
+1 -20
View File
@@ -13,20 +13,15 @@ import (
"evening_detective_server/internal/modules/string_tools"
)
// Ошибки валидации загружаемого файла. Маппятся в gRPC-статусы в app-слое:
// ErrFileTooLarge → ResourceExhausted, остальные → InvalidArgument.
var (
ErrEmptyName = errors.New("пустое имя файла")
ErrEmptyData = errors.New("пустые данные файла")
ErrInvalidExtension = errors.New("недопустимое расширение файла (разрешены pdf, jpg, jpeg, png, gif, webp, mp3, ogg, wav, mp4, webm)")
ErrInvalidContentType = errors.New("содержимое файла не соответствует разрешённым типам (pdf, изображения, аудио или видео)")
ErrFileTooLarge = errors.New("файл слишком большой")
// ErrFileNotFound — файл отсутствует в хранилище.
ErrFileNotFound = errors.New("файл не найден")
ErrFileNotFound = errors.New("файл не найден")
)
// allowedExtensions — расширения, разрешённые для загрузки: PDF, изображения
// и аудио/видео (введение и закрывашка загружаются тем же эндпоинтом).
var allowedExtensions = map[string]struct{}{
".pdf": {},
".jpg": {},
@@ -58,12 +53,6 @@ func NewFileService(
}
}
// UploadFile сохраняет файл в хранилище под уникальным именем и возвращает
// сохранённое имя и категорию файла (image|pdf|audio|video).
//
// Порядок проверок: имя/данные → расширение → base → размер → содержимое.
// Проверка размера — defense-in-depth: фактический лимит запроса
// устанавливается на HTTP-слое (MaxBytesReader) и gRPC-лимитами.
func (s *FileService) UploadFile(
ctx context.Context,
filename string,
@@ -86,8 +75,6 @@ func (s *FileService) UploadFile(
return "", "", ErrEmptyName
}
// Транслитерация может обнулить base (например, «!!!.pdf»): хранимое
// имя без base недопустимо — проверяем до размера и содержимого.
transliterated := string_tools.Transliterate(base)
if transliterated == "" {
return "", "", ErrEmptyName
@@ -97,8 +84,6 @@ func (s *FileService) UploadFile(
return "", "", fmt.Errorf("%w (лимит %d МБ)", ErrFileTooLarge, s.maxFileSize>>20)
}
// Категория по содержимому (сырой sniff): text/* и octet-stream не
// проходят вообще, расширение должно соответствовать категории.
sniffed := http.DetectContentType(data)
if ext == ".mp3" && isMP3(data) {
sniffed = "audio/mpeg"
@@ -124,16 +109,12 @@ func (s *FileService) UploadFile(
return storedName, fileType, nil
}
// isMP3 распознаёт MP3 без необязательного ID3-тега по заголовку MPEG-фрейма.
func isMP3(data []byte) bool {
return len(data) >= 4 && data[0] == 0xff && data[1]&0xe0 == 0xe0 &&
data[1]&0x18 != 0x08 && data[1]&0x06 != 0 &&
data[2]&0xf0 != 0 && data[2]&0xf0 != 0xf0 && data[2]&0x0c != 0x0c
}
// contentTypeCategory возвращает категорию по sniffed-типу:
// application/pdf → pdf; image/* → image; audio/* и application/ogg → audio;
// всё остальное (text/*, application/octet-stream и пр.) → "".
func contentTypeCategory(sniffed string) string {
switch {
case sniffed == "application/pdf":