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)
|
|||
|
|
}
|