Skip to content

Commit

Permalink
feat(server): Import or Export Project Functionality (File resource) (#…
Browse files Browse the repository at this point in the history
…1151)

Co-authored-by: tomokazu tantaka <[email protected]>
  • Loading branch information
hexaforce and tomokazu tantaka authored Sep 26, 2024
1 parent d428b46 commit b240bfc
Show file tree
Hide file tree
Showing 26 changed files with 755 additions and 534 deletions.
433 changes: 8 additions & 425 deletions server/e2e/gql_import_export_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/gql/project.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ type DeleteProjectPayload {
}

type ExportProjectPayload {
projectData: JSON!
projectDataPath: String!
}

type ImportProjectPayload {
Expand Down
32 changes: 16 additions & 16 deletions server/internal/adapter/gql/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/internal/adapter/gql/gqlmodel/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 100 additions & 21 deletions server/internal/adapter/gql/resolver_mutation_project.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package gql

import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"io"
"fmt"
"os"
"strings"
"time"

"github.com/oklog/ulid"
"github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel"
"github.com/reearth/reearth/server/internal/usecase/interfaces"
"github.com/reearth/reearth/server/pkg/file"
"github.com/reearth/reearth/server/pkg/id"
"github.com/reearth/reearth/server/pkg/visualizer"
"github.com/reearth/reearthx/account/accountdomain"
"github.com/spf13/afero"
"golang.org/x/exp/rand"
)

func (r *mutationResolver) CreateProject(ctx context.Context, input gqlmodel.CreateProjectInput) (*gqlmodel.ProjectPayload, error) {
Expand Down Expand Up @@ -111,34 +120,104 @@ func (r *mutationResolver) DeleteProject(ctx context.Context, input gqlmodel.Del
}

func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.ExportProjectInput) (*gqlmodel.ExportProjectPayload, error) {
fs := afero.NewOsFs()

t := time.Now().UTC()
entropy := ulid.Monotonic(rand.New(rand.NewSource(uint64(t.UnixNano()))), 0)
name := ulid.MustNew(ulid.Timestamp(t), entropy)
zipFile, err := fs.Create(fmt.Sprintf("%s.reearth", name.String()))
if err != nil {
return nil, err
}
defer func() {
if cerr := zipFile.Close(); cerr != nil && err == nil {
err = cerr
}
// delete after saving to storage
if cerr := os.Remove(zipFile.Name()); cerr != nil && err == nil {
err = cerr
}
}()
zipWriter := zip.NewWriter(zipFile)
defer func() {
if cerr := zipWriter.Close(); cerr != nil && err == nil {
err = cerr
}
}()

// export project
pid, err := gqlmodel.ToID[id.Project](input.ProjectID)
if err != nil {
return nil, err
}
prj, err := usecases(ctx).Project.ExportProject(ctx, pid, zipWriter, getOperator(ctx))
if err != nil {
return nil, err
}

sce, data, err := usecases(ctx).Scene.ExportScene(ctx, prj, zipWriter)
if err != nil {
return nil, err
}
data["project"] = gqlmodel.ToProject(prj)

plgs, err := usecases(ctx).Plugin.ExportPlugins(ctx, sce, zipWriter)
if err != nil {
return nil, err
}
data["plugins"] = gqlmodel.ToPlugins(plgs)

prj, res, plgs, err := usecases(ctx).Project.ExportProject(ctx, pid, getOperator(ctx))
err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, data, prj)
if err != nil {
return nil, err
}
res["project"] = gqlmodel.ToProject(prj)
res["plugins"] = gqlmodel.ToPlugins(plgs)

return &gqlmodel.ExportProjectPayload{
ProjectData: res,
ProjectDataPath: "/export/" + zipFile.Name(),
}, nil

}

func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.ImportProjectInput) (*gqlmodel.ImportProjectPayload, error) {

fileBytes, err := io.ReadAll(input.File.File)
data, assets, plugins, err := file.UncompressExportZip(input.File.File)
if err != nil {
return nil, err
}

// Assets file import
changedFileName := make(map[string]string)
for fileName, file := range assets {
parts1 := strings.Split(fileName, "/")
beforeName := parts1[0]

url, _, err := usecases(ctx).Asset.UploadAssetFile(ctx, beforeName, file)
if err != nil {
return nil, err
}
parts2 := strings.Split(url.Path, "/")
afterName := parts2[len(parts2)-1]

changedFileName[beforeName] = afterName
}

// Plugin file import
for fileName, file := range plugins {
parts := strings.Split(fileName, "/")
pid, err := id.PluginIDFrom(parts[0])
if err != nil {
return nil, err
}
if err := usecases(ctx).Plugin.ImporPluginFile(ctx, pid, parts[1], file); err != nil {
return nil, err
}
}

for beforeName, afterName := range changedFileName {
data = bytes.Replace(data, []byte(beforeName), []byte(afterName), -1)
}

var jsonData map[string]interface{}
if err := json.Unmarshal(fileBytes, &jsonData); err != nil {
if err := json.Unmarshal(data, &jsonData); err != nil {
return nil, err
}

Expand All @@ -154,43 +233,43 @@ func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.Imp
}()

pluginsData, _ := jsonData["plugins"].([]interface{})
_, err = usecases(ctx).Plugin.ImportPlugins(ctx, pluginsData)
plgs, err := usecases(ctx).Plugin.ImportPlugins(ctx, pluginsData)
if err != nil {
return nil, err
}

sceneData, _ := jsonData["scene"].(map[string]interface{})
_, err = usecases(ctx).Scene.ImportScene(ctx, prj, sceneData)
sce, err := usecases(ctx).Scene.ImportScene(ctx, prj, plgs, sceneData)
if err != nil {
return nil, err
}

_, err = usecases(ctx).NLSLayer.ImportNLSLayers(ctx, sceneData)
nlayers, err := usecases(ctx).NLSLayer.ImportNLSLayers(ctx, sceneData)
if err != nil {
return nil, err
}

_, err = usecases(ctx).Style.ImportStyles(ctx, sceneData)
styleList, err := usecases(ctx).Style.ImportStyles(ctx, sceneData)
if err != nil {
return nil, err
}

_, err = usecases(ctx).StoryTelling.ImportStory(ctx, sceneData)
st, err := usecases(ctx).StoryTelling.ImportStory(ctx, sceneData)
if err != nil {
return nil, err
}

tx.Commit()

prj, res, plgs, err := usecases(ctx).Project.ExportProject(ctx, prj.ID(), getOperator(ctx))
if err != nil {
return nil, err
}
res["project"] = gqlmodel.ToProject(prj)
res["plugins"] = gqlmodel.ToPlugins(plgs)

return &gqlmodel.ImportProjectPayload{
ProjectData: res,
ProjectData: map[string]any{
"project": gqlmodel.ToProject(prj),
"plugins": gqlmodel.ToPlugins(plgs),
"scene": gqlmodel.ToScene(sce),
"nlsLayer": gqlmodel.ToNLSLayers(nlayers, nil),
"style": gqlmodel.ToStyles(styleList),
"story": gqlmodel.ToStory(st),
},
}, nil

}
14 changes: 14 additions & 0 deletions server/internal/app/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ func serveFiles(
}),
)

ec.GET(
"/export/:filename",
fileHandler(func(ctx echo.Context) (io.Reader, string, error) {
filename := ctx.Param("filename")
r, err := repo.ReadExportProjectZip(ctx.Request().Context(), filename)
if err != nil {
return nil, "", rerror.ErrNotFound
}
// download and then delete
err = repo.RemoveExportProjectZip(ctx.Request().Context(), filename)
return r, filename, err
}),
)

ec.GET(
"/plugins/:plugin/:filename",
fileHandler(func(ctx echo.Context) (io.Reader, string, error) {
Expand Down
1 change: 1 addition & 0 deletions server/internal/infrastructure/fs/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ const (
pluginDir = "plugins"
publishedDir = "published"
storyDir = "stories"
exportDir = "export"
manifestFilePath = "reearth.yml"
)
20 changes: 20 additions & 0 deletions server/internal/infrastructure/fs/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ func (f *fileRepo) RemoveStory(ctx context.Context, name string) error {
return f.delete(ctx, filepath.Join(storyDir, sanitize.Path(name+".json")))
}

// export

func (f *fileRepo) ReadExportProjectZip(ctx context.Context, filename string) (io.ReadCloser, error) {
return f.read(ctx, filepath.Join(exportDir, sanitize.Path(filename)))
}

func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile afero.File) error {

file, ok := zipFile.(*os.File)
if !ok {
return errors.New("invalid file type: expected *os.File")
}
_, err := f.upload(ctx, path.Join(exportDir, sanitize.Path(file.Name())), file)
return err
}

func (f *fileRepo) RemoveExportProjectZip(ctx context.Context, filename string) error {
return f.delete(ctx, filepath.Join(exportDir, sanitize.Path(filename)))
}

// helpers

func (f *fileRepo) read(ctx context.Context, filename string) (io.ReadCloser, error) {
Expand Down
Loading

0 comments on commit b240bfc

Please sign in to comment.