diff --git a/audio/file.go b/audio/file.go new file mode 100644 index 0000000..fcae236 --- /dev/null +++ b/audio/file.go @@ -0,0 +1,17 @@ +package audio + +import ( + "context" + "io" +) + +func Upload(ctx context.Context, r io.ReadCloser) (int, error) { + defer r.Close() + select { + case <-ctx.Done(): + return 0, context.Canceled + default: + } + + return 0, nil +} diff --git a/bot/audio/file.go b/audio/path.go similarity index 69% rename from bot/audio/file.go rename to audio/path.go index 43ad576..304ae44 100644 --- a/bot/audio/file.go +++ b/audio/path.go @@ -49,6 +49,34 @@ func Paths() (paths []string, err error) { return } +// PathsWithPagination get fixed-sized list of audio paths from assert dictionary +func PathsWithPagination(page uint32, size uint32) (paths []string, err error) { + dirs, err := os.ReadDir(AssertDir()) + if err != nil { + return nil, err + } + + skipFiles := int(page * size) + if len(dirs) < skipFiles { + return []string{}, nil + } else if len(dirs) <= 0 { + return nil, errors.New("assert directory is empty") + } + + paths = make([]string, 0, size) + for _, dir := range dirs[skipFiles:] { + if dir.Type() != os.ModeDir { + paths = append(paths, dir.Name()) + } + + if len(paths) >= int(size) { + break + } + } + + return +} + func RandomPath() (string, error) { paths, err := Paths() if err != nil { diff --git a/bot/audio/file_test.go b/audio/path_test.go similarity index 100% rename from bot/audio/file_test.go rename to audio/path_test.go diff --git a/bot/bot.go b/bot/bot.go index 81ec3ac..a5e94f6 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -128,7 +128,7 @@ func createJokeGetServices(globalCtx context.Context, database *db.MongodbDataba return []dbJoke.SearchService{ jokeDevServiceID: joke.NewJokeDevService(globalCtx), humorAPIServiceID: joke.NewHumorAPIService(globalCtx), - databaseServiceID: joke.NewDatabaseJokeService(database), + databaseServiceID: joke.NewJokeDatabase(database), } } diff --git a/bot/command/joke_test.go b/bot/command/joke_test.go index 92a8e26..32ef1a4 100644 --- a/bot/command/joke_test.go +++ b/bot/command/joke_test.go @@ -24,7 +24,7 @@ func TestSelectGetService(t *testing.T) { testServices := []dbJoke.SearchService{ joke.NewJokeDevService(ctx), joke.NewHumorAPIService(ctx), - joke.NewDatabaseJokeService(dumpMongoService{}), + joke.NewJokeDatabase(dumpMongoService{}), } service, err := findService(ctx, testServices) @@ -47,7 +47,7 @@ func TestFindJokeService_ContextCancelled(t *testing.T) { testServices := []dbJoke.SearchService{ joke.NewJokeDevService(ctx), joke.NewHumorAPIService(ctx), - joke.NewDatabaseJokeService(dumpMongoService{}), + joke.NewJokeDatabase(dumpMongoService{}), } if _, err := findService(ctx, testServices); err == nil { @@ -58,7 +58,7 @@ func TestFindJokeService_ContextCancelled(t *testing.T) { func TestFindJokeService_ServicesIsDeactivated(t *testing.T) { ctx := context.Background() services := []dbJoke.SearchService{ - joke.NewDatabaseJokeService(dumpMongoService{}), + joke.NewJokeDatabase(dumpMongoService{}), } if _, err := findService(ctx, services); err == nil { diff --git a/bot/command/list.go b/bot/command/list.go index e5e9823..0e6a922 100644 --- a/bot/command/list.go +++ b/bot/command/list.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "github.com/bwmarrin/discordgo" - "github.com/wittano/komputer/bot/audio" + "github.com/wittano/komputer/audio" "os" "regexp" "strings" diff --git a/bot/command/spock.go b/bot/command/spock.go index 744bd98..57032e6 100644 --- a/bot/command/spock.go +++ b/bot/command/spock.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/bwmarrin/dgvoice" "github.com/bwmarrin/discordgo" - "github.com/wittano/komputer/bot/audio" + "github.com/wittano/komputer/audio" "github.com/wittano/komputer/bot/log" "github.com/wittano/komputer/bot/voice" "log/slog" @@ -133,7 +133,7 @@ func audioPath(data discordgo.ApplicationCommandInteractionData) (path string, e } } - if name == "" && err == nil { + if name == "" { path, err = audio.RandomPath() } else { path = audio.Path(path) diff --git a/proto/audio.proto b/proto/audio.proto index 852aaac..f82d3fb 100644 --- a/proto/audio.proto +++ b/proto/audio.proto @@ -17,10 +17,10 @@ message AudioInfo { message Audio { AudioInfo info = 1; - bytes file = 2; + bytes chunk = 2; } -message File { +message FileBuffer { bytes content = 1; uint64 size = 2; } @@ -30,12 +30,17 @@ message UploadAudioResponse { uint32 size = 2; } +message DownloadRequest { + optional UUID uuid = 1; + optional string name = 2; +} + service AudioService { rpc List(komputer.Pagination) returns (stream AudioInfo); rpc Add(stream Audio) returns (UploadAudioResponse); - rpc Remove(NameOrIdRequest) returns (google.protobuf.Empty); + rpc Remove(NameOrIdAudioRequest) returns (google.protobuf.Empty); } service AudioFileService { - rpc Download(FindById) returns (stream File); + rpc Download(DownloadRequest) returns (stream FileBuffer); } \ No newline at end of file diff --git a/proto/request.proto b/proto/request.proto index f33551c..97efbf0 100644 --- a/proto/request.proto +++ b/proto/request.proto @@ -24,11 +24,14 @@ message ObjectID { string object_id = 1; } -message NameOrIdRequest { +message FileQuery { oneof query { uint64 id = 1; - bytes uuid = 2; + UUID uuid = 2; string name = 3; } - optional Pagination page = 4; +} + +message NameOrIdAudioRequest { + repeated FileQuery query = 1; } \ No newline at end of file diff --git a/server/audio.go b/server/audio.go new file mode 100644 index 0000000..49dd350 --- /dev/null +++ b/server/audio.go @@ -0,0 +1,102 @@ +package server + +import ( + "context" + "errors" + "fmt" + "github.com/google/uuid" + komputer "github.com/wittano/komputer/api/proto" + "github.com/wittano/komputer/audio" + "google.golang.org/protobuf/types/known/emptypb" + "os" + "sync" +) + +type audioServer struct { + komputer.UnimplementedAudioServiceServer +} + +func (a audioServer) List(pagination *komputer.Pagination, server komputer.AudioService_ListServer) (err error) { + page := paginationOrDefault(pagination) + + paths, err := audio.PathsWithPagination(page.Page, page.Size) + if err != nil { + return + } + + for _, path := range paths { + err = errors.Join(err, server.Send(&komputer.AudioInfo{Name: path, Type: komputer.FileFormat_MP3})) + } + + return +} + +func (a audioServer) Add(server komputer.AudioService_AddServer) error { + for { + au, err := server.Recv() + if err != nil { + return err + } + + path := audio.Path(fmt.Sprintf("%s-%s.%s", au.Info.Name, uuid.NewString(), au.Info.Type.String())) + + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + f.Close() + + m := sync.Mutex{} + + m.Lock() + if _, err := f.Write(au.Chunk); err != nil { + m.Unlock() + return err + } + m.Unlock() + } +} + +func (a audioServer) Remove(ctx context.Context, request *komputer.NameOrIdAudioRequest) (e *emptypb.Empty, err error) { + e = &emptypb.Empty{} + if request == nil || len(request.Query) == 0 { + return e, nil + } + + var wg sync.WaitGroup + for _, query := range request.Query { + if query == nil { + continue + } + + wg.Add(1) + go func(q *komputer.FileQuery) { + err = remove(ctx, &wg, q) + }(query) + } + wg.Wait() + + return +} + +func remove(ctx context.Context, wg *sync.WaitGroup, query *komputer.FileQuery) error { + defer wg.Done() + select { + case <-ctx.Done(): + return context.Canceled + default: + } + + uid, name := query.GetUuid(), query.GetName() + + var path string + if uid != nil && name != "" { + path = audio.Path(fmt.Sprintf("%s-%s", name, uid.Uuid)) + } + + if path == "" { + return os.ErrNotExist + } + + return os.Remove(path) +} diff --git a/server/file.go b/server/file.go new file mode 100644 index 0000000..7d57b04 --- /dev/null +++ b/server/file.go @@ -0,0 +1,69 @@ +package server + +import ( + "context" + "errors" + "fmt" + komputer "github.com/wittano/komputer/api/proto" + "github.com/wittano/komputer/audio" + "io" + "log/slog" + "os" +) + +type fileServer struct { + komputer.UnimplementedAudioFileServiceServer +} + +func (fs fileServer) Download(request *komputer.DownloadRequest, server komputer.AudioFileService_DownloadServer) error { + if request == nil { + return errors.New("download: missing request data") + } + + path := audio.Path(filename(request)) + f, err := os.Open(path) + if err != nil { + slog.Error("failed find f "+path, err) + return err + } + defer f.Close() + + ctx := server.Context() + + buf := make([]byte, 1024) + for { + select { + case <-ctx.Done(): + return context.Canceled + default: + } + + n, err := f.Read(buf) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return err + } + + if err = server.Send(&komputer.FileBuffer{Content: buf, Size: uint64(n)}); err != nil { + return err + } + } + + return nil +} + +func filename(request *komputer.DownloadRequest) (name string) { + if request == nil { + return + } + + var uuid *komputer.UUID + uuid, name = request.GetUuid(), request.GetName() + + if uuid == nil { + return + } + + return fmt.Sprintf("%s-%s", name, uuid) +} diff --git a/server/joke.go b/server/joke.go index 149c397..c545c64 100644 --- a/server/joke.go +++ b/server/joke.go @@ -41,12 +41,8 @@ func (j jokeServer) FindAll(identity *komputer.JokeParamsPagination, server komp return err } - page := identity.Page - if page == nil { - page = &komputer.Pagination{Size: 10} - } - - jokes, err := j.Db.Jokes(server.Context(), p, identity.Page) + page := paginationOrDefault(identity.Page) + jokes, err := j.Db.Jokes(server.Context(), p, page) if err != nil { return err } diff --git a/server/pagination.go b/server/pagination.go new file mode 100644 index 0000000..f813463 --- /dev/null +++ b/server/pagination.go @@ -0,0 +1,13 @@ +package server + +import komputer "github.com/wittano/komputer/api/proto" + +func paginationOrDefault(p *komputer.Pagination) *komputer.Pagination { + if p == nil { + return &komputer.Pagination{ + Size: 10, + } + } + + return p +} diff --git a/server/server.go b/server/server.go index 2403c82..034ead8 100644 --- a/server/server.go +++ b/server/server.go @@ -43,6 +43,8 @@ func newGRPGServer() *grpc.Server { mongodb := db.Mongodb(ctx) komputer.RegisterJokeServiceServer(s, &jokeServer{Db: joke.Database{Mongodb: mongodb}}) + komputer.RegisterAudioServiceServer(s, &audioServer{}) + komputer.RegisterAudioFileServiceServer(s, &fileServer{}) return s }