Files
efir-api-server/internal/service/file_service.go

216 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}