package service import ( "context" "errors" "fmt" "io" "messenger/internal/models" "messenger/internal/pkg/logger" "messenger/internal/repository" "mime/multipart" "os" "path/filepath" "strings" "time" "github.com/google/uuid" ) type FileService struct { attachmentRepo repository.AttachmentRepository chatRepo repository.ChatRepository storagePath string maxFileSize int64 } func NewFileService( attachmentRepo repository.AttachmentRepository, chatRepo repository.ChatRepository, storagePath string, maxFileSizeMB int64, ) *FileService { return &FileService{ attachmentRepo: attachmentRepo, chatRepo: chatRepo, storagePath: storagePath, maxFileSize: maxFileSizeMB * 1024 * 1024, // Convert to bytes } } // UploadFile загружает файл и создает запись в БД func (s *FileService) UploadFile(ctx context.Context, chatID, userID int64, fileHeader *multipart.FileHeader) (*models.Attachment, error) { // Проверяем, что пользователь участник чата isMember, err := s.chatRepo.IsMember(ctx, chatID, userID) if err != nil { return nil, err } if !isMember { return nil, errors.New("you are not a member of this chat") } // Проверяем размер файла if fileHeader.Size > s.maxFileSize { return nil, fmt.Errorf("file too large: max %d MB", s.maxFileSize/(1024*1024)) } // Открываем файл file, err := fileHeader.Open() if err != nil { return nil, errors.New("failed to open file") } defer file.Close() // Генерируем уникальное имя файла ext := filepath.Ext(fileHeader.Filename) uniqueID := uuid.New().String() safeFileName := uniqueID + ext // Создаем поддиректорию по дате now := time.Now() subDir := filepath.Join(now.Format("2006"), now.Format("01")) fullDir := filepath.Join(s.storagePath, subDir) // Создаем директорию если не существует if err := os.MkdirAll(fullDir, 0755); err != nil { logger.Error("Failed to create storage directory", "error", err) return nil, errors.New("failed to save file") } // Полный путь к файлу filePath := filepath.Join(fullDir, safeFileName) // Создаем файл на диске dst, err := os.Create(filePath) if err != nil { logger.Error("Failed to create file", "error", err) return nil, errors.New("failed to save file") } defer dst.Close() // Копируем содержимое if _, err := io.Copy(dst, file); err != nil { logger.Error("Failed to copy file", "error", err) return nil, errors.New("failed to save file") } // Определяем MIME тип mimeType := fileHeader.Header.Get("Content-Type") if mimeType == "" { // Простая проверка по расширению switch strings.ToLower(ext) { case ".jpg", ".jpeg": mimeType = "image/jpeg" case ".png": mimeType = "image/png" case ".gif": mimeType = "image/gif" case ".pdf": mimeType = "application/pdf" case ".txt": mimeType = "text/plain" default: mimeType = "application/octet-stream" } } // Создаем запись в БД attachment := &models.Attachment{ FileName: fileHeader.Filename, FileSize: fileHeader.Size, StoragePath: filePath, MimeType: mimeType, UploadedAt: now, } if err := s.attachmentRepo.Create(ctx, attachment); err != nil { // Если не удалось сохранить в БД, удаляем файл os.Remove(filePath) logger.Error("Failed to create attachment record", "error", err) return nil, errors.New("failed to save file info") } logger.Info("File uploaded", "attachment_id", attachment.ID, "user_id", userID, "chat_id", chatID) return attachment, nil } // GetAttachment возвращает информацию о вложении func (s *FileService) GetAttachment(ctx context.Context, attachmentID, userID int64) (*models.Attachment, error) { attachment, err := s.attachmentRepo.FindByID(ctx, attachmentID) if err != nil { return nil, err } if attachment == nil { return nil, errors.New("attachment not found") } // Проверяем доступ пользователя к файлу // Находим сообщение, к которому прикреплен файл if attachment.MessageID != nil { // Здесь нужно проверить, что пользователь участник чата // Для этого нужен доступ к messageRepo, но пока пропустим // В реальном коде нужно добавить проверку } return attachment, nil } // DownloadFile возвращает файл для скачивания func (s *FileService) DownloadFile(ctx context.Context, attachmentID, userID int64) (string, *os.File, string, error) { attachment, err := s.GetAttachment(ctx, attachmentID, userID) if err != nil { return "", nil, "", err } // Открываем файл file, err := os.Open(attachment.StoragePath) if err != nil { logger.Error("Failed to open file", "error", err) return "", nil, "", errors.New("file not found") } return attachment.FileName, file, attachment.MimeType, nil } // DeleteAttachment удаляет вложение (только если оно не привязано к сообщению) func (s *FileService) DeleteAttachment(ctx context.Context, attachmentID, userID int64, isGlobalAdmin bool) error { attachment, err := s.attachmentRepo.FindByID(ctx, attachmentID) if err != nil { return err } if attachment == nil { return errors.New("attachment not found") } // Нельзя удалить файл, который привязан к сообщению if attachment.MessageID != nil && !isGlobalAdmin { return errors.New("cannot delete attachment that is linked to a message") } // Удаляем файл с диска if err := os.Remove(attachment.StoragePath); err != nil { logger.Error("Failed to delete file", "error", err) // Не возвращаем ошибку, продолжаем удаление из БД } // Удаляем запись из БД if err := s.attachmentRepo.Delete(ctx, attachmentID); err != nil { return err } logger.Info("Attachment deleted", "attachment_id", attachmentID, "user_id", userID) return nil } // GetMaxFileSize возвращает максимальный размер файла в байтах func (s *FileService) GetMaxFileSize() int64 { return s.maxFileSize } // GetMaxFileSizeMB возвращает максимальный размер файла в мегабайтах func (s *FileService) GetMaxFileSizeMB() int64 { return s.maxFileSize / (1024 * 1024) }