add load applications

This commit is contained in:
2026-08-30 21:17:32 +07:00
parent 3b15fb3a2b
commit c0b5ceb65f
28 changed files with 1536 additions and 139 deletions
+109 -6
View File
@@ -2,34 +2,137 @@ package file_service
import (
"context"
"errors"
"fmt"
"net/http"
"path/filepath"
"strings"
"evening_detective_server/internal/modules/file_storage"
"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)")
ErrInvalidContentType = errors.New("содержимое файла не соответствует разрешённым типам (pdf, изображения или аудио)")
ErrFileTooLarge = errors.New("файл слишком большой")
)
// allowedExtensions — расширения, разрешённые для загрузки: PDF, изображения
// и аудио (аудио-введение сценария загружается тем же эндпоинтом).
var allowedExtensions = map[string]struct{}{
".pdf": {},
".jpg": {},
".jpeg": {},
".png": {},
".gif": {},
".webp": {},
".mp3": {},
".ogg": {},
".wav": {},
}
type FileService struct {
fileStorage file_storage.IFileStorage
stringTools string_tools.IStringTools
maxFileSize int
}
func NewFileService(
fileStorage file_storage.IFileStorage,
stringTools string_tools.IStringTools,
maxFileSize int,
) *FileService {
return &FileService{
fileStorage: fileStorage,
stringTools: stringTools,
maxFileSize: maxFileSize,
}
}
// UploadFile сохраняет файл в хранилище под уникальным именем и возвращает
// сохранённое имя и категорию файла (image|pdf|audio).
//
// Порядок проверок: имя/данные → расширение → base → размер → содержимое.
// Проверка размера — defense-in-depth: фактический лимит запроса
// устанавливается на HTTP-слое (MaxBytesReader) и gRPC-лимитами.
func (s *FileService) UploadFile(
ctx context.Context,
file *file_storage.File,
) (string, error) {
file.Name = s.stringTools.Transliterate(file.Name)
if err := s.fileStorage.Put(ctx, file); err != nil {
return "", err
filename string,
data []byte,
) (string, string, error) {
if filename == "" {
return "", "", ErrEmptyName
}
if len(data) == 0 {
return "", "", ErrEmptyData
}
ext := strings.ToLower(filepath.Ext(filename))
if _, ok := allowedExtensions[ext]; !ok {
return "", "", ErrInvalidExtension
}
base := strings.TrimSuffix(filename, filepath.Ext(filename))
if base == "" {
return "", "", ErrEmptyName
}
// Транслитерация может обнулить base (например, «!!!.pdf»): хранимое
// имя без base недопустимо — проверяем до размера и содержимого.
transliterated := s.stringTools.Transliterate(base)
if transliterated == "" {
return "", "", ErrEmptyName
}
if len(data) > s.maxFileSize {
return "", "", fmt.Errorf("%w (лимит %d МБ)", ErrFileTooLarge, s.maxFileSize>>20)
}
// Категория по содержимому (сырой sniff): text/* и octet-stream не
// проходят вообще, расширение должно соответствовать категории.
sniffed := http.DetectContentType(data)
fileType := contentTypeCategory(sniffed)
if fileType == "" || fileType != file_storage.FileType(filename) {
return "", "", ErrInvalidContentType
}
randomName, err := file_storage.NewName()
if err != nil {
return "", "", fmt.Errorf("не удалось сгенерировать имя файла: %w", err)
}
storedName := transliterated + "_" + randomName + ext
if err := s.fileStorage.Put(ctx, &file_storage.File{
Name: storedName,
Data: data,
Mime: sniffed,
}); err != nil {
return "", "", err
}
return storedName, fileType, nil
}
// 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":
return "pdf"
case strings.HasPrefix(sniffed, "image/"):
return "image"
case strings.HasPrefix(sniffed, "audio/"):
return "audio"
case sniffed == "application/ogg":
return "audio"
default:
return ""
}
return file.Name, nil
}
func (s *FileService) DownloadFile(