UPDATE: Media module
All checks were successful
Build and Release / release (push) Successful in 1m7s
All checks were successful
Build and Release / release (push) Successful in 1m7s
This commit is contained in:
@@ -11,6 +11,7 @@ COPY . .
|
|||||||
|
|
||||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o history-api ./cmd/api
|
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o history-api ./cmd/api
|
||||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o email-worker ./cmd/worker/email
|
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o email-worker ./cmd/worker/email
|
||||||
|
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o storage-worker ./cmd/worker/storage
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
@@ -21,9 +22,10 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY --from=builder /app/history-api .
|
COPY --from=builder /app/history-api .
|
||||||
COPY --from=builder /app/email-worker .
|
COPY --from=builder /app/email-worker .
|
||||||
|
COPY --from=builder /app/storage-worker .
|
||||||
COPY data ./data
|
COPY data ./data
|
||||||
|
|
||||||
RUN chmod +x ./history-api ./email-worker
|
RUN chmod +x ./history-api ./email-worker ./storage-worker
|
||||||
|
|
||||||
EXPOSE 3344
|
EXPOSE 3344
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
_ "history-api/pkg/log"
|
_ "history-api/pkg/log"
|
||||||
"history-api/pkg/mbtiles"
|
"history-api/pkg/mbtiles"
|
||||||
"history-api/pkg/oauth"
|
"history-api/pkg/oauth"
|
||||||
|
"history-api/pkg/storage"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -72,6 +73,12 @@ func StartServer() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storageClient, err := storage.NewS3Storage()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg(err.Error())
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
googleOAuthConfig, err := oauth.NewGoogleProvider()
|
googleOAuthConfig, err := oauth.NewGoogleProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg(err.Error())
|
log.Error().Msg(err.Error())
|
||||||
@@ -89,7 +96,7 @@ func StartServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverHttp := NewHttpServer()
|
serverHttp := NewHttpServer()
|
||||||
serverHttp.SetupServer(poolPg, sqlTile, redisClient, googleOAuthConfig)
|
serverHttp.SetupServer(poolPg, sqlTile, redisClient, storageClient, googleOAuthConfig)
|
||||||
Singleton = serverHttp
|
Singleton = serverHttp
|
||||||
|
|
||||||
done := make(chan bool, 1)
|
done := make(chan bool, 1)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"history-api/internal/routes"
|
"history-api/internal/routes"
|
||||||
"history-api/internal/services"
|
"history-api/internal/services"
|
||||||
"history-api/pkg/cache"
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/storage"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ func NewHttpServer() *FiberServer {
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.Cache, oauth *oauth2.Config) {
|
func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.Cache, sclient storage.Storage, oauth *oauth2.Config) {
|
||||||
// Apply CORS middleware
|
// Apply CORS middleware
|
||||||
s.App.Use(cors.New(cors.Config{
|
s.App.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{
|
AllowOrigins: []string{
|
||||||
@@ -75,22 +76,26 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.
|
|||||||
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
||||||
tileRepo := repositories.NewTileRepository(sqlTile, redis)
|
tileRepo := repositories.NewTileRepository(sqlTile, redis)
|
||||||
tokenRepo := repositories.NewTokenRepository(redis)
|
tokenRepo := repositories.NewTokenRepository(redis)
|
||||||
|
mediaRepo := repositories.NewMediaRepository(sqlPg, redis)
|
||||||
|
|
||||||
// service setup
|
// service setup
|
||||||
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis)
|
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis)
|
||||||
userService := services.NewUserService(userRepo, roleRepo)
|
userService := services.NewUserService(userRepo, roleRepo)
|
||||||
roleService := services.NewRoleService(roleRepo)
|
roleService := services.NewRoleService(roleRepo)
|
||||||
tileService := services.NewTileService(tileRepo)
|
tileService := services.NewTileService(tileRepo)
|
||||||
|
mediaService := services.NewMediaService(mediaRepo, tokenRepo, sclient, redis)
|
||||||
|
|
||||||
// controller setup
|
// controller setup
|
||||||
authController := controllers.NewAuthController(authService, oauth)
|
authController := controllers.NewAuthController(authService, oauth)
|
||||||
userController := controllers.NewUserController(userService)
|
userController := controllers.NewUserController(userService, mediaService)
|
||||||
tileController := controllers.NewTileController(tileService)
|
tileController := controllers.NewTileController(tileService)
|
||||||
roleController := controllers.NewRoleController(roleService)
|
roleController := controllers.NewRoleController(roleService)
|
||||||
|
mediaController := controllers.NewMediaController(mediaService)
|
||||||
|
|
||||||
// route setup
|
// route setup
|
||||||
routes.AuthRoutes(s.App, authController, userRepo)
|
routes.AuthRoutes(s.App, authController, userRepo)
|
||||||
routes.UserRoutes(s.App, userController, userRepo)
|
routes.UserRoutes(s.App, userController, userRepo)
|
||||||
|
routes.MediaRoutes(s.App, mediaController, userRepo)
|
||||||
routes.RoleRoutes(s.App, roleController, userRepo)
|
routes.RoleRoutes(s.App, roleController, userRepo)
|
||||||
routes.TileRoutes(s.App, tileController)
|
routes.TileRoutes(s.App, tileController)
|
||||||
routes.NotFoundRoute(s.App)
|
routes.NotFoundRoute(s.App)
|
||||||
|
|||||||
119
cmd/worker/storage/main.go
Normal file
119
cmd/worker/storage/main.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"history-api/internal/models"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/config"
|
||||||
|
"history-api/pkg/constants"
|
||||||
|
_ "history-api/pkg/log"
|
||||||
|
"history-api/pkg/storage"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runSingleWorker(ctx context.Context, rdb *redis.Client, consumerID int, sc storage.Storage) {
|
||||||
|
consumerName := "worker-" + strconv.Itoa(consumerID)
|
||||||
|
|
||||||
|
log.Info().Str("worker", consumerName).Msg("Worker started and ready")
|
||||||
|
|
||||||
|
for {
|
||||||
|
entries, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||||||
|
Group: constants.GroupStorageName,
|
||||||
|
Consumer: consumerName,
|
||||||
|
Streams: []string{constants.StreamStorageName, ">"},
|
||||||
|
Count: 1,
|
||||||
|
Block: 0,
|
||||||
|
}).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("worker", consumerName).Msg("Failed to read stream")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stream := range entries {
|
||||||
|
for _, message := range stream.Messages {
|
||||||
|
taskType := message.Values["task_type"].(string)
|
||||||
|
payloadStr := message.Values["payload"].(string)
|
||||||
|
|
||||||
|
if taskType == constants.TaskTypeDeleteMedia.String() {
|
||||||
|
var data models.MediaStorageEntity
|
||||||
|
if err := json.Unmarshal([]byte(payloadStr), &data); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to unmarshal payload")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("worker", consumerName).
|
||||||
|
Str("storage_key", data.StorageKey).
|
||||||
|
Msg("Processing delete media task")
|
||||||
|
|
||||||
|
errSend := sc.Delete(ctx, data.StorageKey)
|
||||||
|
if errSend != nil {
|
||||||
|
log.Error().Err(errSend).Str("storage_key", data.StorageKey).Msg("Failed to delete media")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rdb.XAck(ctx, constants.StreamStorageName, constants.GroupStorageName, message.ID)
|
||||||
|
log.Info().Str("msg_id", message.ID).Msg("Task acknowledged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
config.LoadEnv()
|
||||||
|
|
||||||
|
workerCountStr := config.GetConfigWithDefault("STORAGE_WORKER_COUNT", "1")
|
||||||
|
workerCount, err := strconv.Atoi(workerCountStr)
|
||||||
|
if err != nil || workerCount <= 0 {
|
||||||
|
workerCount = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheInterface, err := cache.NewRedisClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to connect to Redis")
|
||||||
|
}
|
||||||
|
|
||||||
|
rdb := cacheInterface.GetRawClient()
|
||||||
|
|
||||||
|
sc, err := storage.NewS3Storage()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to create S3 storage client")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err = rdb.XGroupCreateMkStream(ctx, constants.StreamStorageName, constants.GroupStorageName, "$").Err()
|
||||||
|
if err != nil && err.Error() != "BUSYGROUP Consumer Group name already exists" {
|
||||||
|
log.Fatal().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to create Redis Stream Group")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("worker_count", workerCount).
|
||||||
|
Msg("Starting storage worker system")
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 1; i <= workerCount; i++ {
|
||||||
|
wg.Go(func() {
|
||||||
|
runSingleWorker(ctx, rdb, i, sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@ services:
|
|||||||
image: redis:8.6.2-alpine
|
image: redis:8.6.2-alpine
|
||||||
container_name: history_redis
|
container_name: history_redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- history-api-project
|
||||||
|
|
||||||
@@ -79,9 +82,9 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- history-api-project
|
||||||
|
|
||||||
worker:
|
email_worker:
|
||||||
build: .
|
build: .
|
||||||
container_name: history_worker
|
container_name: history_email_worker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -94,8 +97,24 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- history-api-project
|
||||||
|
|
||||||
|
storage_worker:
|
||||||
|
build: .
|
||||||
|
container_name: history_storage_worker
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
cache:
|
||||||
|
condition: service_started
|
||||||
|
env_file:
|
||||||
|
- ./assets/resources/.env
|
||||||
|
command: ["./storage-worker"]
|
||||||
|
networks:
|
||||||
|
- history-api-project
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
history-api-project:
|
history-api-project:
|
||||||
387
docs/docs.go
387
docs/docs.go
@@ -362,6 +362,294 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/media": {
|
||||||
|
"get": {
|
||||||
|
"description": "Search media with filters, pagination",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Search media",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Items per page",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search keyword",
|
||||||
|
"name": "keyword",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.PaginatedResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/presigned": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Generate a presigned URL for direct upload to storage",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Generate presigned URL",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "File name",
|
||||||
|
"name": "filename",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Content type",
|
||||||
|
"name": "contentType",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/presigned/complete": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Confirm that upload via presigned URL is completed",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Confirm presigned upload",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Storage key",
|
||||||
|
"name": "key",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/upload": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Upload media file through server",
|
||||||
|
"consumes": [
|
||||||
|
"multipart/form-data"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Upload media (server-side)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"description": "Upload file",
|
||||||
|
"name": "file",
|
||||||
|
"in": "formData",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve a media file by its ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Get media by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Media ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Delete a media file by ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Delete media",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Media ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/roles": {
|
"/roles": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -601,7 +889,7 @@ const docTemplate = `{
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
"$ref": "#/definitions/history-api_internal_dtos_response.PaginatedResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -653,6 +941,40 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/current/media": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve media list of the currently authenticated user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get current user's media",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{id}": {
|
"/users/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -793,6 +1115,44 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/{id}/media": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve media list by specific user ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get user's media by user ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{id}/password": {
|
"/users/{id}/password": {
|
||||||
"patch": {
|
"patch": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1166,6 +1526,29 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"history-api_internal_dtos_response.PaginatedResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"has_more": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"next_cursor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"history-api_pkg_constants.TokenType": {
|
"history-api_pkg_constants.TokenType": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
@@ -1179,7 +1562,7 @@ const docTemplate = `{
|
|||||||
"TokenPasswordReset",
|
"TokenPasswordReset",
|
||||||
"TokenEmailVerify",
|
"TokenEmailVerify",
|
||||||
"TokenMagicLink",
|
"TokenMagicLink",
|
||||||
"TokenRefreshToken"
|
"TokenUpload"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -355,6 +355,294 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/media": {
|
||||||
|
"get": {
|
||||||
|
"description": "Search media with filters, pagination",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Search media",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Items per page",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search keyword",
|
||||||
|
"name": "keyword",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.PaginatedResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/presigned": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Generate a presigned URL for direct upload to storage",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Generate presigned URL",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "File name",
|
||||||
|
"name": "filename",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Content type",
|
||||||
|
"name": "contentType",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/presigned/complete": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Confirm that upload via presigned URL is completed",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Confirm presigned upload",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Storage key",
|
||||||
|
"name": "key",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/upload": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Upload media file through server",
|
||||||
|
"consumes": [
|
||||||
|
"multipart/form-data"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Upload media (server-side)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"description": "Upload file",
|
||||||
|
"name": "file",
|
||||||
|
"in": "formData",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/media/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve a media file by its ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Get media by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Media ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Delete a media file by ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Media"
|
||||||
|
],
|
||||||
|
"summary": "Delete media",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Media ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/roles": {
|
"/roles": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -594,7 +882,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
"$ref": "#/definitions/history-api_internal_dtos_response.PaginatedResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -646,6 +934,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/current/media": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve media list of the currently authenticated user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get current user's media",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{id}": {
|
"/users/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -786,6 +1108,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/{id}/media": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve media list by specific user ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get user's media by user ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{id}/password": {
|
"/users/{id}/password": {
|
||||||
"patch": {
|
"patch": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1159,6 +1519,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"history-api_internal_dtos_response.PaginatedResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"has_more": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"next_cursor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"history-api_pkg_constants.TokenType": {
|
"history-api_pkg_constants.TokenType": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
@@ -1172,7 +1555,7 @@
|
|||||||
"TokenPasswordReset",
|
"TokenPasswordReset",
|
||||||
"TokenEmailVerify",
|
"TokenEmailVerify",
|
||||||
"TokenMagicLink",
|
"TokenMagicLink",
|
||||||
"TokenRefreshToken"
|
"TokenUpload"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -150,6 +150,21 @@ definitions:
|
|||||||
status:
|
status:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
history-api_internal_dtos_response.PaginatedResponse:
|
||||||
|
properties:
|
||||||
|
data: {}
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
pagination:
|
||||||
|
properties:
|
||||||
|
has_more:
|
||||||
|
type: boolean
|
||||||
|
next_cursor:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
history-api_pkg_constants.TokenType:
|
history-api_pkg_constants.TokenType:
|
||||||
enum:
|
enum:
|
||||||
- 1
|
- 1
|
||||||
@@ -162,7 +177,7 @@ definitions:
|
|||||||
- TokenPasswordReset
|
- TokenPasswordReset
|
||||||
- TokenEmailVerify
|
- TokenEmailVerify
|
||||||
- TokenMagicLink
|
- TokenMagicLink
|
||||||
- TokenRefreshToken
|
- TokenUpload
|
||||||
info:
|
info:
|
||||||
contact:
|
contact:
|
||||||
email: support@swagger.io
|
email: support@swagger.io
|
||||||
@@ -398,6 +413,191 @@ paths:
|
|||||||
summary: Verify a security token
|
summary: Verify a security token
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
|
/media:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Search media with filters, pagination
|
||||||
|
parameters:
|
||||||
|
- description: Page number
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: Items per page
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- description: Search keyword
|
||||||
|
in: query
|
||||||
|
name: keyword
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.PaginatedResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Search media
|
||||||
|
tags:
|
||||||
|
- Media
|
||||||
|
/media/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Delete a media file by ID
|
||||||
|
parameters:
|
||||||
|
- description: Media ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Delete media
|
||||||
|
tags:
|
||||||
|
- Media
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve a media file by its ID
|
||||||
|
parameters:
|
||||||
|
- description: Media ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Get media by ID
|
||||||
|
tags:
|
||||||
|
- Media
|
||||||
|
/media/presigned:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Generate a presigned URL for direct upload to storage
|
||||||
|
parameters:
|
||||||
|
- description: File name
|
||||||
|
in: query
|
||||||
|
name: filename
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Content type
|
||||||
|
in: query
|
||||||
|
name: contentType
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Generate presigned URL
|
||||||
|
tags:
|
||||||
|
- Media
|
||||||
|
/media/presigned/complete:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Confirm that upload via presigned URL is completed
|
||||||
|
parameters:
|
||||||
|
- description: Storage key
|
||||||
|
in: query
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Confirm presigned upload
|
||||||
|
tags:
|
||||||
|
- Media
|
||||||
|
/media/upload:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- multipart/form-data
|
||||||
|
description: Upload media file through server
|
||||||
|
parameters:
|
||||||
|
- description: Upload file
|
||||||
|
in: formData
|
||||||
|
name: file
|
||||||
|
required: true
|
||||||
|
type: file
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Upload media (server-side)
|
||||||
|
tags:
|
||||||
|
- Media
|
||||||
/roles:
|
/roles:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -551,7 +751,7 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
$ref: '#/definitions/history-api_internal_dtos_response.PaginatedResponse'
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
@@ -654,6 +854,31 @@ paths:
|
|||||||
summary: Update user profile
|
summary: Update user profile
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/users/{id}/media:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve media list by specific user ID
|
||||||
|
parameters:
|
||||||
|
- description: User ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Get user's media by user ID
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/users/{id}/password:
|
/users/{id}/password:
|
||||||
patch:
|
patch:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -777,6 +1002,27 @@ paths:
|
|||||||
summary: Get current user profile
|
summary: Get current user profile
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/users/current/media:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve media list of the currently authenticated user
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Get current user's media
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
BearerAuth:
|
BearerAuth:
|
||||||
description: Type "Bearer " followed by a space and JWT token.
|
description: Type "Bearer " followed by a space and JWT token.
|
||||||
|
|||||||
230
internal/controllers/mediaController.go
Normal file
230
internal/controllers/mediaController.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"history-api/internal/dtos/request"
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/services"
|
||||||
|
"history-api/pkg/validator"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaController struct {
|
||||||
|
service services.MediaService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaController(svc services.MediaService) *MediaController {
|
||||||
|
return &MediaController{service: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaByID godoc
|
||||||
|
// @Summary Get media by ID
|
||||||
|
// @Description Retrieve a media file by its ID
|
||||||
|
// @Tags Media
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Media ID"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /media/{id} [get]
|
||||||
|
func (m *MediaController) GetMediaByID(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
mediaId := c.Params("id")
|
||||||
|
res, err := m.service.GetMediaByID(ctx, mediaId)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchMedia godoc
|
||||||
|
// @Summary Search media
|
||||||
|
// @Description Search media with filters, pagination
|
||||||
|
// @Tags Media
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "Page number"
|
||||||
|
// @Param limit query int false "Items per page"
|
||||||
|
// @Param keyword query string false "Search keyword"
|
||||||
|
// @Success 200 {object} response.PaginatedResponse
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /media [get]
|
||||||
|
func (m *MediaController) SearchMedia(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dto := &request.SearchMediaDto{}
|
||||||
|
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res, err := m.service.SearchMedia(ctx, dto)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMedia godoc
|
||||||
|
// @Summary Delete media
|
||||||
|
// @Description Delete a media file by ID
|
||||||
|
// @Tags Media
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path string true "Media ID"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /media/{id} [delete]
|
||||||
|
func (m *MediaController) DeleteMedia(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
claimsVal := c.Locals("user_claims")
|
||||||
|
if claimsVal == nil {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: "Unauthorized",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := claimsVal.(*response.JWTClaims)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: "Invalid user claims",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaId := c.Params("id")
|
||||||
|
err := m.service.DeleteMedia(ctx, claims, mediaId)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Message: "Media deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadServerSide godoc
|
||||||
|
// @Summary Upload media (server-side)
|
||||||
|
// @Description Upload media file through server
|
||||||
|
// @Tags Media
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param file formData file true "Upload file"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /media/upload [post]
|
||||||
|
func (m *MediaController) UploadServerSide(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: "File is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := m.service.UploadServerSide(ctx, c.Locals("uid").(string), fileHeader)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: url,
|
||||||
|
Message: "Media uploaded successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePresignedURL godoc
|
||||||
|
// @Summary Generate presigned URL
|
||||||
|
// @Description Generate a presigned URL for direct upload to storage
|
||||||
|
// @Tags Media
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param filename query string true "File name"
|
||||||
|
// @Param contentType query string true "Content type"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /media/presigned [get]
|
||||||
|
func (m *MediaController) GeneratePresignedURL(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dto := &request.PreSignedDto{}
|
||||||
|
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res, err := m.service.GeneratePresignedURL(ctx, c.Locals("uid").(string), dto)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreSignedCompleted godoc
|
||||||
|
// @Summary Confirm presigned upload
|
||||||
|
// @Description Confirm that upload via presigned URL is completed
|
||||||
|
// @Tags Media
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param key query string true "Storage key"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /media/presigned/complete [post]
|
||||||
|
func (m *MediaController) PreSignedCompleted(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dto := &request.PreSignedCompleteDto{}
|
||||||
|
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res, err := m.service.PreSignedCompleted(ctx, c.Locals("uid").(string), dto)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(res)
|
||||||
|
}
|
||||||
@@ -13,10 +13,14 @@ import (
|
|||||||
|
|
||||||
type UserController struct {
|
type UserController struct {
|
||||||
service services.UserService
|
service services.UserService
|
||||||
|
mediaService services.MediaService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserController(svc services.UserService) *UserController {
|
func NewUserController(svc services.UserService, mediaSvc services.MediaService) *UserController {
|
||||||
return &UserController{service: svc}
|
return &UserController{
|
||||||
|
service: svc,
|
||||||
|
mediaService: mediaSvc,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserCurrent godoc
|
// GetUserCurrent godoc
|
||||||
@@ -47,6 +51,61 @@ func (h *UserController) GetUserCurrent(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserMedia godoc
|
||||||
|
// @Summary Get current user's media
|
||||||
|
// @Description Retrieve media list of the currently authenticated user
|
||||||
|
// @Tags Users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /users/current/media [get]
|
||||||
|
func (h *UserController) GetUserMedia(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res, err := h.mediaService.GetMediaByUserID(ctx, c.Locals("uid").(string))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaByUserID godoc
|
||||||
|
// @Summary Get user's media by user ID
|
||||||
|
// @Description Retrieve media list by specific user ID
|
||||||
|
// @Tags Users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "User ID"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /users/{id}/media [get]
|
||||||
|
func (h *UserController) GetMediaByUserID(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
userId := c.Params("id")
|
||||||
|
res, err := h.mediaService.GetMediaByUserID(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateProfile godoc
|
// UpdateProfile godoc
|
||||||
// @Summary Update user profile
|
// @Summary Update user profile
|
||||||
// @Description Update the profile details of the currently authenticated user
|
// @Description Update the profile details of the currently authenticated user
|
||||||
@@ -250,11 +309,11 @@ func (h *UserController) GetUserById(c fiber.Ctx) error {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Param query query request.SearchUserDto false "Search Query"
|
// @Param query query request.SearchUserDto false "Search Query"
|
||||||
// @Success 200 {object} response.CommonResponse
|
// @Success 200 {object} response.PaginatedResponse
|
||||||
// @Failure 400 {object} response.CommonResponse
|
// @Failure 400 {object} response.CommonResponse
|
||||||
// @Failure 500 {object} response.CommonResponse
|
// @Failure 500 {object} response.CommonResponse
|
||||||
// @Router /users [get]
|
// @Router /users [get]
|
||||||
func (h *UserController) Search(c fiber.Ctx) error {
|
func (h *UserController) SearchUser(c fiber.Ctx) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -265,7 +324,7 @@ func (h *UserController) Search(c fiber.Ctx) error {
|
|||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
res, err := h.service.Search(ctx, dto)
|
res, err := h.service.SearchUser(ctx, dto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
Status: false,
|
Status: false,
|
||||||
|
|||||||
@@ -3,19 +3,14 @@ package request
|
|||||||
type PreSignedDto struct {
|
type PreSignedDto struct {
|
||||||
FileName string `json:"fileName" validate:"required"`
|
FileName string `json:"fileName" validate:"required"`
|
||||||
ContentType string `json:"contentType" validate:"required"`
|
ContentType string `json:"contentType" validate:"required"`
|
||||||
|
Size int64 `json:"size" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreSignedCompleteDto struct {
|
type PreSignedCompleteDto struct {
|
||||||
FileName string `json:"fileName" validate:"required"`
|
TokenID string `json:"token_id" validate:"required"`
|
||||||
MediaId string `json:"mediaId" validate:"required"`
|
|
||||||
PublicUrl string `json:"publicUrl" validate:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchMediaDto struct {
|
type SearchMediaDto struct {
|
||||||
MediaId string `query:"media_id" validate:"omitempty"`
|
CursorPaginationDto
|
||||||
FileName string `query:"file_name" validate:"omitempty"`
|
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
|
||||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
|
||||||
Order string `query:"order" default:"desc" validate:"oneof=asc desc"`
|
|
||||||
Page int `query:"page" default:"1" validate:"min=1"`
|
|
||||||
Limit int `query:"limit" default:"10" validate:"min=1,max=100"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package response
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type PreSignedResponse struct {
|
type PreSignedResponse struct {
|
||||||
UploadUrl string `json:"uploadUrl"`
|
TokenID string `json:"token_id"`
|
||||||
PublicUrl string `json:"publicUrl"`
|
UploadUrl string `json:"upload_url"`
|
||||||
FileName string `json:"fileName"`
|
StorageKey string `json:"storage_key"`
|
||||||
MediaId string `json:"mediaId"`
|
SignedHeaders map[string]string `json:"signed_headers"`
|
||||||
SignedHeaders map[string]string `json:"signedHeaders"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaResponse struct {
|
type MediaResponse struct {
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ type MediaEntity struct {
|
|||||||
UpdatedAt *time.Time `json:"updated_at"`
|
UpdatedAt *time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MediaStorageEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
StorageKey string `json:"storage_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e * MediaEntity) ToStorageEntity() *MediaStorageEntity {
|
||||||
|
return &MediaStorageEntity{
|
||||||
|
ID: e.ID,
|
||||||
|
StorageKey: e.StorageKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *MediaEntity) ToResponse() *response.MediaResponse {
|
func (e *MediaEntity) ToResponse() *response.MediaResponse {
|
||||||
return &response.MediaResponse{
|
return &response.MediaResponse{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
@@ -30,3 +42,11 @@ func (e *MediaEntity) ToResponse() *response.MediaResponse {
|
|||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MediaEntitiesToResponse(entities []*MediaEntity) []*response.MediaResponse {
|
||||||
|
responses := make([]*response.MediaResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
responses[i] = entity.ToResponse()
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ type TokenEntity struct {
|
|||||||
TokenType constants.TokenType `json:"token_type"`
|
TokenType constants.TokenType `json:"token_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenUploadEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
StorageKey string `json:"storage_key"`
|
||||||
|
OriginalName string `json:"original_name"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
FileMetadata []byte `json:"file_metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
type OAuthState struct {
|
type OAuthState struct {
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
RedirectURL string `json:"redirect"`
|
RedirectURL string `json:"redirect"`
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ type TokenRepository interface {
|
|||||||
Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error)
|
Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error)
|
||||||
Create(ctx context.Context, token *models.TokenEntity) error
|
Create(ctx context.Context, token *models.TokenEntity) error
|
||||||
Delete(ctx context.Context, email string, tokenType constants.TokenType) error
|
Delete(ctx context.Context, email string, tokenType constants.TokenType) error
|
||||||
|
|
||||||
CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error)
|
CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error)
|
||||||
CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||||
DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||||
|
|
||||||
|
CreateUploadToken(ctx context.Context, userId string, token *models.TokenUploadEntity) error
|
||||||
|
GetUploadToken(ctx context.Context, userId string, id string) (*models.TokenUploadEntity, error)
|
||||||
|
DeleteUploadToken(ctx context.Context, userId string, id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenRepository struct {
|
type tokenRepository struct {
|
||||||
@@ -37,16 +42,38 @@ func (t *tokenRepository) DeleteVerified(ctx context.Context, email string, toke
|
|||||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||||
return t.c.Del(ctx, cacheKey)
|
return t.c.Del(ctx, cacheKey)
|
||||||
}
|
}
|
||||||
|
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
|
||||||
|
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||||
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
|
|
||||||
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
|
||||||
exists, err := t.c.Exists(ctx, cacheKey)
|
exists, err := t.c.Exists(ctx, cacheKey)
|
||||||
return exists, err
|
return exists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
|
func (t *tokenRepository) CreateUploadToken(ctx context.Context, userId string, token *models.TokenUploadEntity) error {
|
||||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, token.ID)
|
||||||
|
err := t.c.Set(ctx, cacheKey, token, constants.TokenUploadDuration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenRepository) GetUploadToken(ctx context.Context, userId string, id string) (*models.TokenUploadEntity, error) {
|
||||||
|
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, id)
|
||||||
|
var token models.TokenUploadEntity
|
||||||
|
err := t.c.Get(ctx, cacheKey, &token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &token, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenRepository) DeleteUploadToken(ctx context.Context, userId string, id string) error {
|
||||||
|
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, id)
|
||||||
|
return t.c.Del(ctx, cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
|
||||||
|
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||||
exists, err := t.c.Exists(ctx, cacheKey)
|
exists, err := t.c.Exists(ctx, cacheKey)
|
||||||
return exists, err
|
return exists, err
|
||||||
}
|
}
|
||||||
|
|||||||
53
internal/routes/mediaRoute.go
Normal file
53
internal/routes/mediaRoute.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/controllers"
|
||||||
|
"history-api/internal/middlewares"
|
||||||
|
"history-api/internal/repositories"
|
||||||
|
"history-api/pkg/constants"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MediaRoutes(app *fiber.App, controller *controllers.MediaController, userRepo repositories.UserRepository) {
|
||||||
|
route := app.Group("/media")
|
||||||
|
route.Get(
|
||||||
|
"/",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
|
controller.SearchMedia,
|
||||||
|
)
|
||||||
|
|
||||||
|
route.Post(
|
||||||
|
"/upload",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
|
controller.UploadServerSide,
|
||||||
|
)
|
||||||
|
|
||||||
|
route.Get(
|
||||||
|
"/presigned",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
controller.GeneratePresignedURL,
|
||||||
|
)
|
||||||
|
|
||||||
|
route.Post(
|
||||||
|
"/presigned/complete",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
controller.GeneratePresignedURL,
|
||||||
|
)
|
||||||
|
|
||||||
|
route.Get(
|
||||||
|
"/:id",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
|
controller.GetMediaByID,
|
||||||
|
)
|
||||||
|
|
||||||
|
route.Delete(
|
||||||
|
"/:id",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
controller.DeleteMedia,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
|||||||
"/",
|
"/",
|
||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
controller.Search,
|
controller.SearchUser,
|
||||||
)
|
)
|
||||||
|
|
||||||
route.Get(
|
route.Get(
|
||||||
@@ -24,11 +24,18 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
|||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
controller.GetUserCurrent,
|
controller.GetUserCurrent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.Get(
|
||||||
|
"/current/media",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
controller.GetUserMedia,
|
||||||
|
)
|
||||||
|
|
||||||
route.Get(
|
route.Get(
|
||||||
"/:id",
|
"/:id",
|
||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
controller.Search,
|
controller.SearchUser,
|
||||||
)
|
)
|
||||||
|
|
||||||
route.Put(
|
route.Put(
|
||||||
@@ -43,6 +50,14 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
|||||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
controller.DeleteUser,
|
controller.DeleteUser,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.Get(
|
||||||
|
"/:id/media",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
|
controller.GetMediaByUserID,
|
||||||
|
)
|
||||||
|
|
||||||
route.Patch(
|
route.Patch(
|
||||||
"/:id/restore",
|
"/:id/restore",
|
||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
|
|||||||
@@ -2,19 +2,35 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"history-api/internal/dtos/request"
|
"history-api/internal/dtos/request"
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/gen/sqlc"
|
||||||
|
"history-api/internal/models"
|
||||||
"history-api/internal/repositories"
|
"history-api/internal/repositories"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/constants"
|
||||||
|
"history-api/pkg/convert"
|
||||||
"history-api/pkg/storage"
|
"history-api/pkg/storage"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaService interface {
|
type MediaService interface {
|
||||||
GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error)
|
GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error)
|
||||||
GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error)
|
GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error)
|
||||||
SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error)
|
SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error)
|
||||||
DeleteMedia(ctx context.Context, mediaId string) error
|
DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) error
|
||||||
GetMediaByTarget(ctx context.Context, targetType string, targetId string) ([]*response.MediaResponse, error)
|
|
||||||
UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error)
|
UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error)
|
||||||
GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error)
|
GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error)
|
||||||
PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error)
|
PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error)
|
||||||
@@ -22,56 +38,292 @@ type MediaService interface {
|
|||||||
|
|
||||||
type mediaService struct {
|
type mediaService struct {
|
||||||
mediaRepo repositories.MediaRepository
|
mediaRepo repositories.MediaRepository
|
||||||
|
tokenRepo repositories.TokenRepository
|
||||||
s storage.Storage
|
s storage.Storage
|
||||||
|
c cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMediaService(
|
func NewMediaService(
|
||||||
mediaRepo repositories.MediaRepository,
|
mediaRepo repositories.MediaRepository,
|
||||||
|
tokenRepo repositories.TokenRepository,
|
||||||
s storage.Storage,
|
s storage.Storage,
|
||||||
|
c cache.Cache,
|
||||||
) MediaService {
|
) MediaService {
|
||||||
return &mediaService{
|
return &mediaService{
|
||||||
mediaRepo: mediaRepo,
|
mediaRepo: mediaRepo,
|
||||||
|
tokenRepo: tokenRepo,
|
||||||
s: s,
|
s: s,
|
||||||
|
c: c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMedia implements [MediaService].
|
func (m *mediaService) DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) error {
|
||||||
func (m *mediaService) DeleteMedia(ctx context.Context, mediaId string) error {
|
mediaIdUUID, err := convert.StringToUUID(mediaId)
|
||||||
panic("unimplemented")
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePresignedURL implements [MediaService].
|
media, err := m.mediaRepo.GetByID(ctx, mediaIdUUID)
|
||||||
func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error) {
|
if err != nil {
|
||||||
panic("unimplemented")
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMediaByID implements [MediaService].
|
shoudDelete := false
|
||||||
func (m *mediaService) GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error) {
|
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) || media.UserID == claims.UId {
|
||||||
panic("unimplemented")
|
shoudDelete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMediaByTarget implements [MediaService].
|
if !shoudDelete {
|
||||||
func (m *mediaService) GetMediaByTarget(ctx context.Context, targetType string, targetId string) ([]*response.MediaResponse, error) {
|
return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this media")
|
||||||
panic("unimplemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMediaByUserID implements [MediaService].
|
err = m.mediaRepo.Delete(ctx, mediaIdUUID)
|
||||||
func (m *mediaService) GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error) {
|
if err != nil {
|
||||||
panic("unimplemented")
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreSignedCompleted implements [MediaService].
|
m.c.PublishTask(ctx, constants.StreamStorageName, constants.TaskTypeDeleteMedia, media.ToStorageEntity())
|
||||||
func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error) {
|
|
||||||
panic("unimplemented")
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaService) GetMediaByID(ctx context.Context, id string) (*response.MediaResponse, error) {
|
||||||
|
mediaId, err := convert.StringToUUID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
media, err := m.mediaRepo.GetByID(ctx, mediaId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return media.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaService) GetMediaByUserID(ctx context.Context, id string) ([]*response.MediaResponse, error) {
|
||||||
|
userId, err := convert.StringToUUID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
medias, err := m.mediaRepo.GetByUserID(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return models.MediaEntitiesToResponse(medias), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchMedia implements [MediaService].
|
|
||||||
func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error) {
|
func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error) {
|
||||||
panic("unimplemented")
|
arg := sqlc.SearchMediasParams{
|
||||||
|
Limit: int32(dto.Limit + 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if dto.Cursor != "" {
|
||||||
|
pgID, err := convert.StringToUUID(dto.Cursor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
|
||||||
|
}
|
||||||
|
arg.Cursor = pgID
|
||||||
|
}
|
||||||
|
|
||||||
|
if dto.Search != "" {
|
||||||
|
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := m.mediaRepo.Search(ctx, arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore := false
|
||||||
|
var nextCursor string
|
||||||
|
|
||||||
|
if len(rows) > dto.Limit {
|
||||||
|
hasMore = true
|
||||||
|
nextCursor = rows[dto.Limit-1].ID
|
||||||
|
rows = rows[:dto.Limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &response.PaginatedResponse{
|
||||||
|
Data: rows,
|
||||||
|
Status: true,
|
||||||
|
Message: "",
|
||||||
|
}
|
||||||
|
res.Pagination.HasMore = hasMore
|
||||||
|
res.Pagination.NextCursor = nextCursor
|
||||||
|
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadServerSide implements [MediaService].
|
|
||||||
func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) {
|
func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) {
|
||||||
panic("unimplemented")
|
userIdUUID, err := convert.StringToUUID(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Cannot open file")
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
var reader io.Reader = file
|
||||||
|
fileExt := filepath.Ext(fileHeader.Filename)
|
||||||
|
contentType := fileHeader.Header.Get("Content-Type")
|
||||||
|
mid, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
|
||||||
|
}
|
||||||
|
newFileName := mid.String() + fileExt
|
||||||
|
originalName := fileHeader.Filename
|
||||||
|
encodedName := url.QueryEscape(originalName)
|
||||||
|
|
||||||
|
dispositionType := "attachment"
|
||||||
|
if strings.HasPrefix(contentType, "image/") || contentType == "application/pdf" {
|
||||||
|
dispositionType = "inline"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
|
||||||
|
dispositionType,
|
||||||
|
"file"+fileExt,
|
||||||
|
encodedName,
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata := map[string]string{
|
||||||
|
"original-name": encodedName,
|
||||||
|
}
|
||||||
|
|
||||||
|
mdByte, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.s.Upload(ctx, newFileName, reader, fileHeader.Size, storage.UploadOptions{
|
||||||
|
ContentType: contentType,
|
||||||
|
ContentDisposition: contentDisposition,
|
||||||
|
Metadata: metadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to upload file to storage")
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload file")
|
||||||
|
}
|
||||||
|
|
||||||
|
media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
|
||||||
|
UserID: userIdUUID,
|
||||||
|
StorageKey: newFileName,
|
||||||
|
OriginalName: originalName,
|
||||||
|
MimeType: contentType,
|
||||||
|
Size: fileHeader.Size,
|
||||||
|
FileMetadata: mdByte,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return media.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error) {
|
||||||
|
fileExt := filepath.Ext(dto.FileName)
|
||||||
|
mid, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
|
||||||
|
}
|
||||||
|
newFileName := mid.String() + fileExt
|
||||||
|
encodedName := url.QueryEscape(dto.FileName)
|
||||||
|
|
||||||
|
dispositionType := "attachment"
|
||||||
|
if dto.ContentType == "application/pdf" || (len(dto.ContentType) > 6 && dto.ContentType[:6] == "image/") {
|
||||||
|
dispositionType = "inline"
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
|
||||||
|
dispositionType, "file"+fileExt, encodedName)
|
||||||
|
|
||||||
|
metadata := map[string]string{
|
||||||
|
"original-name": encodedName,
|
||||||
|
}
|
||||||
|
mdByte, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
presignedURL, err := m.s.PresignUpload(ctx, newFileName, constants.PreSignedURLDuration, storage.UploadOptions{
|
||||||
|
ContentType: dto.ContentType,
|
||||||
|
ContentDisposition: contentDisposition,
|
||||||
|
Metadata: metadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate presigned URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenId := uuid.New().String()
|
||||||
|
err = m.tokenRepo.CreateUploadToken(
|
||||||
|
ctx,
|
||||||
|
userId,
|
||||||
|
&models.TokenUploadEntity{
|
||||||
|
ID: tokenId,
|
||||||
|
UserID: userId,
|
||||||
|
StorageKey: newFileName,
|
||||||
|
OriginalName: dto.FileName,
|
||||||
|
MimeType: dto.ContentType,
|
||||||
|
Size: dto.Size,
|
||||||
|
FileMetadata: mdByte,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.PreSignedResponse{
|
||||||
|
TokenID: tokenId,
|
||||||
|
UploadUrl: presignedURL,
|
||||||
|
StorageKey: newFileName,
|
||||||
|
SignedHeaders: map[string]string{
|
||||||
|
"x-amz-meta-original-name": encodedName,
|
||||||
|
"Content-Disposition": contentDisposition,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error) {
|
||||||
|
token, err := m.tokenRepo.GetUploadToken(ctx, userId, dto.TokenID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get upload token")
|
||||||
|
}
|
||||||
|
if token == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||||
|
}
|
||||||
|
userIdUUID, err := convert.StringToUUID(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.s.Move(
|
||||||
|
ctx,
|
||||||
|
&storage.MoveOptions{
|
||||||
|
Bucket: m.s.GetTempBucket(),
|
||||||
|
Key: token.StorageKey,
|
||||||
|
},
|
||||||
|
&storage.MoveOptions{
|
||||||
|
Bucket: m.s.GetMainBucket(),
|
||||||
|
Key: token.StorageKey,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to move file to final destination")
|
||||||
|
}
|
||||||
|
|
||||||
|
media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
|
||||||
|
UserID: userIdUUID,
|
||||||
|
StorageKey: token.StorageKey,
|
||||||
|
OriginalName: token.OriginalName,
|
||||||
|
MimeType: token.MimeType,
|
||||||
|
Size: token.Size,
|
||||||
|
FileMetadata: token.FileMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create media record")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = m.tokenRepo.DeleteUploadToken(ctx, userId, dto.TokenID)
|
||||||
|
|
||||||
|
return media.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type UserService interface {
|
|||||||
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
||||||
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||||
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||||
Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userService struct {
|
type userService struct {
|
||||||
@@ -208,7 +208,7 @@ func (u *userService) RestoreUser(ctx context.Context, userId string) (*response
|
|||||||
return user.ToResponse(), nil
|
return user.ToResponse(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userService) Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||||
arg := sqlc.SearchUsersParams{
|
arg := sqlc.SearchUsersParams{
|
||||||
Limit: int32(dto.Limit + 1),
|
Limit: int32(dto.Limit + 1),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ package constants
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
StreamEmailName = "stream:email_tasks"
|
StreamEmailName = "stream:email_tasks"
|
||||||
|
StreamStorageName = "stream:storage_tasks"
|
||||||
GroupEmailName = "email_workers_group"
|
GroupEmailName = "email_workers_group"
|
||||||
|
GroupStorageName = "storage_workers_group"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ type TaskType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
TaskTypeSendEmailOTP TaskType = "SEND_EMAIL_OTP"
|
TaskTypeSendEmailOTP TaskType = "SEND_EMAIL_OTP"
|
||||||
|
TaskTypeDeleteMedia TaskType = "DELETE_MEDIA"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t TaskType) String() string {
|
func (t TaskType) String() string {
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ const (
|
|||||||
AccessTokenDuration = 15 * time.Minute
|
AccessTokenDuration = 15 * time.Minute
|
||||||
RefreshTokenDuration = 7 * 24 * time.Hour
|
RefreshTokenDuration = 7 * 24 * time.Hour
|
||||||
TokenVerifiedDuration = 10 * time.Minute
|
TokenVerifiedDuration = 10 * time.Minute
|
||||||
|
PreSignedURLDuration = 15 * time.Minute
|
||||||
|
TokenUploadDuration = 1 * time.Hour
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const (
|
|||||||
TokenPasswordReset TokenType = 1
|
TokenPasswordReset TokenType = 1
|
||||||
TokenEmailVerify TokenType = 2
|
TokenEmailVerify TokenType = 2
|
||||||
TokenMagicLink TokenType = 3
|
TokenMagicLink TokenType = 3
|
||||||
TokenRefreshToken TokenType = 4
|
TokenUpload TokenType = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t TokenType) String() string {
|
func (t TokenType) String() string {
|
||||||
@@ -17,8 +17,8 @@ func (t TokenType) String() string {
|
|||||||
return "EMAIL_VERIFY"
|
return "EMAIL_VERIFY"
|
||||||
case TokenMagicLink:
|
case TokenMagicLink:
|
||||||
return "LOGIN_MAGIC_LINK"
|
return "LOGIN_MAGIC_LINK"
|
||||||
case TokenRefreshToken:
|
case TokenUpload:
|
||||||
return "REFRESH_TOKEN"
|
return "UPLOAD"
|
||||||
default:
|
default:
|
||||||
return "UNKNOWN"
|
return "UNKNOWN"
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ func ParseTokenType(v int16) TokenType {
|
|||||||
case 3:
|
case 3:
|
||||||
return TokenMagicLink
|
return TokenMagicLink
|
||||||
case 4:
|
case 4:
|
||||||
return TokenRefreshToken
|
return TokenUpload
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -51,8 +51,8 @@ func ParseTokenTypeFromString(s string) TokenType {
|
|||||||
return TokenEmailVerify
|
return TokenEmailVerify
|
||||||
case "LOGIN_MAGIC_LINK":
|
case "LOGIN_MAGIC_LINK":
|
||||||
return TokenMagicLink
|
return TokenMagicLink
|
||||||
case "REFRESH_TOKEN":
|
case "UPLOAD":
|
||||||
return TokenRefreshToken
|
return TokenUpload
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
@@ -20,17 +22,26 @@ type UploadOptions struct {
|
|||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MoveOptions struct {
|
||||||
|
Bucket string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
Upload(ctx context.Context, key string, body io.Reader, size int64, opts UploadOptions) error
|
Upload(ctx context.Context, key string, body io.Reader, size int64, opts UploadOptions) error
|
||||||
|
Move(ctx context.Context, src *MoveOptions, dest *MoveOptions) error
|
||||||
PresignUpload(ctx context.Context, key string, expire time.Duration, opts UploadOptions) (string, error)
|
PresignUpload(ctx context.Context, key string, expire time.Duration, opts UploadOptions) (string, error)
|
||||||
GetURL(ctx context.Context, key string, expire time.Duration) (string, error)
|
GetURL(ctx context.Context, key string, expire time.Duration) (string, error)
|
||||||
Delete(ctx context.Context, key string) error
|
Delete(ctx context.Context, key string) error
|
||||||
|
GetMainBucket() string
|
||||||
|
GetTempBucket() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type s3Storage struct {
|
type s3Storage struct {
|
||||||
client *s3.Client
|
client *s3.Client
|
||||||
ps *s3.PresignClient
|
ps *s3.PresignClient
|
||||||
bucket string
|
bucket string
|
||||||
|
tempBucket string
|
||||||
endPoint string
|
endPoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,16 +60,26 @@ func NewS3Storage() (Storage, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
tempBucketName, err := ffconfig.GetConfig("STORAGE_BUCKET_TEMP_NAME")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
endpoint, err := ffconfig.GetConfig("STORAGE_ENDPOINT")
|
endpoint, err := ffconfig.GetConfig("STORAGE_ENDPOINT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
region, err := ffconfig.GetConfig("STORAGE_REGION")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadDefaultConfig(context.TODO(),
|
cfg, err := config.LoadDefaultConfig(context.TODO(),
|
||||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretAccessKey, "")),
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretAccessKey, "")),
|
||||||
config.WithRegion("auto"),
|
config.WithRegion(region),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("unable to load AWS SDK config, %v", err)
|
log.Error().Msgf("unable to load AWS SDK config, %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,15 +87,42 @@ func NewS3Storage() (Storage, error) {
|
|||||||
|
|
||||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||||
o.BaseEndpoint = aws.String(endpoint)
|
o.BaseEndpoint = aws.String(endpoint)
|
||||||
|
o.UsePathStyle = true
|
||||||
})
|
})
|
||||||
|
|
||||||
return &s3Storage{
|
return &s3Storage{
|
||||||
client: client,
|
client: client,
|
||||||
ps: s3.NewPresignClient(client),
|
ps: s3.NewPresignClient(client),
|
||||||
bucket: bucketName,
|
bucket: bucketName,
|
||||||
|
tempBucket: tempBucketName,
|
||||||
endPoint: endpoint,
|
endPoint: endpoint,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
func (s *s3Storage) GetMainBucket() string { return s.bucket }
|
||||||
|
func (s *s3Storage) GetTempBucket() string { return s.tempBucket }
|
||||||
|
|
||||||
|
func (s *s3Storage) Move(ctx context.Context, src *MoveOptions, dest *MoveOptions) error {
|
||||||
|
copySource := url.PathEscape(fmt.Sprintf("%s/%s", src.Bucket, src.Key))
|
||||||
|
|
||||||
|
_, err := s.client.CopyObject(ctx, &s3.CopyObjectInput{
|
||||||
|
Bucket: aws.String(dest.Bucket),
|
||||||
|
Key: aws.String(dest.Key),
|
||||||
|
CopySource: aws.String(copySource),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(src.Bucket),
|
||||||
|
Key: aws.String(src.Key),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to delete source object after copy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *s3Storage) Upload(ctx context.Context, key string, body io.Reader, size int64, opts UploadOptions) error {
|
func (s *s3Storage) Upload(ctx context.Context, key string, body io.Reader, size int64, opts UploadOptions) error {
|
||||||
input := &s3.PutObjectInput{
|
input := &s3.PutObjectInput{
|
||||||
@@ -99,7 +147,7 @@ func (s *s3Storage) Upload(ctx context.Context, key string, body io.Reader, size
|
|||||||
|
|
||||||
func (s *s3Storage) PresignUpload(ctx context.Context, key string, expire time.Duration, opts UploadOptions) (string, error) {
|
func (s *s3Storage) PresignUpload(ctx context.Context, key string, expire time.Duration, opts UploadOptions) (string, error) {
|
||||||
input := &s3.PutObjectInput{
|
input := &s3.PutObjectInput{
|
||||||
Bucket: &s.bucket,
|
Bucket: &s.tempBucket,
|
||||||
Key: &key,
|
Key: &key,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +167,7 @@ func (s *s3Storage) PresignUpload(ctx context.Context, key string, expire time.D
|
|||||||
}
|
}
|
||||||
return req.URL, nil
|
return req.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *s3Storage) GetURL(ctx context.Context, key string, expire time.Duration) (string, error) {
|
func (s *s3Storage) GetURL(ctx context.Context, key string, expire time.Duration) (string, error) {
|
||||||
req, err := s.ps.PresignGetObject(ctx, &s3.GetObjectInput{
|
req, err := s.ps.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||||
Bucket: &s.bucket,
|
Bucket: &s.bucket,
|
||||||
Reference in New Issue
Block a user