Initial commit: Эфир мессенджер
This commit is contained in:
216
internal/service/file_service.go
Normal file
216
internal/service/file_service.go
Normal file
@@ -0,0 +1,216 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user