Skip to content

Commit

Permalink
feat(api): added new endpoints /api/v1/audio - uploading new audio files
Browse files Browse the repository at this point in the history
  • Loading branch information
Wittano committed Apr 11, 2024
1 parent d51953a commit cd60c42
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 139 deletions.
95 changes: 63 additions & 32 deletions api/handler/audio.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,61 @@ package handler

import (
"context"
"fmt"
"github.com/wittano/komputer/pkgs/settings"
"github.com/wittano/komputer/pkgs/voice"
"net/http"
"time"
"os"
"path/filepath"
)

// TODO export property to config file/environment variable
const maxFileSize = 8 * 1024 * 1024 // 8MB in bytes
const oneMegaByte = 1 << 20 // 8MB in bytes

func UploadNewAudio(res http.ResponseWriter, req *http.Request) (err error) {
err = req.ParseMultipartForm(maxFileSize)
err = req.ParseMultipartForm(settings.Config.Upload.MaxFileSize * oneMegaByte)
if err != nil {
return newInternalApiError(err)
}

// TODO export to property how much user can upload files
if counts := len(req.MultipartForm.Value); counts < 1 || counts > 5 {
filesCount := len(req.MultipartForm.File)
if !settings.Config.CheckFileCountLimit(filesCount) {
return apiError{
Status: http.StatusBadRequest,
Msg: "illegal uploaded files count",
}
}

// TODO export uploading timeout to external properties
const uploadingTimeout = time.Second * 2
ctx, cancel := context.WithTimeout(req.Context(), uploadingTimeout)
defer cancel()

filesCount := len(req.MultipartForm.File)
var (
errCh = make(chan error)
successSigCh = make(chan struct{}, filesCount)
errCh = make(chan error)
successCh = make(chan struct{}, filesCount)
)
defer close(errCh)
defer close(successSigCh)
defer close(successCh)

for k := range req.MultipartForm.File {
go uploadFile(ctx, *req, k, errCh, successSigCh)
err = validRequestedFile(k, *req)
if err != nil {
return apiError{
Status: http.StatusBadRequest,
Msg: fmt.Sprintf("invalid '%s' file", k),
Err: err,
}
}

go uploadRequestedFile(req.Context(), k, req, errCh, successCh)
}

var (
resError error
successCounter = filesCount
)
successCounter := filesCount

for {
select {
case <-ctx.Done():
resError = context.Canceled
break
case <-req.Context().Done():
return context.Canceled
case err = <-errCh:
resError = err
break
case <-successSigCh:
return err
case <-successCh:
successCounter -= 1
break
}

if successCounter <= 0 {
Expand All @@ -65,24 +66,54 @@ func UploadNewAudio(res http.ResponseWriter, req *http.Request) (err error) {
}
}

return resError
return nil
}

func uploadFile(ctx context.Context, req http.Request, name string, errCh chan<- error, successSig chan<- struct{}) {
file, fileHeader, err := req.FormFile(name)
func validRequestedFile(filename string, req http.Request) error {
_, fileHeader, err := req.FormFile(filename)
if err != nil {
return err
}

if err = voice.ValidMp3File(fileHeader); err != nil {
return err
}

destFile := filepath.Join(settings.Config.AssetDir, filename)
if _, err = os.Stat(destFile); err == nil {
return os.ErrExist
}

return nil
}

func uploadRequestedFile(ctx context.Context, filename string, req *http.Request, errCh chan<- error, successSig chan<- struct{}) {
select {
case <-ctx.Done():
errCh <- context.Canceled
return
default:
}

f, _, err := req.FormFile(filename)
if err != nil {
errCh <- newInternalApiError(err)

return
}
defer file.Close()
defer f.Close()

if err := voice.ValidMp3File(fileHeader); err != nil {
dest, err := os.Create(filepath.Join(settings.Config.AssetDir, filename))
if err != nil {
errCh <- newInternalApiError(err)

return
}
defer dest.Close()

if err = voice.UploadFile(ctx, fileHeader.Filename, file); err != nil {
if err = voice.UploadFile(ctx, f, dest); err != nil {
errCh <- newInternalApiError(err)
os.Remove(dest.Name())

return
}
Expand Down
159 changes: 159 additions & 0 deletions api/handler/audio_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package handler

import (
"bytes"
"context"
"errors"
"github.com/wittano/komputer/pkgs/settings"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"testing"
"time"
)

const testFileName = "test.mp3"

func createTempAudioFiles(t *testing.T) (string, error) {
dir := t.TempDir()
f, err := os.CreateTemp(dir, "test.*.mp3")
if err != nil {
return "", err
}
defer f.Close()

_, err = f.Write([]byte{0xff, 0xfb})
if err != nil {
return "", err
}

return f.Name(), nil
}

func createMultipartFileHeader(filename string) (*multipart.FileHeader, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()

var buf bytes.Buffer

formWriter := multipart.NewWriter(&buf)
formPart, err := formWriter.CreateFormFile(testFileName, filepath.Base(filename))
if err != nil {
return nil, err
}

if _, err = io.Copy(formPart, f); err != nil {
return nil, err
}

err = formWriter.Close()
if err != nil {
return nil, err
}

reader := bytes.NewReader(buf.Bytes())
formReader := multipart.NewReader(reader, formWriter.Boundary())

multipartForm, err := formReader.ReadForm(1 << 20)
if err != nil {
return nil, err
}

if file, ok := multipartForm.File[testFileName]; !ok || len(file) <= 0 {
return nil, errors.New("failed create multipart file")
} else {
return file[0], nil
}
}

func loadDefaultConfig(t *testing.T) error {
configFile := filepath.Join(t.TempDir(), "config.yml")

if err := settings.Load(configFile); err != nil {
return err
}

return settings.Config.Update(settings.Settings{AssetDir: filepath.Join(t.TempDir(), "assets")})
}

func TestValidRequestedFile(t *testing.T) {
if err := loadDefaultConfig(t); err != nil {
t.Fatal(err)
}

filePath, err := createTempAudioFiles(t)
if err != nil {
t.Fatal(err)
}

multipartFileHeader, err := createMultipartFileHeader(filePath)
if err != nil {
t.Fatal(err)
}

req := http.Request{
MultipartForm: &multipart.Form{
File: map[string][]*multipart.FileHeader{
testFileName: {
multipartFileHeader,
},
},
},
}

err = validRequestedFile(testFileName, req)
if err != nil {
t.Fatal(err)
}
}

func TestUploadRequestedFile(t *testing.T) {
if err := loadDefaultConfig(t); err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
filePath, err := createTempAudioFiles(t)
if err != nil {
t.Fatal(err)
}

multipartFileHeader, err := createMultipartFileHeader(filePath)
if err != nil {
t.Fatal(err)
}

req := &http.Request{
MultipartForm: &multipart.Form{
File: map[string][]*multipart.FileHeader{
testFileName: {
multipartFileHeader,
},
},
},
}

successCh := make(chan struct{})
errCh := make(chan error)
defer close(successCh)
defer close(errCh)

go uploadRequestedFile(ctx, testFileName, req, errCh, successCh)

for {
select {
case <-ctx.Done():
t.Fatal(context.Canceled)
case err = <-errCh:
t.Fatal(err)
case <-successCh:
return
}
}
}
46 changes: 10 additions & 36 deletions pkgs/settings/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ package settings
import (
"errors"
"github.com/mitchellh/go-homedir"
"github.com/wittano/komputer/internal/assets"
"gopkg.in/yaml.v3"
"os"
"path/filepath"
"sync"
)

const (
DefaultAssertDir = ".cache/komputer"
DefaultSettingsPath = ".config/komputer/settings.yml"
)

const maxFileSize = 8 * (1 << 20) // 8MB in bytes
const defaultMaxFileSize = 8 * (1 << 20) // 8MB in bytes

type UploadSettings struct {
MaxFileCount uint `yaml:"max_file_count" json:"max_file_count"`
MaxFileSize uint `yaml:"max_file_size" json:"max_file_size"`
MaxFileCount int64 `yaml:"max_file_count" json:"max_file_count"`
MaxFileSize int64 `yaml:"max_file_size" json:"max_file_size"`
}

type Settings struct {
Expand All @@ -32,7 +32,7 @@ func (s *Settings) Update(new Settings) error {
return err
}

err := moveAssets(s.AssetDir, new.AssetDir)
err := assets.Move(s.AssetDir, new.AssetDir)

Check failure on line 35 in pkgs/settings/types.go

View workflow job for this annotation

GitHub Actions / build

undefined: assets.Move
if err != nil {
return err
}
Expand All @@ -51,6 +51,10 @@ func (s *Settings) Update(new Settings) error {
return nil
}

func (s Settings) CheckFileCountLimit(count int) bool {
return count >= 1 && int64(count) <= s.Upload.MaxFileCount
}

var Config *Settings

func Load(path string) error {
Expand Down Expand Up @@ -98,7 +102,7 @@ func defaultSettings(path string) (*Settings, error) {
AssetDir: DefaultAssertDir,
Upload: UploadSettings{
MaxFileCount: 5,
MaxFileSize: maxFileSize,
MaxFileSize: defaultMaxFileSize,
},
}

Expand All @@ -110,33 +114,3 @@ func defaultSettings(path string) (*Settings, error) {

return &defaultSettings, nil
}

func moveAssets(oldSrc string, path string) (err error) {
dirs, err := os.ReadDir(oldSrc)
if err != nil {
return err
}

var wg sync.WaitGroup
wg.Add(len(dirs))

for _, dir := range dirs {
go func(wg *sync.WaitGroup, oldSrc string, file os.DirEntry) {
defer wg.Done()

if err != nil {
return
}

filename := filepath.Join(oldSrc, file.Name())
newPath := filepath.Join(path, filepath.Base(filename))
if err = os.Rename(filename, newPath); err != nil {
return
}
}(&wg, oldSrc, dir)
}

wg.Wait()

return
}
Loading

0 comments on commit cd60c42

Please sign in to comment.