Initial commit: Эфир мессенджер

This commit is contained in:
2026-04-06 14:57:36 +03:00
commit ff93679b6d
50 changed files with 5642 additions and 0 deletions

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