initial media service
This commit is contained in:
commit
af2e675cbb
16
.air.toml
Normal file
16
.air.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
cmd = "go build -o ./tmp/cmd.exe ./cmd"
|
||||||
|
bin = "./tmp/cmd.exe"
|
||||||
|
delay = 500
|
||||||
|
exclude_dir = ["tmp", "vendor"]
|
||||||
|
include_ext = ["go"]
|
||||||
|
exclude_file = ["*.md"]
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = true
|
||||||
|
|
||||||
|
[env]
|
||||||
|
APP_ENV="development"
|
||||||
21
.env.example
Normal file
21
.env.example
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# General
|
||||||
|
APP_ENV='development'
|
||||||
|
|
||||||
|
# HTTP server
|
||||||
|
HTTP_PORT=4200
|
||||||
|
HTTP_HOST='localhost:4200'
|
||||||
|
|
||||||
|
# gRPC server
|
||||||
|
GRPC_PORT=50059
|
||||||
|
GRPC_HOST='localhost'
|
||||||
|
|
||||||
|
# S3 Storage
|
||||||
|
S3_BUCKET=''
|
||||||
|
S3_REGION=''
|
||||||
|
S3_ENDPOINT=''
|
||||||
|
S3_ACCESS_KEY=''
|
||||||
|
S3_SECRET_KEY=''
|
||||||
|
S3_PUBLIC_URL=''
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL='debug'
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Go binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Go test binaries
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Go coverage output
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Vendor (если не используешь vendoring)
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
102
README.md
Normal file
102
README.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# Media Service
|
||||||
|
|
||||||
|
Этот репозиторий содержит **media-service**, используемый в проекте TeaCinema - сервис для хранения и раздачи медиа-файлов (постеры, баннеры фильмов и другие ассеты).
|
||||||
|
|
||||||
|
Сервис написан на **Golang** и используется в рамках курса по микросервисной архитектуре.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст курса
|
||||||
|
|
||||||
|
В видео-курсе media-service **не разрабатывается с нуля**.
|
||||||
|
Вместо этого:
|
||||||
|
|
||||||
|
- показано, как сервис подключается к системе;
|
||||||
|
- как он интегрируется через **gRPC contracts (proto)**;
|
||||||
|
- как его запускать и использовать в общей инфраструктуре.
|
||||||
|
|
||||||
|
Поэтому данный репозиторий предоставляется в готовом виде - как часть учебных материалов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proto-контракты
|
||||||
|
|
||||||
|
Media-service работает на основе **gRPC proto-контрактов**.
|
||||||
|
|
||||||
|
В рамках курса:
|
||||||
|
|
||||||
|
- proto-файлы **пишутся и объясняются в видео**;
|
||||||
|
- media-service просто подключается к этим контрактам.
|
||||||
|
|
||||||
|
Если вы хотите повторить проект самостоятельно, вам необходимо:
|
||||||
|
|
||||||
|
1. Создать proto-контракты, ориентируясь на видео;
|
||||||
|
2. Сгенерировать код для Go;
|
||||||
|
3. Подключить их к media-service.
|
||||||
|
|
||||||
|
Таймкод в видео, где разбираются proto-контракты и их подключение: https://youtube.com/watch?v=EFXFVNDk2xM&t=83886s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
Media-service запускается как отдельный микросервис и предполагает:
|
||||||
|
|
||||||
|
- наличие gRPC-контрактов;
|
||||||
|
- корректные настройки окружения;
|
||||||
|
|
||||||
|
Подробная схема запуска всей системы разбирается в видеокурсе.
|
||||||
|
|
||||||
|
## Установка зависимостей
|
||||||
|
|
||||||
|
Для установки зависимостей используйте стандартные команды Go:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Инициализация модулей
|
||||||
|
go mod init github.com/yourusername/media-service
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!Важно]
|
||||||
|
> Перед запуском сервиса переименуйте файл .env.example в .env и заполните реальные значения для подключения к S3.
|
||||||
|
|
||||||
|
## Запуск сервиса
|
||||||
|
|
||||||
|
Для запуска media-service есть два варианта:
|
||||||
|
|
||||||
|
### Стандартный запуск через Go
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск сервиса напрямую
|
||||||
|
go run cmd/main.go
|
||||||
|
|
||||||
|
# Или сборка бинарника и запуск
|
||||||
|
go build -o media-service cmd/main.go
|
||||||
|
./media-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Разработка с hot reload через Air
|
||||||
|
|
||||||
|
Сервис можно запускать в режиме разработки с авто-перезапуском при изменениях кода, используя скрипт `run_dev.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/run_dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт автоматически:
|
||||||
|
|
||||||
|
- загружает переменные окружения из .env;
|
||||||
|
- запускает media-service с hot reload через Air.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Media-service предоставляется **в учебных целях**.
|
||||||
|
|
||||||
|
Вы можете:
|
||||||
|
|
||||||
|
- изменить способ хранения файлов;
|
||||||
|
- адаптировать API под свои задачи.
|
||||||
|
|
||||||
|
Главная цель сервиса в рамках курса - показать **интеграцию и архитектурную роль**, а не конкретную реализацию.
|
||||||
82
cmd/main.go
Normal file
82
cmd/main.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/teacinema/media-service/internal/config"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/grpc"
|
||||||
|
httpserver "github.com/teacinema/media-service/internal/infrastructure/http"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/storage"
|
||||||
|
"github.com/teacinema/media-service/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
logger.Init(cfg.Logging.Level)
|
||||||
|
|
||||||
|
logger.Info("🚀 Starting media-service in %s mode", cfg.App.Env)
|
||||||
|
|
||||||
|
var mediaStorage storage.Storage
|
||||||
|
var err error
|
||||||
|
|
||||||
|
mediaStorage, err = storage.NewS3Storage(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("failed to init S3 storage: %v", err)
|
||||||
|
}
|
||||||
|
logger.Info("✅ S3 storage connected (bucket: %s)", cfg.Storage.Bucket)
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer(mediaStorage, cfg)
|
||||||
|
grpcListener, err := net.Listen("tcp", fmt.Sprintf(":%s", cfg.GRPC.Port))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("failed to listen gRPC: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpSrv := httpserver.NewServer(mediaStorage, cfg)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Info("gRPC listening on :%s", cfg.GRPC.Port)
|
||||||
|
if err := grpcServer.Serve(grpcListener); err != nil {
|
||||||
|
logger.Fatal("gRPC serve error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Info("HTTP listening on :%s", cfg.HTTP.Port)
|
||||||
|
if err := httpSrv.Start(); err != nil {
|
||||||
|
logger.Fatal("HTTP server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Info("📦 Background queue workers started")
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForShutdown(func() {
|
||||||
|
logger.Warn("🛑 Graceful shutdown started...")
|
||||||
|
grpcServer.GracefulStop()
|
||||||
|
mediaStorage.Close()
|
||||||
|
httpSrv.Stop(context.Background())
|
||||||
|
logger.Info("✅ Shutdown complete")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForShutdown(cleanup func()) {
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
70
go.mod
Normal file
70
go.mod
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
module github.com/teacinema/media-service
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.10
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.39.6
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.17
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.21
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
||||||
|
github.com/disintegration/imaging v1.6.2
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/kolesa-team/go-webp v1.0.5
|
||||||
|
github.com/teacinema/contracts v1.0.2
|
||||||
|
google.golang.org/grpc v1.77.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.2 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/chai2010/webp v1.4.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/crypto v0.43.0 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||||
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
|
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
golang.org/x/text v0.30.0 // indirect
|
||||||
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
167
go.sum
Normal file
167
go.sum
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 h1:2fjfz3/G9BRvIKuNZ655GwzpklC2kEH0cowZQGO7uBg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4/go.mod h1:Ymws824lvMypLFPwyyUXM52SXuGgxpu0+DISLfKvB+c=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||||
|
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||||
|
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
||||||
|
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kolesa-team/go-webp v1.0.5 h1:GZQHJBaE8dsNKZltfwqsL0qVJ7vqHXsfA+4AHrQW3pE=
|
||||||
|
github.com/kolesa-team/go-webp v1.0.5/go.mod h1:QmJu0YHXT3ex+4SgUvs+a+1SFCDcCqyZg+LbIuNNTnE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/teacinema/contracts v1.0.2 h1:ZA4CupsMjjjNQMrwSbGjtf9cWodDZgGHcmt0cRxlLto=
|
||||||
|
github.com/teacinema/contracts v1.0.2/go.mod h1:oYc1RkjWhR4czmX7uZt4GaP6C6omGyZEdTkuWV5OIf8=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
|
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
|
||||||
|
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
35
internal/application/dto/media.go
Normal file
35
internal/application/dto/media.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type UploadMediaRequest struct {
|
||||||
|
FileName string
|
||||||
|
Folder string
|
||||||
|
ContentType string
|
||||||
|
ResizeWidth int32
|
||||||
|
ResizeHeight int32
|
||||||
|
Preset string
|
||||||
|
Reader io.Reader
|
||||||
|
Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadMediaResponse struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMediaRequest struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMediaResponse struct {
|
||||||
|
Reader io.ReadCloser
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteMediaRequest struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteMediaResponse struct {
|
||||||
|
OK bool
|
||||||
|
}
|
||||||
24
internal/application/usecases/delete.go
Normal file
24
internal/application/usecases/delete.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package usecases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/teacinema/media-service/internal/application/dto"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteUseCase struct {
|
||||||
|
storage storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeleteUseCase(s storage.Storage) *DeleteUseCase {
|
||||||
|
return &DeleteUseCase{storage: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DeleteUseCase) Execute(ctx context.Context, input dto.DeleteMediaRequest) (*dto.DeleteMediaResponse, error) {
|
||||||
|
err := u.storage.Delete(ctx, input.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dto.DeleteMediaResponse{OK: true}, nil
|
||||||
|
}
|
||||||
32
internal/application/usecases/fetch.go
Normal file
32
internal/application/usecases/fetch.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package usecases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/teacinema/media-service/internal/application/dto"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetUseCase struct {
|
||||||
|
storage storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetUseCase(s storage.Storage) *GetUseCase {
|
||||||
|
return &GetUseCase{storage: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *GetUseCase) Execute(
|
||||||
|
ctx context.Context,
|
||||||
|
input dto.GetMediaRequest,
|
||||||
|
) (*dto.GetMediaResponse, error) {
|
||||||
|
|
||||||
|
reader, contentType, err := u.storage.GetStream(ctx, input.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dto.GetMediaResponse{
|
||||||
|
Reader: reader,
|
||||||
|
ContentType: contentType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
42
internal/application/usecases/upload.go
Normal file
42
internal/application/usecases/upload.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package usecases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/teacinema/media-service/internal/application/dto"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/images"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploadUseCase struct {
|
||||||
|
storage storage.Storage
|
||||||
|
processor images.Processor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUploadUseCase(s storage.Storage, p images.Processor) *UploadUseCase {
|
||||||
|
return &UploadUseCase{
|
||||||
|
storage: s,
|
||||||
|
processor: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UploadUseCase) Execute(
|
||||||
|
ctx context.Context,
|
||||||
|
input dto.UploadMediaRequest,
|
||||||
|
) (*dto.UploadMediaResponse, error) {
|
||||||
|
key := fmt.Sprintf("%s/%s", input.Folder, input.FileName)
|
||||||
|
|
||||||
|
if err := u.storage.UploadStream(
|
||||||
|
ctx,
|
||||||
|
key,
|
||||||
|
input.Reader,
|
||||||
|
input.ContentType,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dto.UploadMediaResponse{
|
||||||
|
Key: key,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
76
internal/config/config.go
Normal file
76
internal/config/config.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
App struct {
|
||||||
|
Env string
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP struct {
|
||||||
|
Port string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
GRPC struct {
|
||||||
|
Port string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage struct {
|
||||||
|
Driver string
|
||||||
|
Bucket string
|
||||||
|
Region string
|
||||||
|
Endpoint string
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
PublicURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
Image struct {
|
||||||
|
MaxSizeMB int
|
||||||
|
AllowedTypes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
Logging struct {
|
||||||
|
Level string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
var cfg Config
|
||||||
|
loadFromEnv(&cfg)
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFromEnv(cfg *Config) {
|
||||||
|
get := func(key string) string {
|
||||||
|
return strings.TrimSpace(os.Getenv(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// General
|
||||||
|
cfg.App.Env = get("APP_ENV")
|
||||||
|
|
||||||
|
// HTTP server
|
||||||
|
cfg.HTTP.Port = get("HTTP_PORT")
|
||||||
|
cfg.HTTP.Host = get("HTTP_HOST")
|
||||||
|
|
||||||
|
// gRPC server
|
||||||
|
cfg.GRPC.Port = get("GRPC_PORT")
|
||||||
|
cfg.GRPC.Host = get("GRPC_HOST")
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
cfg.Storage.Driver = get("S3_DRIVER")
|
||||||
|
cfg.Storage.Bucket = get("S3_BUCKET")
|
||||||
|
cfg.Storage.Region = get("S3_REGION")
|
||||||
|
cfg.Storage.Endpoint = get("S3_ENDPOINT")
|
||||||
|
cfg.Storage.AccessKey = get("S3_ACCESS_KEY")
|
||||||
|
cfg.Storage.SecretKey = get("S3_SECRET_KEY")
|
||||||
|
cfg.Storage.PublicURL = get("S3_PUBLIC_URL")
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
cfg.Logging.Level = get("LOG_LEVEL")
|
||||||
|
}
|
||||||
56
internal/infrastructure/grpc/interceptor.go
Normal file
56
internal/infrastructure/grpc/interceptor.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/teacinema/media-service/pkg/logger"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequestLoggerInterceptor(
|
||||||
|
ctx context.Context,
|
||||||
|
req interface{},
|
||||||
|
info *grpc.UnaryServerInfo,
|
||||||
|
handler grpc.UnaryHandler,
|
||||||
|
) (interface{}, error) {
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := handler(ctx, req)
|
||||||
|
|
||||||
|
status := "✅"
|
||||||
|
if err != nil {
|
||||||
|
status = "❌"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("%s %s %v", status, info.FullMethod, time.Since(start))
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TraceIDInterceptor(
|
||||||
|
ctx context.Context,
|
||||||
|
req interface{},
|
||||||
|
info *grpc.UnaryServerInfo,
|
||||||
|
handler grpc.UnaryHandler,
|
||||||
|
) (interface{}, error) {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
md = metadata.New(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := md.Get("x-trace-id")
|
||||||
|
var traceID string
|
||||||
|
if len(ids) == 0 {
|
||||||
|
traceID = uuid.New().String()
|
||||||
|
md.Set("x-trace-id", traceID)
|
||||||
|
} else {
|
||||||
|
traceID = ids[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = metadata.NewIncomingContext(ctx, md)
|
||||||
|
ctx = context.WithValue(ctx, "traceID", traceID)
|
||||||
|
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
45
internal/infrastructure/grpc/server.go
Normal file
45
internal/infrastructure/grpc/server.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
pb "github.com/teacinema/contracts/gen/go/media"
|
||||||
|
"github.com/teacinema/media-service/internal/application/usecases"
|
||||||
|
"github.com/teacinema/media-service/internal/config"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/images"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/storage"
|
||||||
|
handler "github.com/teacinema/media-service/internal/interfaces/grpc"
|
||||||
|
"github.com/teacinema/media-service/pkg/logger"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewServer(storage storage.Storage, cfg *config.Config) *grpc.Server {
|
||||||
|
server := grpc.NewServer(
|
||||||
|
grpc.ChainUnaryInterceptor(
|
||||||
|
RequestLoggerInterceptor,
|
||||||
|
TraceIDInterceptor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
uploadUC := usecases.NewUploadUseCase(storage, images.NewImageProcessor())
|
||||||
|
getUC := usecases.NewGetUseCase(storage)
|
||||||
|
deleteUC := usecases.NewDeleteUseCase(storage)
|
||||||
|
|
||||||
|
h := handler.NewMediaHandler(uploadUC, getUC, deleteUC)
|
||||||
|
|
||||||
|
pb.RegisterMediaServiceServer(server, h)
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartGRPC(server *grpc.Server, port int) error {
|
||||||
|
addr := fmt.Sprintf(":%d", port)
|
||||||
|
lis, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("gRPC server listening on %s", addr)
|
||||||
|
return server.Serve(lis)
|
||||||
|
}
|
||||||
81
internal/infrastructure/http/server.go
Normal file
81
internal/infrastructure/http/server.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/teacinema/media-service/internal/config"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/images"
|
||||||
|
"github.com/teacinema/media-service/internal/infrastructure/storage"
|
||||||
|
"github.com/teacinema/media-service/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
engine *gin.Engine
|
||||||
|
storage storage.Storage
|
||||||
|
cfg *config.Config
|
||||||
|
processor images.Processor
|
||||||
|
httpSrv *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(s storage.Storage, cfg *config.Config) *Server {
|
||||||
|
if cfg.App.Env == "production" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(gin.Logger())
|
||||||
|
r.Use(gin.Recovery())
|
||||||
|
|
||||||
|
srv := &Server{
|
||||||
|
engine: r,
|
||||||
|
storage: s,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/*key", srv.getMediaHandler)
|
||||||
|
|
||||||
|
srv.httpSrv = &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%s", cfg.HTTP.Port),
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
return s.httpSrv.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop(ctx context.Context) error {
|
||||||
|
return s.httpSrv.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getMediaHandler(c *gin.Context) {
|
||||||
|
key := c.Param("key")[1:]
|
||||||
|
|
||||||
|
obj, contentType, err := s.storage.GetStream(c, key)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusNotFound, "file not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer obj.Close()
|
||||||
|
|
||||||
|
img, err := io.ReadAll(obj)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed read: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "error reading file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mime := mimetype.Detect(img)
|
||||||
|
|
||||||
|
c.Header("Content-Type", contentType)
|
||||||
|
c.Header("Cache-Control", "public, max-age=86400")
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, mime.String(), img)
|
||||||
|
}
|
||||||
28
internal/infrastructure/images/processor.go
Normal file
28
internal/infrastructure/images/processor.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResizeOptions struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Crop bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Processor interface {
|
||||||
|
Process(input io.Reader, opts *ResizeOptions) (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoopProcessor struct{}
|
||||||
|
|
||||||
|
func NewImageProcessor() *NoopProcessor {
|
||||||
|
return &NoopProcessor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NoopProcessor) Process(
|
||||||
|
input io.Reader,
|
||||||
|
opts *ResizeOptions,
|
||||||
|
) (io.Reader, error) {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
187
internal/infrastructure/storage/s3.go
Normal file
187
internal/infrastructure/storage/s3.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
awsConfig "github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
|
"github.com/teacinema/media-service/internal/config"
|
||||||
|
"github.com/teacinema/media-service/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3Storage struct {
|
||||||
|
client *s3.Client
|
||||||
|
uploader *manager.Uploader
|
||||||
|
downloader *manager.Downloader
|
||||||
|
bucket string
|
||||||
|
cfg *config.Config
|
||||||
|
presigner *s3.PresignClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3Storage(c *config.Config) (*S3Storage, error) {
|
||||||
|
var loadOpts []func(*awsConfig.LoadOptions) error
|
||||||
|
if c.Storage.Region != "" {
|
||||||
|
loadOpts = append(loadOpts, awsConfig.WithRegion(c.Storage.Region))
|
||||||
|
}
|
||||||
|
if c.Storage.AccessKey != "" && c.Storage.SecretKey != "" {
|
||||||
|
loadOpts = append(loadOpts, awsConfig.WithCredentialsProvider(
|
||||||
|
credentials.NewStaticCredentialsProvider(c.Storage.AccessKey, c.Storage.SecretKey, ""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
awsCfg, err := awsConfig.LoadDefaultConfig(context.Background(), loadOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load aws config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientOpts []func(*s3.Options)
|
||||||
|
if strings.TrimSpace(c.Storage.Endpoint) != "" {
|
||||||
|
ep := c.Storage.Endpoint
|
||||||
|
|
||||||
|
clientOpts = append(clientOpts, func(o *s3.Options) {
|
||||||
|
o.UsePathStyle = true
|
||||||
|
o.BaseEndpoint = aws.String(ep)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
client := s3.NewFromConfig(awsCfg, clientOpts...)
|
||||||
|
uploader := manager.NewUploader(client)
|
||||||
|
downloader := manager.NewDownloader(client)
|
||||||
|
presigner := s3.NewPresignClient(client)
|
||||||
|
|
||||||
|
s := &S3Storage{
|
||||||
|
client: client,
|
||||||
|
uploader: uploader,
|
||||||
|
downloader: downloader,
|
||||||
|
bucket: c.Storage.Bucket,
|
||||||
|
cfg: c,
|
||||||
|
presigner: presigner,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
_, headErr := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(s.bucket)})
|
||||||
|
if headErr != nil {
|
||||||
|
_, createErr := s.client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
|
||||||
|
CreateBucketConfiguration: &s3Types.CreateBucketConfiguration{
|
||||||
|
LocationConstraint: s3Types.BucketLocationConstraint(aws.ToString(&c.Storage.Region)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if createErr != nil {
|
||||||
|
return nil, fmt.Errorf("create bucket: %w (head err: %v)", createErr, headErr)
|
||||||
|
}
|
||||||
|
logger.Info("🪣 Created S3 bucket: %s", s.bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("✅ Connected to S3 bucket: %s (region=%s)", s.bucket, c.Storage.Region)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) UploadStream(
|
||||||
|
ctx context.Context,
|
||||||
|
key string,
|
||||||
|
reader io.Reader,
|
||||||
|
contentType string,
|
||||||
|
) error {
|
||||||
|
_, err := s.uploader.Upload(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
Body: reader,
|
||||||
|
ContentType: aws.String(contentType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("s3 upload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) GetStream(
|
||||||
|
ctx context.Context,
|
||||||
|
key string,
|
||||||
|
) (io.ReadCloser, string, error) {
|
||||||
|
out, err := s.client.GetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("s3 get: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := ""
|
||||||
|
if out.ContentType != nil {
|
||||||
|
contentType = *out.ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Body, contentType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) Delete(ctx context.Context, key string) error {
|
||||||
|
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("s3 delete: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) GetPublicURL(key string) string {
|
||||||
|
host := s.cfg.HTTP.Host
|
||||||
|
|
||||||
|
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
|
||||||
|
host = "http://" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s", strings.TrimRight(host, "/"), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) GetPresignedURL(ctx context.Context, key string, expire time.Duration, method string) (string, error) {
|
||||||
|
switch strings.ToUpper(method) {
|
||||||
|
case "GET":
|
||||||
|
ps, err := s.presigner.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
}, s3.WithPresignExpires(expire))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("presign GET: %w", err)
|
||||||
|
}
|
||||||
|
return ps.URL, nil
|
||||||
|
case "PUT":
|
||||||
|
ps, err := s.presigner.PresignPutObject(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
}, s3.WithPresignExpires(expire))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("presign PUT: %w", err)
|
||||||
|
}
|
||||||
|
return ps.URL, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported method for presign: %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureEndpointURL(ep string, ssl bool) string {
|
||||||
|
ep = strings.TrimSpace(ep)
|
||||||
|
if strings.HasPrefix(ep, "http://") || strings.HasPrefix(ep, "https://") {
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
if ssl {
|
||||||
|
return "https://" + ep
|
||||||
|
}
|
||||||
|
return "http://" + ep
|
||||||
|
}
|
||||||
29
internal/infrastructure/storage/storage.go
Normal file
29
internal/infrastructure/storage/storage.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
URL string
|
||||||
|
Size int64
|
||||||
|
MIMEType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
UploadStream(
|
||||||
|
ctx context.Context,
|
||||||
|
key string,
|
||||||
|
reader io.Reader,
|
||||||
|
contentType string,
|
||||||
|
) error
|
||||||
|
|
||||||
|
GetStream(
|
||||||
|
ctx context.Context,
|
||||||
|
key string,
|
||||||
|
) (io.ReadCloser, string, error)
|
||||||
|
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
50
internal/interfaces/grpc/media_handler.go
Normal file
50
internal/interfaces/grpc/media_handler.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
pb "github.com/teacinema/contracts/gen/go/media"
|
||||||
|
"github.com/teacinema/media-service/internal/application/dto"
|
||||||
|
"github.com/teacinema/media-service/internal/application/usecases"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaHandler struct {
|
||||||
|
pb.UnimplementedMediaServiceServer
|
||||||
|
uploadUC *usecases.UploadUseCase
|
||||||
|
getUC *usecases.GetUseCase
|
||||||
|
deleteUC *usecases.DeleteUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaHandler(
|
||||||
|
u *usecases.UploadUseCase,
|
||||||
|
g *usecases.GetUseCase,
|
||||||
|
d *usecases.DeleteUseCase,
|
||||||
|
) *MediaHandler {
|
||||||
|
return &MediaHandler{
|
||||||
|
uploadUC: u,
|
||||||
|
getUC: g,
|
||||||
|
deleteUC: d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MediaHandler) Upload(
|
||||||
|
ctx context.Context,
|
||||||
|
req *pb.UploadRequest,
|
||||||
|
) (*pb.UploadResponse, error) {
|
||||||
|
res, err := h.uploadUC.Execute(ctx, dto.UploadMediaRequest{
|
||||||
|
FileName: req.FileName,
|
||||||
|
Folder: req.Folder,
|
||||||
|
ContentType: req.ContentType,
|
||||||
|
Reader: bytes.NewReader(req.Data),
|
||||||
|
Size: int64(len(req.Data)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.UploadResponse{
|
||||||
|
Key: res.Key,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
53
pkg/logger/logger.go
Normal file
53
pkg/logger/logger.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var level = "info"
|
||||||
|
|
||||||
|
func Init(lvl string) {
|
||||||
|
level = strings.ToLower(lvl)
|
||||||
|
|
||||||
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
|
log.Printf("[LOGGER] initialized with level: %s", level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(format string, v ...any) {
|
||||||
|
if levelAllowed("info") {
|
||||||
|
log.Printf("[INFO] "+format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(format string, v ...any) {
|
||||||
|
if levelAllowed("warn") {
|
||||||
|
log.Printf("[WARN] "+format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(format string, v ...any) {
|
||||||
|
if levelAllowed("error") {
|
||||||
|
log.Printf("[ERROR] "+format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(format string, v ...any) {
|
||||||
|
log.Printf("[FATAL] "+format, v...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
func levelAllowed(l string) bool {
|
||||||
|
levels := map[string]int{
|
||||||
|
"debug": 1,
|
||||||
|
"info": 2,
|
||||||
|
"warn": 3,
|
||||||
|
"error": 4,
|
||||||
|
"fatal": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels[strings.ToLower(level)] <= levels[l]
|
||||||
|
}
|
||||||
10
scripts/run_dev.sh
Normal file
10
scripts/run_dev.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
echo "Starting media-service in dev mode..."
|
||||||
|
|
||||||
|
# Source .env if exists
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
fi
|
||||||
|
|
||||||
|
air
|
||||||
Loading…
x
Reference in New Issue
Block a user