diff --git a/server/e2e/publicapi_test.go b/server/e2e/publicapi_test.go index 93705591f1..6506b1da19 100644 --- a/server/e2e/publicapi_test.go +++ b/server/e2e/publicapi_test.go @@ -203,16 +203,42 @@ func TestPublicAPI(t *testing.T) { "error": "not found", }) + e.GET("/api/p/{project}/assets", publicAPIProjectAlias). + Expect(). + Status(http.StatusOK). + JSON(). + IsEqual(map[string]any{ + "hasMore": false, + "limit": 50, + "offset": 0, + "page": 1, + "results": []map[string]any{ + map[string]any{ + "id": publicAPIAsset1ID.String(), + "type": "asset", + "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + "contentType": "application/zip", + "files": []string{ + fmt.Sprintf("https://example.com/assets/%s/%s/aaa/bbb.txt", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + fmt.Sprintf("https://example.com/assets/%s/%s/aaa/ccc.txt", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + }, + }, + }, + "totalCount": 1, + }) + e.GET("/api/p/{project}/assets/{assetid}", publicAPIProjectAlias, publicAPIAsset1ID). Expect(). Status(http.StatusOK). JSON(). IsEqual(map[string]any{ - "type": "asset", - "id": publicAPIAsset1ID.String(), - "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + "type": "asset", + "id": publicAPIAsset1ID.String(), + "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + "contentType": "application/zip", "files": []string{ fmt.Sprintf("https://example.com/assets/%s/%s/aaa/bbb.txt", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + fmt.Sprintf("https://example.com/assets/%s/%s/aaa/ccc.txt", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), }, }) @@ -354,7 +380,8 @@ func publicAPISeeder(ctx context.Context, r *repo.Container) error { a := asset.New().ID(publicAPIAsset1ID).Project(p1.ID()).CreatedByUser(uid).Size(1).Thread(id.NewThreadID()). FileName("aaa.zip").UUID(publicAPIAssetUUID).MustBuild() - af := asset.NewFile().Name("bbb.txt").Path("aaa/bbb.txt").Build() + c := []*asset.File{asset.NewFile().Name("bbb.txt").Path("aaa/bbb.txt").Build(), asset.NewFile().Name("ccc.txt").Path("aaa/ccc.txt").Build()} + af := asset.NewFile().Name("aaa.zip").Path("aaa.zip").ContentType("application/zip").Size(10).Children(c).Build() fid := id.NewFieldID() gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} diff --git a/server/internal/adapter/publicapi/api.go b/server/internal/adapter/publicapi/api.go index 998767889f..4385fa8242 100644 --- a/server/internal/adapter/publicapi/api.go +++ b/server/internal/adapter/publicapi/api.go @@ -33,7 +33,7 @@ func GetController(ctx context.Context) *Controller { func Echo(e *echo.Group) { e.Use(middleware.CORS()) - e.GET("/:project/:model", PublicApiItemList()) + e.GET("/:project/:model", PublicApiItemOrAssetList()) e.GET("/:project/:model/:item", PublicApiItemOrAsset()) } @@ -59,33 +59,42 @@ func PublicApiItemOrAsset() echo.HandlerFunc { } } -func PublicApiItemList() echo.HandlerFunc { +func PublicApiItemOrAssetList() echo.HandlerFunc { return func(c echo.Context) error { ctx := c.Request().Context() ctrl := GetController(ctx) + mKey := c.Param("model") + pKey := c.Param("project") p, err := listParamFromEchoContext(c) if err != nil { return c.JSON(http.StatusBadRequest, "invalid offset or limit") } + if mKey == "assets" { + res, err := ctrl.GetAssets(ctx, pKey, p) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) + } + resType := "" - m := c.Param("model") - if strings.Contains(m, ".") { - m, resType, _ = strings.Cut(m, ".") + if strings.Contains(mKey, ".") { + mKey, resType, _ = strings.Cut(mKey, ".") } if resType != "csv" && resType != "json" && resType != "geojson" { resType = "json" } - items, _, err := ctrl.GetItems(ctx, c.Param("project"), m, p) + res, _, err := ctrl.GetItems(ctx, pKey, mKey, p) if err != nil { return err } - vi, s, err1 := ctrl.GetVersionedItems(ctx, c.Param("project"), m, p) - if err1 != nil { - return err1 + vi, s, err := ctrl.GetVersionedItems(ctx, pKey, mKey, p) + if err != nil { + return err } switch resType { @@ -94,9 +103,9 @@ func PublicApiItemList() echo.HandlerFunc { case "geojson": return toGeoJSON(c, vi, s) case "json": - return c.JSON(http.StatusOK, items) + return c.JSON(http.StatusOK, res) default: - return c.JSON(http.StatusOK, items) + return c.JSON(http.StatusOK, res) } } } diff --git a/server/internal/adapter/publicapi/asset.go b/server/internal/adapter/publicapi/asset.go index c7c49985a8..b2bee3ecd4 100644 --- a/server/internal/adapter/publicapi/asset.go +++ b/server/internal/adapter/publicapi/asset.go @@ -4,8 +4,12 @@ import ( "context" "errors" + "github.com/reearth/reearth-cms/server/internal/usecase/interfaces" + "github.com/reearth/reearth-cms/server/pkg/asset" "github.com/reearth/reearth-cms/server/pkg/id" + "github.com/reearth/reearth-cms/server/pkg/project" "github.com/reearth/reearthx/rerror" + "github.com/reearth/reearthx/util" ) func (c *Controller) GetAsset(ctx context.Context, prj, i string) (Asset, error) { @@ -34,3 +38,36 @@ func (c *Controller) GetAsset(ctx context.Context, prj, i string) (Asset, error) return NewAsset(a, f, c.assetUrlResolver), nil } + +func (c *Controller) GetAssets(ctx context.Context, pKey string, p ListParam) (ListResult[Asset], error) { + prj, err := c.checkProject(ctx, pKey) + if err != nil { + return ListResult[Asset]{}, err + } + + if prj.Publication().Scope() != project.PublicationScopePublic || !prj.Publication().AssetPublic() { + return ListResult[Asset]{}, rerror.ErrNotFound + } + + al, pi, err := c.usecases.Asset.FindByProject(ctx, prj.ID(), interfaces.AssetFilter{ + Sort: nil, + Keyword: nil, + Pagination: p.Pagination, + }, nil) + + if err != nil { + if errors.Is(err, rerror.ErrNotFound) { + return ListResult[Asset]{}, rerror.ErrNotFound + } + return ListResult[Asset]{}, err + } + + fileMap, err := c.usecases.Asset.FindFilesByIDs(ctx, al.IDs(), nil) + if err != nil { + return ListResult[Asset]{}, err + } + + return NewListResult(util.Map(al, func(a *asset.Asset) Asset { + return NewAsset(a, fileMap[a.ID()], c.assetUrlResolver) + }), pi, p.Pagination), nil +} diff --git a/server/internal/adapter/publicapi/types.go b/server/internal/adapter/publicapi/types.go index 020fcd3c2b..4e1d495e91 100644 --- a/server/internal/adapter/publicapi/types.go +++ b/server/internal/adapter/publicapi/types.go @@ -177,9 +177,9 @@ func NewAsset(a *asset.Asset, f *asset.File, urlResolver asset.URLResolver) Asse base, _ := url.Parse(u) base.Path = path.Dir(base.Path) - files = lo.Map(f.FlattenChildren(), func(f *asset.File, _ int) string { + files = lo.Map(f.FilePaths(), func(p string, _ int) string { b := *base - b.Path = path.Join(b.Path, f.Path()) + b.Path = path.Join(b.Path, p) return b.String() }) } diff --git a/server/internal/infrastructure/memory/asset_file.go b/server/internal/infrastructure/memory/asset_file.go index 28c2967ecf..762b893483 100644 --- a/server/internal/infrastructure/memory/asset_file.go +++ b/server/internal/infrastructure/memory/asset_file.go @@ -41,6 +41,32 @@ func (r *AssetFile) FindByID(ctx context.Context, id id.AssetID) (*asset.File, e return rerror.ErrIfNil(f, rerror.ErrNotFound) } +func (r *AssetFile) FindByIDs(ctx context.Context, ids id.AssetIDList) (map[id.AssetID]*asset.File, error) { + if r.err != nil { + return nil, r.err + } + + filesMap := make(map[id.AssetID]*asset.File) + for _, id := range ids { + f := r.data.Find(func(key asset.ID, value *asset.File) bool { + return key == id + }).Clone() + fs := r.files.Find(func(key asset.ID, value []*asset.File) bool { + return key == id + }) + if len(fs) > 0 { + f.SetFiles(fs) + } + if f == nil { + return nil, rerror.ErrNotFound + } + filesMap[id] = f + } + + return filesMap, nil +} + + func (r *AssetFile) Save(ctx context.Context, id id.AssetID, file *asset.File) error { if r.err != nil { return r.err diff --git a/server/internal/infrastructure/mongo/asset_file.go b/server/internal/infrastructure/mongo/asset_file.go index ddf5c532db..48eb1992fd 100644 --- a/server/internal/infrastructure/mongo/asset_file.go +++ b/server/internal/infrastructure/mongo/asset_file.go @@ -74,6 +74,52 @@ func (r *AssetFile) FindByID(ctx context.Context, id id.AssetID) (*asset.File, e return f, nil } +func (r *AssetFile) FindByIDs(ctx context.Context, ids id.AssetIDList) (map[id.AssetID]*asset.File, error) { + filesMap := make(map[id.AssetID]*asset.File) + + c := &mongodoc.AssetAndFileConsumer{} + if err := r.client.Find(ctx, bson.M{ + "id": bson.M{"$in": ids.Strings()}, + }, c, options.Find().SetProjection(bson.M{ + "id": 1, + "file": 1, + "flatfiles": 1, + })); err != nil { + return nil, err + } + + for _, result := range c.Result { + assetID := result.ID + f := result.File.Model() + if f == nil { + return nil, rerror.ErrNotFound + } + + if result.FlatFiles { + var afc mongodoc.AssetFilesConsumer + if err := r.assetFilesClient.Find(ctx, bson.M{ + "assetid": assetID, + }, &afc, options.Find().SetSort(bson.D{ + {Key: "page", Value: 1}, + })); err != nil { + return nil, err + } + files := afc.Result().Model() + f.SetFiles(files) + } else if len(f.Children()) > 0 { + f.SetFiles(f.FlattenChildren()) + } + + aId, err := id.AssetIDFrom(assetID) + if err != nil { + return nil, err + } + filesMap[aId] = f + } + + return filesMap, nil +} + func (r *AssetFile) Save(ctx context.Context, id id.AssetID, file *asset.File) error { doc := mongodoc.NewFile(file) _, err := r.client.Client().UpdateOne(ctx, bson.M{ diff --git a/server/internal/usecase/interactor/asset.go b/server/internal/usecase/interactor/asset.go index 0551fe30c2..79815b6085 100644 --- a/server/internal/usecase/interactor/asset.go +++ b/server/internal/usecase/interactor/asset.go @@ -69,6 +69,20 @@ func (i *Asset) FindFileByID(ctx context.Context, aid id.AssetID, _ *usecase.Ope return files, nil } +func (i *Asset) FindFilesByIDs(ctx context.Context, ids id.AssetIDList, _ *usecase.Operator) (map[id.AssetID]*asset.File, error) { + _, err := i.repos.Asset.FindByIDs(ctx, ids) + if err != nil { + return nil, err + } + + files, err := i.repos.AssetFile.FindByIDs(ctx, ids) + if err != nil { + return nil, err + } + + return files, nil +} + func (i *Asset) DownloadByID(ctx context.Context, aid id.AssetID, _ *usecase.Operator) (io.ReadCloser, error) { a, err := i.repos.Asset.FindByID(ctx, aid) if err != nil { diff --git a/server/internal/usecase/interfaces/asset.go b/server/internal/usecase/interfaces/asset.go index 7fafca5e55..958258306e 100644 --- a/server/internal/usecase/interfaces/asset.go +++ b/server/internal/usecase/interfaces/asset.go @@ -61,6 +61,7 @@ type Asset interface { FindByIDs(context.Context, []id.AssetID, *usecase.Operator) (asset.List, error) FindByProject(context.Context, id.ProjectID, AssetFilter, *usecase.Operator) (asset.List, *usecasex.PageInfo, error) FindFileByID(context.Context, id.AssetID, *usecase.Operator) (*asset.File, error) + FindFilesByIDs(context.Context, id.AssetIDList, *usecase.Operator) (map[id.AssetID]*asset.File, error) DownloadByID(context.Context, id.AssetID, *usecase.Operator) (io.ReadCloser, error) GetURL(*asset.Asset) string Create(context.Context, CreateAssetParam, *usecase.Operator) (*asset.Asset, *asset.File, error) diff --git a/server/internal/usecase/repo/asset.go b/server/internal/usecase/repo/asset.go index 67cb1275e0..a7bc23b2dd 100644 --- a/server/internal/usecase/repo/asset.go +++ b/server/internal/usecase/repo/asset.go @@ -25,6 +25,7 @@ type Asset interface { type AssetFile interface { FindByID(context.Context, id.AssetID) (*asset.File, error) + FindByIDs(context.Context, id.AssetIDList) (map[id.AssetID]*asset.File, error) Save(context.Context, id.AssetID, *asset.File) error SaveFlat(context.Context, id.AssetID, *asset.File, []*asset.File) error } diff --git a/server/pkg/asset/list.go b/server/pkg/asset/list.go index e9cc7cd886..6c384b9d26 100644 --- a/server/pkg/asset/list.go +++ b/server/pkg/asset/list.go @@ -1,6 +1,7 @@ package asset import ( + "github.com/reearth/reearth-cms/server/pkg/id" "github.com/reearth/reearthx/util" "github.com/samber/lo" "golang.org/x/exp/slices" @@ -27,3 +28,10 @@ func (l List) Map() Map { return a.ID(), a }) } + +func (l List) IDs() (ids id.AssetIDList) { + for _, a := range l { + ids = ids.Add(a.ID()) + } + return +} diff --git a/server/pkg/asset/list_test.go b/server/pkg/asset/list_test.go index 099a8478b0..84809293d6 100644 --- a/server/pkg/asset/list_test.go +++ b/server/pkg/asset/list_test.go @@ -3,6 +3,7 @@ package asset import ( "testing" + "github.com/reearth/reearth-cms/server/pkg/id" "github.com/reearth/reearthx/account/accountdomain" "github.com/stretchr/testify/assert" ) @@ -51,3 +52,12 @@ func TestList_Map(t *testing.T) { }, List{a, nil}.Map()) assert.Equal(t, Map{}, List(nil).Map()) } + +func TestList_IDs(t *testing.T) { + pid := NewProjectID() + uid := accountdomain.NewUserID() + a1 := New().NewID().Project(pid).CreatedByUser(uid).Size(1000).Thread(NewThreadID()).NewUUID().MustBuild() + a2 := New().NewID().Project(pid).CreatedByUser(uid).Size(1000).Thread(NewThreadID()).NewUUID().MustBuild() + al := List{a1, a2} + assert.Equal(t, al.IDs(), id.AssetIDList{a1.ID(), a2.ID()}) +}