From f7d3d64674928b2e71c0a7b941ca44c6b8589a0f Mon Sep 17 00:00:00 2001 From: Wittano Bonarotti Date: Fri, 12 Apr 2024 16:43:52 +0200 Subject: [PATCH] feat(audio): added bot integration web api audio endpoints Added integration with GET /api/v1/audio/{id} endpoint to downloading audio file from server. Also bot can save files local as cache --- .env.example | 4 +- .gitignore | 7 +++- bot/bot.go | 16 +++++++- bot/internal/api/types.go | 59 +++++++++++++++++++++++++++++ bot/internal/command/spock.go | 68 ++++++++++++++++++++-------------- bot/internal/voice/audio.go | 50 ++++++++++++------------- bot/internal/voice/file.go | 25 +++++++++++++ cmd/web/main.go | 2 +- db/types.go | 5 ++- go.mod | 4 +- go.sum | 18 +++++---- web/go.mod | 9 +++++ web/go.sum | 8 ++++ web/internal/api/error.go | 12 ++++++ web/internal/audio/database.go | 22 ++++++++--- web/internal/audio/service.go | 21 +++++++++-- web/internal/handler/audio.go | 10 ++++- web/settings/types.go | 10 ++++- 18 files changed, 270 insertions(+), 80 deletions(-) create mode 100644 bot/internal/api/types.go create mode 100644 bot/internal/voice/file.go create mode 100644 web/internal/api/error.go diff --git a/.env.example b/.env.example index b436b7a..78824a5 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,6 @@ APPLICATION_ID= # Your bot application id SERVER_GUID= # Your server ID MONGODB_URI= # URI to connect MongoDB database MONGODB_DB_NAME= # Main database for komputer bot -RAPID_API_KEY= # OPTIONAL! API key for RapidAPIs. In project is use: HumorAP \ No newline at end of file +RAPID_API_KEY= # OPTIONAL! API key for RapidAPIs. In project is use: HumorAP +WEB_API_BASE_URL= # OPTIONAL! Base url to web api +CACHE_AUDIO_DIR= # OPTIONAL! Path to dictionary for local storage audio \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7b46fb7..e725e89 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,9 @@ build/ .vscode/ result/ -assets \ No newline at end of file +assets + +# Web files +*.mp3 +*.mp4 +*.yml \ No newline at end of file diff --git a/bot/bot.go b/bot/bot.go index 74e504a..61c3c2a 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -6,12 +6,14 @@ import ( "fmt" "github.com/bwmarrin/discordgo" "github.com/google/uuid" + "github.com/wittano/komputer/bot/internal/api" "github.com/wittano/komputer/bot/internal/command" "github.com/wittano/komputer/bot/internal/config" "github.com/wittano/komputer/bot/internal/joke" "github.com/wittano/komputer/bot/internal/voice" "github.com/wittano/komputer/db" "log/slog" + "os" "time" ) @@ -21,7 +23,10 @@ const ( databaseServiceID = 2 ) -const requestIDKey = "requestID" +const ( + baseURLKey = "WEB_API_BASE_URL" + requestIDKey = "requestID" +) type slashCommandHandler struct { ctx context.Context @@ -115,10 +120,17 @@ func createJokeGetServices(globalCtx context.Context, database *db.MongodbDataba } func createCommands(globalCtx context.Context, services []joke.GetService, spockVoiceChns map[string]chan struct{}, guildVoiceChats map[string]voice.ChatInfo) map[string]command.DiscordSlashCommandHandler { + var client *api.WebClient + if url, ok := os.LookupEnv(baseURLKey); ok && url != "" { + client = api.NewClient(url) + } else { + slog.WarnContext(globalCtx, "BaseURL for Web API is empty. Web API is disabled") + } + welcomeCmd := command.WelcomeCommand{} addJokeCmd := command.AddJokeCommand{Service: services[databaseServiceID].(joke.DatabaseJokeService)} jokeCmd := command.JokeCommand{Services: services} - spockCmd := command.SpockCommand{GlobalCtx: globalCtx, SpockMusicStopChs: spockVoiceChns, GuildVoiceChats: guildVoiceChats} + spockCmd := command.SpockCommand{GlobalCtx: globalCtx, SpockMusicStopChs: spockVoiceChns, GuildVoiceChats: guildVoiceChats, ApiClient: client} stopSpockCmd := command.SpockStopCommand{SpockMusicStopChs: spockVoiceChns} return map[string]command.DiscordSlashCommandHandler{ diff --git a/bot/internal/api/types.go b/bot/internal/api/types.go new file mode 100644 index 0000000..e98c03e --- /dev/null +++ b/bot/internal/api/types.go @@ -0,0 +1,59 @@ +package api + +import ( + "fmt" + "github.com/wittano/komputer/bot/internal/voice" + "io" + "net/http" + "os" + "time" +) + +type WebClient struct { + baseURL string + client http.Client +} + +func (c WebClient) DownloadAudio(id string) (path string, err error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/audio/%s", c.baseURL, id), nil) + if err != nil { + return "", err + } + + res, err := c.client.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + body, err := io.ReadAll(res.Body) + if err == nil { + err = fmt.Errorf("failed download audio. Response: %s", body) + } + + return "", err + } + + dest := voice.Path(id) + f, err := os.Create(dest) + if err != nil { + return "", err + } + defer f.Close() + + if _, err = io.Copy(f, res.Body); err != nil { + return "", err + } + + return dest, nil +} + +func NewClient(url string) *WebClient { + return &WebClient{ + baseURL: url, + client: http.Client{ + Timeout: time.Second * 5, + }, + } +} diff --git a/bot/internal/command/spock.go b/bot/internal/command/spock.go index 6a0e17d..50e7ba8 100644 --- a/bot/internal/command/spock.go +++ b/bot/internal/command/spock.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/bwmarrin/discordgo" + "github.com/wittano/komputer/bot/internal/api" "github.com/wittano/komputer/bot/internal/voice" "log/slog" "os" @@ -18,6 +19,7 @@ type SpockCommand struct { GlobalCtx context.Context SpockMusicStopChs map[string]chan struct{} GuildVoiceChats map[string]voice.ChatInfo + ApiClient *api.WebClient } func (sc SpockCommand) Command() *discordgo.ApplicationCommand { @@ -52,45 +54,55 @@ func (sc SpockCommand) Execute(ctx context.Context, s *discordgo.Session, i *dis return SimpleMessageResponse{Msg: "Kapitanie gdzie jesteś? Wejdź na kanał głosowy a ja dołącze"}, nil } - go sc.playAudio(ctx, info.ChannelID, s, i) + logger := slog.With(requestIDKey, ctx.Value(requestIDKey)) - return defaultMsg, nil -} + audioID, err := audioID(i.Data.(discordgo.ApplicationCommandInteractionData)) + if err != nil { + logger.ErrorContext(ctx, "failed find song path", "error", err) -func (sc SpockCommand) playAudio(ctx context.Context, channelID string, s *discordgo.Session, i *discordgo.InteractionCreate) { - select { - case <-ctx.Done(): - return - default: + return nil, nil } - logger := slog.With(requestIDKey, ctx.Value(requestIDKey)) + if err = voice.CheckIfAudioIsDownloaded(audioID); err != nil && sc.ApiClient != nil { + go func() { + logger.Info("download audio with id " + audioID) + _, err := sc.ApiClient.DownloadAudio(audioID) + if err != nil { + logger.Error(fmt.Sprintf("failed download audio. %s", err)) + return + } + + logger.Info("success download audio with id " + audioID) + sc.playAudio(logger, s, i, info.ChannelID, audioID) + }() + + return SimpleMessageResponse{Msg: "Panie Kapitanie. Pobieram utwór. Proszę poczekać"}, nil + } else { + go sc.playAudio(logger, s, i, info.ChannelID, audioID) + } + return defaultMsg, nil +} + +func (sc SpockCommand) playAudio(l *slog.Logger, s *discordgo.Session, i *discordgo.InteractionCreate, channelID string, audioID string) { s.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsGuildVoiceStates) voiceChat, err := s.ChannelVoiceJoin(i.GuildID, channelID, false, true) if err != nil { - logger.ErrorContext(ctx, "failed join to voice channel", "error", err) + l.Error("failed join to voice channel", "error", err) return } defer func(voiceChat *discordgo.VoiceConnection) { if err := voiceChat.Disconnect(); err != nil { - logger.ErrorContext(ctx, "failed disconnect discord from voice channel", "error", err) + l.Error("failed disconnect discord from voice channel", "error", err) } }(voiceChat) defer voiceChat.Close() - songPath, err := songPath(i.Data.(discordgo.ApplicationCommandInteractionData)) - if err != nil { - logger.ErrorContext(ctx, "failed find song path", "error", err) - - return - } - stopCh, ok := sc.SpockMusicStopChs[i.GuildID] if !ok { - logger.ErrorContext(ctx, fmt.Sprintf("failed find user on voice channels on '%s' server", i.Member.GuildID), "error", fmt.Errorf("user with ID '%s' wasn't found on any voice chat on '%s' server", i.Member.User.ID, i.GuildID)) + l.Error(fmt.Sprintf("failed find user on voice channels on '%s' server", i.Member.GuildID), "error", fmt.Errorf("user with ID '%s' wasn't found on any voice chat on '%s' server", i.Member.User.ID, i.GuildID)) return } @@ -100,22 +112,24 @@ func (sc SpockCommand) playAudio(ctx context.Context, channelID string, s *disco cancel context.CancelFunc ) - if audioDuration, err := voice.DuractionAudio(songPath); err != nil { - logger.WarnContext(ctx, "failed calculated audio duration", "error", err) + voiceCtx := context.Background() + if audioDuration, err := voice.Duration(audioID); err != nil { + l.WarnContext(voiceCtx, "failed calculated audio duration", "error", err) - playingCtx, cancel = context.WithCancel(context.Background()) + playingCtx, cancel = context.WithCancel(voiceCtx) } else { - playingCtx, cancel = context.WithTimeout(context.Background(), audioDuration) + playingCtx, cancel = context.WithTimeout(voiceCtx, audioDuration) } - defer cancel() - if err = voice.PlayAudio(playingCtx, voiceChat, songPath, stopCh); err != nil { - logger.ErrorContext(ctx, fmt.Sprintf("failed play '%s' songPath", songPath), "error", err) + path := voice.Path(audioID) + + if err = voice.Play(playingCtx, voiceChat, path, stopCh); err != nil { + l.ErrorContext(playingCtx, fmt.Sprintf("failed play '%s' audioID", audioID), "error", err) } } -func songPath(data discordgo.ApplicationCommandInteractionData) (path string, err error) { +func audioID(data discordgo.ApplicationCommandInteractionData) (path string, err error) { for _, o := range data.Options { switch o.Name { case idOptionName: diff --git a/bot/internal/voice/audio.go b/bot/internal/voice/audio.go index ef05989..3f2fd3f 100644 --- a/bot/internal/voice/audio.go +++ b/bot/internal/voice/audio.go @@ -22,7 +22,7 @@ const ( audioBufSize = 16384 ) -func PlayAudio(ctx context.Context, vc *discordgo.VoiceConnection, path string, stop <-chan struct{}) (err error) { +func Play(ctx context.Context, vc *discordgo.VoiceConnection, path string, stop <-chan struct{}) (err error) { select { case <-ctx.Done(): return context.Canceled @@ -103,6 +103,30 @@ func PlayAudio(ctx context.Context, vc *discordgo.VoiceConnection, path string, } } +func Duration(path string) (duration time.Duration, err error) { + cmd := exec.Command("ffprobe", "-i", path, "-show_entries", "format=duration", "-v", "quiet", "-of", "csv='p=0'") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + + if err = cmd.Start(); err != nil { + return + } + + output, err := cmd.StdoutPipe() + if err != nil { + return + } + defer output.Close() + + rawTime, err := io.ReadAll(output) + if err != nil { + return + } + + return time.ParseDuration(string(rawTime) + "s") +} + func sendPCM(ctx context.Context, v *discordgo.VoiceConnection, pcm <-chan []int16) error { opusEncoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio) if err != nil { @@ -135,27 +159,3 @@ func sendPCM(ctx context.Context, v *discordgo.VoiceConnection, pcm <-chan []int } } } - -func DuractionAudio(path string) (duration time.Duration, err error) { - cmd := exec.Command("ffprobe", "-i", path, "-show_entries", "format=duration", "-v", "quiet", "-of", "csv='p=0'") - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - - if err = cmd.Start(); err != nil { - return - } - - output, err := cmd.StdoutPipe() - if err != nil { - return - } - defer output.Close() - - rawTime, err := io.ReadAll(output) - if err != nil { - return - } - - return time.ParseDuration(string(rawTime) + "s") -} diff --git a/bot/internal/voice/file.go b/bot/internal/voice/file.go new file mode 100644 index 0000000..58f1d30 --- /dev/null +++ b/bot/internal/voice/file.go @@ -0,0 +1,25 @@ +package voice + +import ( + "os" + "path/filepath" +) + +const ( + defaultCacheDirAudio = "assets" + CacheDirAudioKey = "CACHE_AUDIO_DIR" +) + +func CheckIfAudioIsDownloaded(id string) (err error) { + _, err = os.Stat(Path(id)) + return +} + +func Path(id string) string { + cachePath := defaultCacheDirAudio + if cacheDir, ok := os.LookupEnv(CacheDirAudioKey); ok && cacheDir != "" { + cachePath = cacheDir + } + + return filepath.Join(cachePath, id+".mp3") +} diff --git a/cmd/web/main.go b/cmd/web/main.go index 5da266f..771ebab 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -6,7 +6,7 @@ import ( "log" ) -const defaultConfigPath = "config.yml" +const defaultConfigPath = "settings.yml" func main() { configPath := flag.String("config", defaultConfigPath, "Path to web console configuration audio") diff --git a/db/types.go b/db/types.go index 2dfe925..9122025 100644 --- a/db/types.go +++ b/db/types.go @@ -11,8 +11,9 @@ import ( const DatabaseName = "komputer" type AudioInfo struct { - ID primitive.ObjectID `bson:"_id"` - Path string `bson:"path"` + ID primitive.ObjectID `bson:"_id"` + Path string `bson:"path"` + Original string `bson:"original_name"` } type MongodbService interface { diff --git a/go.mod b/go.mod index 1e99b2a..465dbdf 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,13 @@ go 1.22.1 require ( github.com/wittano/komputer/bot v0.0.0-20240411141946-658aa8d45c90 - github.com/wittano/komputer/web v0.0.0-20240411141946-658aa8d45c90 + github.com/wittano/komputer/web v0.0.0-20240412103354-9dbece601221 go.mongodb.org/mongo-driver v1.14.0 ) require ( github.com/bwmarrin/discordgo v0.28.1 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect @@ -32,6 +33,7 @@ require ( golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32 // indirect ) diff --git a/go.sum b/go.sum index bd56049..79c5739 100644 --- a/go.sum +++ b/go.sum @@ -37,12 +37,14 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -101,8 +103,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E= github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0= github.com/testcontainers/testcontainers-go/modules/mongodb v0.29.1 h1:UEU6STi5h1A0TcVyAI8MtAPxnLD6DrDogZpTQ6TZ4qs= @@ -117,8 +119,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wittano/komputer/bot v0.0.0-20240411141946-658aa8d45c90 h1:b3koNS5jN9y/5f9ud7yYhbm25ojPmBV58UQQxQ680WQ= github.com/wittano/komputer/bot v0.0.0-20240411141946-658aa8d45c90/go.mod h1:hLu/V9pqAvQNUOhIB94IOrfenlJmWooiJuEGTq+M/Oc= -github.com/wittano/komputer/web v0.0.0-20240411141946-658aa8d45c90 h1:W9TgTEpjoO5gk/ZIjRk2331Ezjo+4OScTVorYZxS3cU= -github.com/wittano/komputer/web v0.0.0-20240411141946-658aa8d45c90/go.mod h1:U5NlQ4hMJ0MyTQSnN8ukCgy1TtSpF07aDV2Qh3DJaD0= +github.com/wittano/komputer/web v0.0.0-20240412103354-9dbece601221 h1:2iTNX2vEN41kBjegTpcw3n4Q/Ji8UhlZYC0XVfifBL8= +github.com/wittano/komputer/web v0.0.0-20240412103354-9dbece601221/go.mod h1:ZDDeswBhrpbjXkFAeGo1jglnQ7OmsS8dlSuJk1zDw/o= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -179,14 +181,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= diff --git a/web/go.mod b/web/go.mod index 0a5c67a..2b7c4c4 100644 --- a/web/go.mod +++ b/web/go.mod @@ -9,18 +9,27 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/web/go.sum b/web/go.sum index 8e65db5..cd04ab1 100644 --- a/web/go.sum +++ b/web/go.sum @@ -2,7 +2,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -20,6 +22,7 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -31,9 +34,14 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= diff --git a/web/internal/api/error.go b/web/internal/api/error.go new file mode 100644 index 0000000..f44389d --- /dev/null +++ b/web/internal/api/error.go @@ -0,0 +1,12 @@ +package api + +import "github.com/labstack/echo/v4" + +type Error struct { + HttpErr *echo.HTTPError + Err error +} + +func (a Error) Error() string { + return a.Err.Error() +} diff --git a/web/internal/audio/database.go b/web/internal/audio/database.go index bed5fa3..2d0e1c2 100644 --- a/web/internal/audio/database.go +++ b/web/internal/audio/database.go @@ -7,6 +7,8 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "os" + "path/filepath" + "strings" ) const audioCollectionName = "audio" @@ -17,19 +19,27 @@ type DatabaseService struct { var NotFoundErr = errors.New("audio not found") -func (a DatabaseService) save(ctx context.Context, filename string) error { +func (a DatabaseService) save(ctx context.Context, filename string) (primitive.ObjectID, error) { client, err := a.Database.Client(ctx) if err != nil { - return err + return primitive.ObjectID{}, err } + base := filepath.Base(filename) + id := primitive.NewObjectID() + realPath := strings.ReplaceAll(filename, base, id.Hex()+".mp3") info := db.AudioInfo{ - ID: primitive.NewObjectID(), - Path: filename, + ID: id, + Original: base, + Path: realPath, } - _, err = client.Database(db.DatabaseName).Collection(audioCollectionName).InsertOne(ctx, info) - return err + result, err := client.Database(db.DatabaseName).Collection(audioCollectionName).InsertOne(ctx, info) + if err != nil { + return primitive.ObjectID{}, err + } + + return result.InsertedID.(primitive.ObjectID), nil } func (a DatabaseService) Get(ctx context.Context, id string) (result db.AudioInfo, err error) { diff --git a/web/internal/audio/service.go b/web/internal/audio/service.go index 52a8987..ea9b130 100644 --- a/web/internal/audio/service.go +++ b/web/internal/audio/service.go @@ -6,12 +6,14 @@ import ( "fmt" "github.com/labstack/echo/v4" "github.com/wittano/komputer/db" + "github.com/wittano/komputer/web/internal/api" "github.com/wittano/komputer/web/settings" "io" "mime/multipart" "net/http" "os" "path/filepath" + "strings" ) type UploadService struct { @@ -28,8 +30,13 @@ func (u UploadService) Upload(ctx context.Context, files []*multipart.FileHeader filesCount := len(files) for _, f := range files { - if err := validRequestedFile(*f); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid '%s' audio", f.Filename)) + if err := validRequestedFile(*f); errors.Is(err, os.ErrExist) { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("file with name: '%s' exists", f.Filename)) + } else if err != nil { + return api.Error{ + HttpErr: echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid '%s' audio", f.Filename)), + Err: err, + } } go u.save(ctx, f, errCh, successCh) @@ -92,10 +99,18 @@ func (u UploadService) save(ctx context.Context, file *multipart.FileHeader, err if errors.Is(err, io.EOF) { audioService := DatabaseService{u.Db} - err = audioService.save(ctx, dest.Name()) + id, err := audioService.save(ctx, dest.Name()) if err != nil { errCh <- err } else { + renameFile := strings.ReplaceAll(dest.Name(), file.Filename, id.Hex()+".mp3") + if err = os.Rename(destPath, renameFile); err != nil { + errCh <- err + os.Remove(destPath) + + return + } + successSig <- struct{}{} } diff --git a/web/internal/handler/audio.go b/web/internal/handler/audio.go index 1be9bf1..7019618 100644 --- a/web/internal/handler/audio.go +++ b/web/internal/handler/audio.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/labstack/echo/v4" "github.com/wittano/komputer/db" + "github.com/wittano/komputer/web/internal/api" "github.com/wittano/komputer/web/internal/audio" "github.com/wittano/komputer/web/settings" "mime/multipart" @@ -51,13 +52,20 @@ func UploadNewAudio(c echo.Context) (err error) { filesCount := len(files) if !settings.Config.CheckFileCountLimit(filesCount) { - return echo.NewHTTPError(http.StatusBadRequest, "invalid number of uploaded files") + return errors.Join(echo.NewHTTPError(http.StatusBadRequest, "invalid number of uploaded files"), err) } ctx := c.Request().Context() service := audio.UploadService{Db: db.Mongodb(ctx)} if err := service.Upload(ctx, files); err != nil { + var apiError api.Error + + if errors.As(err, &apiError) { + c.Logger().Error(apiError.Err) + err = apiError.HttpErr + } + return err } diff --git a/web/settings/types.go b/web/settings/types.go index 438d5fc..a6b7a8c 100644 --- a/web/settings/types.go +++ b/web/settings/types.go @@ -80,7 +80,7 @@ func Load(path string) error { return err } - return nil + return os.MkdirAll(Config.AssetDir, 0700) } func defaultSettings(path string) (*Settings, error) { @@ -90,8 +90,14 @@ func defaultSettings(path string) (*Settings, error) { } defer f.Close() + const cacheDirKey = "CACHE_AUDIO_DIR" + assetDir := DefaultAssertDir + if assetDirPath, ok := os.LookupEnv(cacheDirKey); ok && assetDirPath != "" { + assetDir = assetDirPath + } + defaultSettings := Settings{ - AssetDir: DefaultAssertDir, + AssetDir: assetDir, Upload: UploadSettings{ MaxFileCount: 5, MaxFileSize: defaultMaxFileSize,