216 lines
6.6 KiB
Go
216 lines
6.6 KiB
Go
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)
|
||
} |