package processor import ( "context" "fmt" "mime/multipart" "path/filepath" "strings" "time" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/mappers" "apskel-pos-be/internal/models" "github.com/google/uuid" ) type FileProcessor interface { UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) } type FileRepository interface { Create(ctx context.Context, file *entities.File) error GetByID(ctx context.Context, id uuid.UUID) (*entities.File, error) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.File, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.File, error) Update(ctx context.Context, file *entities.File) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.File, int64, error) GetByFileName(ctx context.Context, fileName string) (*entities.File, error) ExistsByFileName(ctx context.Context, fileName string) (bool, error) } type FileProcessorImpl struct { fileRepo FileRepository fileClient FileClient } func NewFileProcessorImpl(fileRepo FileRepository, fileClient FileClient) *FileProcessorImpl { return &FileProcessorImpl{ fileRepo: fileRepo, fileClient: fileClient, } } func (p *FileProcessorImpl) UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) { if file == nil { return nil, fmt.Errorf("file is required") } if file.Size == 0 { return nil, fmt.Errorf("file cannot be empty") } const maxFileSize = 10 * 1024 * 1024 // 10MB if file.Size > maxFileSize { return nil, fmt.Errorf("file size exceeds maximum limit of 10MB") } if !constants.IsValidFileType(req.FileType) { return nil, fmt.Errorf("invalid file type: %s", req.FileType) } originalName := file.Filename fileName := mappers.GenerateFileName(originalName, organizationID, userID) for { exists, err := p.fileRepo.ExistsByFileName(ctx, fileName) if err != nil { return nil, fmt.Errorf("failed to check filename uniqueness: %w", err) } if !exists { break } ext := filepath.Ext(fileName) base := strings.TrimSuffix(fileName, ext) fileName = fmt.Sprintf("%s_%d%s", base, time.Now().UnixNano(), ext) } src, err := file.Open() if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) } defer src.Close() fileContent := make([]byte, file.Size) _, err = src.Read(fileContent) if err != nil { return nil, fmt.Errorf("failed to read file content: %w", err) } fileURL, err := p.fileClient.UploadFile(ctx, fileName, fileContent) if err != nil { return nil, fmt.Errorf("failed to upload file to storage: %w", err) } mimeType := file.Header.Get("Content-Type") if mimeType == "" { mimeType = "application/octet-stream" } fileType := req.FileType if fileType == "" { fileType = constants.GetFileTypeFromMimeType(mimeType) } fileEntity := mappers.UploadFileRequestToEntity( &models.UploadFileRequest{ FileType: fileType, IsPublic: req.IsPublic, Metadata: req.Metadata, }, organizationID, userID, fileName, originalName, fileURL, mimeType, fileName, file.Size, ) if err := p.fileRepo.Create(ctx, fileEntity); err != nil { return nil, fmt.Errorf("failed to save file metadata: %w", err) } response := mappers.FileEntityToResponse(fileEntity) return response, nil } func (p *FileProcessorImpl) GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) { file, err := p.fileRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get file: %w", err) } response := mappers.FileEntityToResponse(file) return response, nil } func (p *FileProcessorImpl) UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) { file, err := p.fileRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get file: %w", err) } updates := mappers.UpdateFileRequestToEntityUpdates(req) for key, value := range updates { switch key { case "is_public": file.IsPublic = value.(bool) case "metadata": file.Metadata = entities.Metadata(value.(map[string]interface{})) } } if err := p.fileRepo.Update(ctx, file); err != nil { return nil, fmt.Errorf("failed to update file: %w", err) } response := mappers.FileEntityToResponse(file) return response, nil } func (p *FileProcessorImpl) ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) { filters := make(map[string]interface{}) if req.OrganizationID != nil { filters["organization_id"] = *req.OrganizationID } if req.UserID != nil { filters["user_id"] = *req.UserID } if req.FileType != nil { filters["file_type"] = string(*req.FileType) } if req.IsPublic != nil { filters["is_public"] = *req.IsPublic } offset := (req.Page - 1) * req.Limit files, total, err := p.fileRepo.List(ctx, filters, req.Limit, offset) if err != nil { return nil, fmt.Errorf("failed to list files: %w", err) } fileResponses := mappers.FileEntitiesToResponses(files) totalPages := (int(total) + req.Limit - 1) / req.Limit response := &models.ListFilesResponse{ Files: fileResponses, TotalCount: int(total), Page: req.Page, Limit: req.Limit, TotalPages: totalPages, } return response, nil } func (p *FileProcessorImpl) GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) { files, err := p.fileRepo.GetByOrganizationID(ctx, organizationID) if err != nil { return nil, fmt.Errorf("failed to get files by organization: %w", err) } responses := mappers.FileEntitiesToResponses(files) return responses, nil } func (p *FileProcessorImpl) GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) { files, err := p.fileRepo.GetByUserID(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to get files by user: %w", err) } responses := mappers.FileEntitiesToResponses(files) return responses, nil }