// Package file_service handles file upload and download operations. 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" ) 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 = errors.New("файл не найден") ) var allowedExtensions = map[string]struct{}{ ".pdf": {}, ".jpg": {}, ".jpeg": {}, ".png": {}, ".gif": {}, ".webp": {}, ".mp3": {}, ".ogg": {}, ".wav": {}, ".mp4": {}, ".webm": {}, } // FileService handles file downloads. type FileService struct { fileStorage file_storage.IFileStorage maxFileSize int } // NewFileService creates a file service. func NewFileService( fileStorage file_storage.IFileStorage, maxFileSize int, ) *FileService { return &FileService{ fileStorage: fileStorage, maxFileSize: maxFileSize, } } func (s *FileService) UploadFile( ctx context.Context, 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 } transliterated := string_tools.Transliterate(base) if transliterated == "" { return "", "", ErrEmptyName } if len(data) > s.maxFileSize { return "", "", fmt.Errorf("%w (лимит %d МБ)", ErrFileTooLarge, s.maxFileSize>>20) } sniffed := http.DetectContentType(data) if ext == ".mp3" && isMP3(data) { sniffed = "audio/mpeg" } 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 } 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 } 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 strings.HasPrefix(sniffed, "video/"): return "video" case sniffed == "application/ogg": return "audio" default: return "" } } // DownloadFile returns a file for download. func (s *FileService) DownloadFile( ctx context.Context, filename string, ) (*file_storage.File, error) { file, err := s.fileStorage.Get(ctx, filename) if err != nil { if errors.Is(err, file_storage.ErrFileNotFound) { return nil, ErrFileNotFound } return nil, err } return file, nil } // DeleteFile removes a stored file. func (s *FileService) DeleteFile(ctx context.Context, filename string) error { return s.fileStorage.Delete(ctx, filename) }