diff --git a/server/e2e/integration_asset_project_test.go b/server/e2e/integration_asset_project_test.go index 40ed13d9e8..a22f4ede0c 100644 --- a/server/e2e/integration_asset_project_test.go +++ b/server/e2e/integration_asset_project_test.go @@ -116,7 +116,6 @@ func TestIntegrationCreateAssetAPI(t *testing.T) { Status(http.StatusOK). JSON(). Object(). - // HasValue("id", aid1.String()). HasValue("projectId", pid). HasValue("name", "testFile.jpg"). HasValue("contentType", "image/jpeg"). diff --git a/server/e2e/integration_asset_test.go b/server/e2e/integration_asset_test.go index 1887f45870..44c1ab59df 100644 --- a/server/e2e/integration_asset_test.go +++ b/server/e2e/integration_asset_test.go @@ -2,27 +2,12 @@ package e2e import ( "net/http" - "strings" "testing" - "github.com/gavv/httpexpect/v2" "github.com/reearth/reearth-cms/server/internal/app" "github.com/reearth/reearth-cms/server/pkg/id" ) -func UploadAsset(e *httpexpect.Expect, pId string, path string, content string) *httpexpect.Value { - res := e.POST("/api/projects/{projectId}/assets", pId). - WithHeader("X-Reearth-Debug-User", uId1.String()). - WithMultipart(). - WithFile("file", path, strings.NewReader(content)). - WithForm(map[string]any{"skipDecompression": true}). - Expect(). - Status(http.StatusOK). - JSON() - - return res -} - // GET /assets/{assetId} func TestIntegrationGetAssetAPI(t *testing.T) { e := StartServer(t, &app.Config{}, true, baseSeeder) diff --git a/server/e2e/integration_item_import_test.go b/server/e2e/integration_item_import_test.go index 839165bd55..11877c558a 100644 --- a/server/e2e/integration_item_import_test.go +++ b/server/e2e/integration_item_import_test.go @@ -101,7 +101,7 @@ func TestIntegrationModelImportJSONWithGeoJsonInput(t *testing.T) { // strategy="insert" and mutateSchema=false fileContent1 := `{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [139.28179282584915,36.58570985749664]}, "properties": {"text": "test2"}}]}` - aId := UploadAsset(e, pId, "./test1.geojson", fileContent1).Object().Value("id").String().Raw() + aId := uploadAsset(e, pId, "./test1.geojson", fileContent1).Object().Value("id").String().Raw() res := IntegrationModelImportJSON(e, mId, aId, "geoJson", "insert", false, &fids.geometryObjectFid) res.Object().Value("modelId").String().IsEqual(mId) res.Object().IsEqual(map[string]any{ @@ -168,7 +168,7 @@ func TestIntegrationModelImportJSONWithJsonInput1(t *testing.T) { createFieldOfEachType(t, e, mId) jsonContent := `[{"text": "test1", "bool": true, "number": 1.1},{"text": "test2", "bool": false, "number": 2},{"text": "test3", "bool": null, "number": null}]` - aId := UploadAsset(e, pId, "./test1.json", jsonContent).Object().Value("id").String().Raw() + aId := uploadAsset(e, pId, "./test1.json", jsonContent).Object().Value("id").String().Raw() res := IntegrationModelImportJSON(e, mId, aId, "json", "insert", false, nil) res.Object().IsEqual(map[string]any{ "modelId": mId, @@ -273,7 +273,7 @@ func TestIntegrationModelImportJSONWithJsonInput2(t *testing.T) { iId := r.Value("id").String().Raw() jsonContent := `[{"id": "` + iId + `","text": "test1", "bool": true, "number": 1.1},{"text": "test2", "bool": false, "number": 2},{"text": "test3", "bool": null, "number": null}]` - aId := UploadAsset(e, pId, "./test1.json", jsonContent).Object().Value("id").String().Raw() + aId := uploadAsset(e, pId, "./test1.json", jsonContent).Object().Value("id").String().Raw() res := IntegrationModelImportJSON(e, mId, aId, "json", "upsert", true, nil) res.Object().IsEqual(map[string]any{ "modelId": mId, @@ -310,3 +310,16 @@ func TestIntegrationModelImportJSONWithJsonInput2(t *testing.T) { i.Value("fields").Array().Length().IsEqual(1) // endregion } + +func uploadAsset(e *httpexpect.Expect, pId string, path string, content string) *httpexpect.Value { + res := e.POST("/api/projects/{projectId}/assets", pId). + WithHeader("X-Reearth-Debug-User", uId1.String()). + WithMultipart(). + WithFile("file", path, strings.NewReader(content)). + WithForm(map[string]any{"skipDecompression": true}). + Expect(). + Status(http.StatusOK). + JSON() + + return res +} diff --git a/server/i18n/en.yml b/server/i18n/en.yml index 0342743b88..72676f4962 100644 --- a/server/i18n/en.yml +++ b/server/i18n/en.yml @@ -84,6 +84,7 @@ reviewer should be owner or maintainer: "" thread is required: "" title cannot be empty: "" unauthorized: "" +unsupported content encoding: "" unsupported entity: "" unsupported geometry type: "" unsupported operation: "" diff --git a/server/i18n/ja.yml b/server/i18n/ja.yml index fa7d6f60e9..294e485f15 100644 --- a/server/i18n/ja.yml +++ b/server/i18n/ja.yml @@ -84,8 +84,9 @@ reviewer should be owner or maintainer: レビュワーはオーナーもしく thread is required: スレッドは必須です。 title cannot be empty: タイトルは必須です。 unauthorized: 未認証 -unsupported entity: サポートされていないエンティティ -unsupported geometry type: サポートされていないジオメトリタイプ +unsupported content encoding: サポートされていないContent-Encodingです。 +unsupported entity: サポートされていないエンティティです。 +unsupported geometry type: サポートされていないジオメトリタイプです。 unsupported operation: サポートされていない処理です。 uuid is required: UUIDは必須です。 value is required: 値は必須です。 diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go index 6808dddc05..f670adff1b 100644 --- a/server/internal/adapter/gql/generated.go +++ b/server/internal/adapter/gql/generated.go @@ -77,6 +77,7 @@ type ComplexityRoot struct { Asset struct { ArchiveExtractionStatus func(childComplexity int) int + ContentEncoding func(childComplexity int) int CreatedAt func(childComplexity int) int CreatedBy func(childComplexity int) int CreatedByID func(childComplexity int) int @@ -107,11 +108,12 @@ type ComplexityRoot struct { } AssetFile struct { - ContentType func(childComplexity int) int - FilePaths func(childComplexity int) int - Name func(childComplexity int) int - Path func(childComplexity int) int - Size func(childComplexity int) int + ContentEncoding func(childComplexity int) int + ContentType func(childComplexity int) int + FilePaths func(childComplexity int) int + Name func(childComplexity int) int + Path func(childComplexity int) int + Size func(childComplexity int) int } AssetItem struct { @@ -165,11 +167,12 @@ type ComplexityRoot struct { } CreateAssetUploadPayload struct { - ContentLength func(childComplexity int) int - ContentType func(childComplexity int) int - Next func(childComplexity int) int - Token func(childComplexity int) int - URL func(childComplexity int) int + ContentEncoding func(childComplexity int) int + ContentLength func(childComplexity int) int + ContentType func(childComplexity int) int + Next func(childComplexity int) int + Token func(childComplexity int) int + URL func(childComplexity int) int } CreateWorkspacePayload struct { @@ -1102,6 +1105,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Asset.ArchiveExtractionStatus(childComplexity), true + case "Asset.contentEncoding": + if e.complexity.Asset.ContentEncoding == nil { + break + } + + return e.complexity.Asset.ContentEncoding(childComplexity), true + case "Asset.createdAt": if e.complexity.Asset.CreatedAt == nil { break @@ -1249,6 +1259,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AssetEdge.Node(childComplexity), true + case "AssetFile.contentEncoding": + if e.complexity.AssetFile.ContentEncoding == nil { + break + } + + return e.complexity.AssetFile.ContentEncoding(childComplexity), true + case "AssetFile.contentType": if e.complexity.AssetFile.ContentType == nil { break @@ -1466,6 +1483,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.CreateAssetPayload.Asset(childComplexity), true + case "CreateAssetUploadPayload.contentEncoding": + if e.complexity.CreateAssetUploadPayload.ContentEncoding == nil { + break + } + + return e.complexity.CreateAssetUploadPayload.ContentEncoding(childComplexity), true + case "CreateAssetUploadPayload.contentLength": if e.complexity.CreateAssetUploadPayload.ContentLength == nil { break @@ -5082,6 +5106,7 @@ schema { items: [AssetItem!] size: FileSize! previewType: PreviewType + contentEncoding: String uuid: String! thread: Thread threadId: ID! @@ -5089,6 +5114,7 @@ schema { fileName: String! archiveExtractionStatus: ArchiveExtractionStatus } + type AssetItem { itemId: ID! modelId: ID! @@ -5098,6 +5124,7 @@ type AssetFile { name: String! size: FileSize! contentType: String + contentEncoding: String path: String! filePaths: [String!] } @@ -5127,6 +5154,8 @@ input CreateAssetInput { url: String token: String skipDecompression: Boolean + # specify "gzip" if you want to uplaod a gzip file so that the server can serve it with the correct content-encoding. + contentEncoding: String } # If ` + "`" + `cursor` + "`" + ` is specified, both ` + "`" + `filename` + "`" + ` and ` + "`" + `contentLength` + "`" + ` will be ignored. @@ -5137,6 +5166,8 @@ input CreateAssetUploadInput { filename: String # The size of the file to upload. contentLength: Int + # specify "gzip" if you want to uplaod a gzip file so that the server can serve it with the correct content-encoding. + contentEncoding: String # Required if uploading in multiple parts. cursor: String @@ -5184,6 +5215,7 @@ type CreateAssetUploadPayload { contentType: String # The size of the upload. contentLength: Int! + contentEncoding: String # A cursor to obtain the URL for the next PUT request. next: String } @@ -10506,6 +10538,47 @@ func (ec *executionContext) fieldContext_Asset_previewType(_ context.Context, fi return fc, nil } +func (ec *executionContext) _Asset_contentEncoding(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Asset) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Asset_contentEncoding(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ContentEncoding, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Asset_contentEncoding(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Asset", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Asset_uuid(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Asset) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Asset_uuid(ctx, field) if err != nil { @@ -10883,6 +10956,8 @@ func (ec *executionContext) fieldContext_AssetConnection_nodes(_ context.Context return ec.fieldContext_Asset_size(ctx, field) case "previewType": return ec.fieldContext_Asset_previewType(ctx, field) + case "contentEncoding": + return ec.fieldContext_Asset_contentEncoding(ctx, field) case "uuid": return ec.fieldContext_Asset_uuid(ctx, field) case "thread": @@ -11100,6 +11175,8 @@ func (ec *executionContext) fieldContext_AssetEdge_node(_ context.Context, field return ec.fieldContext_Asset_size(ctx, field) case "previewType": return ec.fieldContext_Asset_previewType(ctx, field) + case "contentEncoding": + return ec.fieldContext_Asset_contentEncoding(ctx, field) case "uuid": return ec.fieldContext_Asset_uuid(ctx, field) case "thread": @@ -11248,6 +11325,47 @@ func (ec *executionContext) fieldContext_AssetFile_contentType(_ context.Context return fc, nil } +func (ec *executionContext) _AssetFile_contentEncoding(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.AssetFile) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AssetFile_contentEncoding(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ContentEncoding, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AssetFile_contentEncoding(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AssetFile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _AssetFile_path(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.AssetFile) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AssetFile_path(ctx, field) if err != nil { @@ -12535,6 +12653,8 @@ func (ec *executionContext) fieldContext_CreateAssetPayload_asset(_ context.Cont return ec.fieldContext_Asset_size(ctx, field) case "previewType": return ec.fieldContext_Asset_previewType(ctx, field) + case "contentEncoding": + return ec.fieldContext_Asset_contentEncoding(ctx, field) case "uuid": return ec.fieldContext_Asset_uuid(ctx, field) case "thread": @@ -12727,6 +12847,47 @@ func (ec *executionContext) fieldContext_CreateAssetUploadPayload_contentLength( return fc, nil } +func (ec *executionContext) _CreateAssetUploadPayload_contentEncoding(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.CreateAssetUploadPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CreateAssetUploadPayload_contentEncoding(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ContentEncoding, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CreateAssetUploadPayload_contentEncoding(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreateAssetUploadPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _CreateAssetUploadPayload_next(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.CreateAssetUploadPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_CreateAssetUploadPayload_next(ctx, field) if err != nil { @@ -12881,6 +13042,8 @@ func (ec *executionContext) fieldContext_DecompressAssetPayload_asset(_ context. return ec.fieldContext_Asset_size(ctx, field) case "previewType": return ec.fieldContext_Asset_previewType(ctx, field) + case "contentEncoding": + return ec.fieldContext_Asset_contentEncoding(ctx, field) case "uuid": return ec.fieldContext_Asset_uuid(ctx, field) case "thread": @@ -15991,6 +16154,8 @@ func (ec *executionContext) fieldContext_Item_assets(_ context.Context, field gr return ec.fieldContext_Asset_size(ctx, field) case "previewType": return ec.fieldContext_Asset_previewType(ctx, field) + case "contentEncoding": + return ec.fieldContext_Asset_contentEncoding(ctx, field) case "uuid": return ec.fieldContext_Asset_uuid(ctx, field) case "thread": @@ -19526,6 +19691,8 @@ func (ec *executionContext) fieldContext_Mutation_createAssetUpload(ctx context. return ec.fieldContext_CreateAssetUploadPayload_contentType(ctx, field) case "contentLength": return ec.fieldContext_CreateAssetUploadPayload_contentLength(ctx, field) + case "contentEncoding": + return ec.fieldContext_CreateAssetUploadPayload_contentEncoding(ctx, field) case "next": return ec.fieldContext_CreateAssetUploadPayload_next(ctx, field) } @@ -24421,6 +24588,8 @@ func (ec *executionContext) fieldContext_Query_assetFile(ctx context.Context, fi return ec.fieldContext_AssetFile_size(ctx, field) case "contentType": return ec.fieldContext_AssetFile_contentType(ctx, field) + case "contentEncoding": + return ec.fieldContext_AssetFile_contentEncoding(ctx, field) case "path": return ec.fieldContext_AssetFile_path(ctx, field) case "filePaths": @@ -31036,6 +31205,8 @@ func (ec *executionContext) fieldContext_UpdateAssetPayload_asset(_ context.Cont return ec.fieldContext_Asset_size(ctx, field) case "previewType": return ec.fieldContext_Asset_previewType(ctx, field) + case "contentEncoding": + return ec.fieldContext_Asset_contentEncoding(ctx, field) case "uuid": return ec.fieldContext_Asset_uuid(ctx, field) case "thread": @@ -36324,7 +36495,7 @@ func (ec *executionContext) unmarshalInputCreateAssetInput(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"projectId", "file", "url", "token", "skipDecompression"} + fieldsInOrder := [...]string{"projectId", "file", "url", "token", "skipDecompression", "contentEncoding"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -36366,6 +36537,13 @@ func (ec *executionContext) unmarshalInputCreateAssetInput(ctx context.Context, return it, err } it.SkipDecompression = data + case "contentEncoding": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contentEncoding")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.ContentEncoding = data } } @@ -36379,7 +36557,7 @@ func (ec *executionContext) unmarshalInputCreateAssetUploadInput(ctx context.Con asMap[k] = v } - fieldsInOrder := [...]string{"projectId", "filename", "contentLength", "cursor"} + fieldsInOrder := [...]string{"projectId", "filename", "contentLength", "contentEncoding", "cursor"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -36407,6 +36585,13 @@ func (ec *executionContext) unmarshalInputCreateAssetUploadInput(ctx context.Con return it, err } it.ContentLength = data + case "contentEncoding": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contentEncoding")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.ContentEncoding = data case "cursor": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("cursor")) data, err := ec.unmarshalOString2ᚖstring(ctx, v) @@ -41547,6 +41732,8 @@ func (ec *executionContext) _Asset(ctx context.Context, sel ast.SelectionSet, ob } case "previewType": out.Values[i] = ec._Asset_previewType(ctx, field, obj) + case "contentEncoding": + out.Values[i] = ec._Asset_contentEncoding(ctx, field, obj) case "uuid": out.Values[i] = ec._Asset_uuid(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -41743,6 +41930,8 @@ func (ec *executionContext) _AssetFile(ctx context.Context, sel ast.SelectionSet } case "contentType": out.Values[i] = ec._AssetFile_contentType(ctx, field, obj) + case "contentEncoding": + out.Values[i] = ec._AssetFile_contentEncoding(ctx, field, obj) case "path": out.Values[i] = ec._AssetFile_path(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -42231,6 +42420,8 @@ func (ec *executionContext) _CreateAssetUploadPayload(ctx context.Context, sel a if out.Values[i] == graphql.Null { out.Invalids++ } + case "contentEncoding": + out.Values[i] = ec._CreateAssetUploadPayload_contentEncoding(ctx, field, obj) case "next": out.Values[i] = ec._CreateAssetUploadPayload_next(ctx, field, obj) default: diff --git a/server/internal/adapter/gql/gqlmodel/convert_asset.go b/server/internal/adapter/gql/gqlmodel/convert_asset.go index c1772f671e..445c71e588 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_asset.go +++ b/server/internal/adapter/gql/gqlmodel/convert_asset.go @@ -133,11 +133,12 @@ func ToAssetFile(a *asset.File) *AssetFile { } return &AssetFile{ - Name: a.Name(), - Size: int64(a.Size()), - ContentType: lo.ToPtr(a.ContentType()), - Path: a.Path(), - FilePaths: a.FilePaths(), + Name: a.Name(), + Size: int64(a.Size()), + ContentType: lo.EmptyableToPtr(a.ContentType()), + ContentEncoding: lo.EmptyableToPtr(a.ContentEncoding()), + Path: a.Path(), + FilePaths: a.FilePaths(), } } diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go index e6f99269c5..3df93ff5f3 100644 --- a/server/internal/adapter/gql/gqlmodel/models_gen.go +++ b/server/internal/adapter/gql/gqlmodel/models_gen.go @@ -84,6 +84,7 @@ type Asset struct { Items []*AssetItem `json:"items,omitempty"` Size int64 `json:"size"` PreviewType *PreviewType `json:"previewType,omitempty"` + ContentEncoding *string `json:"contentEncoding,omitempty"` UUID string `json:"uuid"` Thread *Thread `json:"thread,omitempty"` ThreadID ID `json:"threadId"` @@ -108,11 +109,12 @@ type AssetEdge struct { } type AssetFile struct { - Name string `json:"name"` - Size int64 `json:"size"` - ContentType *string `json:"contentType,omitempty"` - Path string `json:"path"` - FilePaths []string `json:"filePaths,omitempty"` + Name string `json:"name"` + Size int64 `json:"size"` + ContentType *string `json:"contentType,omitempty"` + ContentEncoding *string `json:"contentEncoding,omitempty"` + Path string `json:"path"` + FilePaths []string `json:"filePaths,omitempty"` } type AssetItem struct { @@ -221,6 +223,7 @@ type CreateAssetInput struct { URL *string `json:"url,omitempty"` Token *string `json:"token,omitempty"` SkipDecompression *bool `json:"skipDecompression,omitempty"` + ContentEncoding *string `json:"contentEncoding,omitempty"` } type CreateAssetPayload struct { @@ -228,18 +231,20 @@ type CreateAssetPayload struct { } type CreateAssetUploadInput struct { - ProjectID ID `json:"projectId"` - Filename *string `json:"filename,omitempty"` - ContentLength *int `json:"contentLength,omitempty"` - Cursor *string `json:"cursor,omitempty"` + ProjectID ID `json:"projectId"` + Filename *string `json:"filename,omitempty"` + ContentLength *int `json:"contentLength,omitempty"` + ContentEncoding *string `json:"contentEncoding,omitempty"` + Cursor *string `json:"cursor,omitempty"` } type CreateAssetUploadPayload struct { - Token string `json:"token"` - URL string `json:"url"` - ContentType *string `json:"contentType,omitempty"` - ContentLength int `json:"contentLength"` - Next *string `json:"next,omitempty"` + Token string `json:"token"` + URL string `json:"url"` + ContentType *string `json:"contentType,omitempty"` + ContentLength int `json:"contentLength"` + ContentEncoding *string `json:"contentEncoding,omitempty"` + Next *string `json:"next,omitempty"` } type CreateFieldInput struct { diff --git a/server/internal/adapter/gql/loader_integration.go b/server/internal/adapter/gql/loader_integration.go index 845b4029a3..d4d459fad4 100644 --- a/server/internal/adapter/gql/loader_integration.go +++ b/server/internal/adapter/gql/loader_integration.go @@ -26,9 +26,6 @@ func (c *IntegrationLoader) Fetch(ctx context.Context, ids []gqlmodel.ID) ([]*gq } op := getOperator(ctx) - if err != nil { - return nil, []error{err} - } res, err := c.usecase.FindByIDs(ctx, iIDs, op) if err != nil { diff --git a/server/internal/adapter/gql/resolver_asset.go b/server/internal/adapter/gql/resolver_asset.go index 5c797141e6..e7eaa1b055 100644 --- a/server/internal/adapter/gql/resolver_asset.go +++ b/server/internal/adapter/gql/resolver_asset.go @@ -46,27 +46,25 @@ func (r *assetResolver) Thread(ctx context.Context, obj *gqlmodel.Asset) (*gqlmo // CreateAsset is the resolver for the createAsset field. func (r *mutationResolver) CreateAsset(ctx context.Context, input gqlmodel.CreateAssetInput) (*gqlmodel.CreateAssetPayload, error) { + uc := usecases(ctx).Asset + pid, err := gqlmodel.ToID[id.Project](input.ProjectID) if err != nil { return nil, err } - uc := usecases(ctx).Asset + params := interfaces.CreateAssetParam{ ProjectID: pid, File: gqlmodel.FromFile(input.File), } if input.URL != nil { - params.File, err = file.FromURL(*input.URL) + params.File, err = file.FromURL(ctx, *input.URL) if err != nil { return nil, err } } - if input.Token != nil { - params.Token = *input.Token - } - if input.SkipDecompression != nil { - params.SkipDecompression = *input.SkipDecompression - } + params.Token = lo.FromPtr(input.Token) + params.SkipDecompression = lo.FromPtr(input.SkipDecompression) res, _, err := uc.Create(ctx, params, getOperator(ctx)) if err != nil { @@ -137,10 +135,11 @@ func (r *mutationResolver) CreateAssetUpload(ctx context.Context, input gqlmodel return nil, err } au, err := usecases(ctx).Asset.CreateUpload(ctx, interfaces.CreateAssetUploadParam{ - ProjectID: pid, - Filename: lo.FromPtr(input.Filename), - ContentLength: int64(lo.FromPtr(input.ContentLength)), - Cursor: lo.FromPtr(input.Cursor), + ProjectID: pid, + Filename: lo.FromPtr(input.Filename), + ContentLength: int64(lo.FromPtr(input.ContentLength)), + ContentEncoding: lo.FromPtr(input.ContentEncoding), + Cursor: lo.FromPtr(input.Cursor), }, getOperator(ctx)) if err != nil { return nil, err diff --git a/server/internal/adapter/integration/asset.go b/server/internal/adapter/integration/asset.go index d04c333260..e80141cbf0 100644 --- a/server/internal/adapter/integration/asset.go +++ b/server/internal/adapter/integration/asset.go @@ -70,7 +70,8 @@ func (s *Server) AssetCreate(ctx context.Context, request AssetCreateRequestObje var f *file.File var token string - skipDecompression := false + var skipDecompression bool + var err error if request.MultipartBody != nil { var inp integrationapi.AssetCreateMultipartBody @@ -85,11 +86,11 @@ func (s *Server) AssetCreate(ctx context.Context, request AssetCreateRequestObje return AssetCreate400Response{}, err } f = &file.File{ - Content: fc, - Name: inp.File.Filename(), - Size: inp.File.FileSize(), - // ContentType: inp.File.ContentType(), - ContentType: "", + Content: fc, + Name: inp.File.Filename(), + Size: inp.File.FileSize(), + ContentType: lo.FromPtr(inp.ContentType), // TODO: check HTTP header also + ContentEncoding: lo.FromPtr(inp.ContentEncoding), // TODO: check HTTP header also } skipDecompression = lo.FromPtrOr(inp.SkipDecompression, false) } @@ -100,7 +101,7 @@ func (s *Server) AssetCreate(ctx context.Context, request AssetCreateRequestObje } token = lo.FromPtr(request.JSONBody.Token) if request.JSONBody.Url != nil { - f, err = file.FromURL(*request.JSONBody.Url) + f, err = file.FromURL(ctx, *request.JSONBody.Url) if err != nil { return AssetCreate400Response{}, err } @@ -170,10 +171,11 @@ func (s *Server) AssetUploadCreate(ctx context.Context, request AssetUploadCreat uc := adapter.Usecases(ctx) op := adapter.Operator(ctx) au, err := uc.Asset.CreateUpload(ctx, interfaces.CreateAssetUploadParam{ - ProjectID: request.ProjectId, - Filename: lo.FromPtr(request.Body.Name), - ContentLength: int64(lo.FromPtr(request.Body.ContentLength)), - Cursor: lo.FromPtr(request.Body.Cursor), + ProjectID: request.ProjectId, + Filename: lo.FromPtr(request.Body.Name), + ContentLength: int64(lo.FromPtr(request.Body.ContentLength)), + ContentEncoding: lo.FromPtr(request.Body.ContentEncoding), + Cursor: lo.FromPtr(request.Body.Cursor), }, op) if err != nil { diff --git a/server/internal/adapter/integration/convert.go b/server/internal/adapter/integration/convert.go index 7ee7667eb7..30d99eb4c8 100644 --- a/server/internal/adapter/integration/convert.go +++ b/server/internal/adapter/integration/convert.go @@ -46,7 +46,7 @@ func Page(p usecasex.OffsetPagination) int { return int(p.Offset/int64(p.Limit)) + 1 } -func fromItemFieldParam(f integrationapi.Field, sf *schema.Field) interfaces.ItemFieldParam { +func fromItemFieldParam(f integrationapi.Field, _ *schema.Field) interfaces.ItemFieldParam { var v any = f.Value if f.Value != nil { v = *f.Value @@ -198,7 +198,7 @@ func fromQuery(sp schema.Package, req ItemFilterRequestObject) *item.Query { WithFilter(c) } -func fromSort(sp schema.Package, sort integrationapi.ItemFilterParamsSort, dir *integrationapi.ItemFilterParamsDir) *view.Sort { +func fromSort(_ schema.Package, sort integrationapi.ItemFilterParamsSort, dir *integrationapi.ItemFilterParamsDir) *view.Sort { if dir == nil { dir = lo.ToPtr(integrationapi.ItemFilterParamsDirAsc) } @@ -227,6 +227,6 @@ func fromSort(sp schema.Package, sort integrationapi.ItemFilterParamsSort, dir * return nil } -func fromCondition(sp schema.Package, condition integrationapi.Condition) *view.Condition { +func fromCondition(_ schema.Package, condition integrationapi.Condition) *view.Condition { return condition.Into() } diff --git a/server/internal/adapter/integration/item.go b/server/internal/adapter/integration/item.go index 479db985ec..71fefc9710 100644 --- a/server/internal/adapter/integration/item.go +++ b/server/internal/adapter/integration/item.go @@ -161,9 +161,6 @@ func (s *Server) ItemFilterWithProject(ctx context.Context, request ItemFilterWi } metaSchemas, metaItems := getMetaSchemasAndItems(ctx, items) - if err != nil { - return ItemFilterWithProject400Response{}, err - } res, err := util.TryMap(items, func(i item.Versioned) (integrationapi.VersionedItem, error) { metaItem, _ := lo.Find(metaItems, func(itm item.Versioned) bool { @@ -431,9 +428,6 @@ func (s *Server) ItemGet(ctx context.Context, request ItemGetRequestObject) (Ite } msList, miList := getMetaSchemasAndItems(ctx, item.VersionedList{i}) - if err != nil { - return ItemGet400Response{}, err - } var mi item.Versioned var ms *schema.Schema diff --git a/server/internal/adapter/integration/server.gen.go b/server/internal/adapter/integration/server.gen.go index 048e1b8ba3..83533f0f63 100644 --- a/server/internal/adapter/integration/server.gen.go +++ b/server/internal/adapter/integration/server.gen.go @@ -5287,79 +5287,80 @@ func (sh *strictHandler) ProjectFilter(ctx echo.Context, workspaceId WorkspaceId // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "ZVar1f8HAAD//z8HlnZatQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/internal/infrastructure/fs/file.go b/server/internal/infrastructure/fs/file.go index d65d323327..92fed26b24 100644 --- a/server/internal/infrastructure/fs/file.go +++ b/server/internal/infrastructure/fs/file.go @@ -96,6 +96,9 @@ func (f *fileRepo) UploadAsset(_ context.Context, file *file.File) (string, int6 if file.Size >= fileSizeLimit { return "", 0, gateway.ErrFileTooLarge } + if file.ContentEncoding != "" && file.ContentEncoding != "identity" { + return "", 0, gateway.ErrUnsupportedContentEncoding + } fileUUID := newUUID() diff --git a/server/internal/infrastructure/gcp/file.go b/server/internal/infrastructure/gcp/file.go index 1cbe03ff00..17b3d54f2c 100644 --- a/server/internal/infrastructure/gcp/file.go +++ b/server/internal/infrastructure/gcp/file.go @@ -89,8 +89,10 @@ func (f *fileRepo) GetAssetFiles(ctx context.Context, u string) ([]gateway.FileE fe := gateway.FileEntry{ // /22/2232222233333/hoge/tileset.json -> hoge/tileset.json - Name: strings.TrimPrefix(strings.TrimPrefix(attrs.Name, p), "/"), - Size: attrs.Size, + Name: strings.TrimPrefix(strings.TrimPrefix(attrs.Name, p), "/"), + Size: attrs.Size, + ContentType: attrs.ContentType, + ContentEncoding: attrs.ContentEncoding, } fileEntries = append(fileEntries, fe) } @@ -117,7 +119,7 @@ func (f *fileRepo) UploadAsset(ctx context.Context, file *file.File) (string, in return "", 0, gateway.ErrInvalidFile } - size, err := f.upload(ctx, p, file.Content) + size, err := f.upload(ctx, file) if err != nil { return "", 0, err } @@ -154,16 +156,20 @@ func (f *fileRepo) IssueUploadAssetLink(ctx context.Context, param gateway.Issue Expires: param.ExpiresAt, ContentType: contentType, } + if param.ContentEncoding != "" { + opt.Headers = []string{"Content-Encoding:" + param.ContentEncoding} + } uploadURL, err := bucket.SignedURL(p, opt) if err != nil { log.Warnf("gcs: failed to issue signed url: %v", err) return nil, gateway.ErrUnsupportedOperation } return &gateway.UploadAssetLink{ - URL: uploadURL, - ContentType: contentType, - ContentLength: param.ContentLength, - Next: "", + URL: uploadURL, + ContentType: contentType, + ContentLength: param.ContentLength, + ContentEncoding: param.ContentEncoding, + Next: "", }, nil } @@ -208,18 +214,26 @@ func (f *fileRepo) read(ctx context.Context, filename string) (io.ReadCloser, er return reader, nil } -func (f *fileRepo) upload(ctx context.Context, filename string, content io.Reader) (int64, error) { - if filename == "" { +func (f *fileRepo) upload(ctx context.Context, file *file.File) (int64, error) { + if file.Name == "" { return 0, gateway.ErrInvalidFile } + if err := validateContentEncoding(file.ContentEncoding); err != nil { + return 0, err + } + + if file.ContentEncoding == "identity" { + file.ContentEncoding = "" + } + bucket, err := f.bucket(ctx) if err != nil { log.Errorf("gcs: upload bucket err: %+v\n", err) return 0, rerror.ErrInternalBy(err) } - object := bucket.Object(filename) + object := bucket.Object(file.Name) if err := object.Delete(ctx); err != nil && !errors.Is(err, storage.ErrObjectNotExist) { log.Errorf("gcs: upload delete err: %+v\n", err) return 0, gateway.ErrFailedToUploadFile @@ -227,12 +241,21 @@ func (f *fileRepo) upload(ctx context.Context, filename string, content io.Reade writer := object.NewWriter(ctx) writer.ObjectAttrs.CacheControl = f.cacheControl - ct := getContentType(filename) - if ct != "" { - writer.ObjectAttrs.ContentType = ct + + if file.ContentType == "" { + writer.ObjectAttrs.ContentType = getContentType(file.Name) + } else { + writer.ObjectAttrs.ContentType = file.ContentType } - if _, err := io.Copy(writer, content); err != nil { + if file.ContentEncoding == "gzip" { + writer.ObjectAttrs.ContentEncoding = "gzip" + if writer.ObjectAttrs.ContentType == "" || writer.ObjectAttrs.ContentType == "application/gzip" { + writer.ObjectAttrs.ContentType = "application/octet-stream" + } + } + + if _, err := io.Copy(writer, file.Content); err != nil { log.Errorf("gcs: upload err: %+v\n", err) return 0, gateway.ErrFailedToUploadFile } @@ -307,3 +330,10 @@ func IsValidUUID(u string) bool { func getURL(host *url.URL, uuid, fName string) string { return host.JoinPath(gcsAssetBasePath, uuid[:2], uuid[2:], fName).String() } + +func validateContentEncoding(ce string) error { + if ce != "" && ce != "identity" && ce != "gzip" { + return gateway.ErrUnsupportedContentEncoding + } + return nil +} diff --git a/server/internal/infrastructure/mongo/mongodoc/asset.go b/server/internal/infrastructure/mongo/mongodoc/asset.go index 5ac7258015..ef1374f301 100644 --- a/server/internal/infrastructure/mongo/mongodoc/asset.go +++ b/server/internal/infrastructure/mongo/mongodoc/asset.go @@ -33,11 +33,12 @@ type AssetAndFileDocument struct { } type AssetFileDocument struct { - Name string - Size uint64 - ContentType string - Path string - Children []*AssetFileDocument + Name string + Size uint64 + ContentType string + ContentEncoding string + Path string + Children []*AssetFileDocument } type AssetConsumer = mongox.SliceFuncConsumer[*AssetDocument, *asset.Asset] @@ -142,11 +143,12 @@ func NewFile(f *asset.File) *AssetFileDocument { } return &AssetFileDocument{ - Name: f.Name(), - Size: f.Size(), - ContentType: f.ContentType(), - Path: f.Path(), - Children: c, + Name: f.Name(), + Size: f.Size(), + ContentType: f.ContentType(), + ContentEncoding: f.ContentEncoding(), + Path: f.Path(), + Children: c, } } @@ -167,6 +169,7 @@ func (f *AssetFileDocument) Model() *asset.File { Name(f.Name). Size(f.Size). ContentType(f.ContentType). + ContentEncoding(f.ContentEncoding). Path(f.Path). Children(c). Build() diff --git a/server/internal/usecase/gateway/file.go b/server/internal/usecase/gateway/file.go index 675cbbe95f..07ecd26096 100644 --- a/server/internal/usecase/gateway/file.go +++ b/server/internal/usecase/gateway/file.go @@ -15,31 +15,36 @@ import ( ) var ( - ErrInvalidFile error = rerror.NewE(i18n.T("invalid file")) - ErrFailedToUploadFile error = rerror.NewE(i18n.T("failed to upload file")) - ErrFileTooLarge error = rerror.NewE(i18n.T("file too large")) - ErrFailedToDeleteFile error = rerror.NewE(i18n.T("failed to delete file")) - ErrFileNotFound error = rerror.NewE(i18n.T("file not found")) - ErrUnsupportedOperation error = rerror.NewE(i18n.T("unsupported operation")) + ErrInvalidFile error = rerror.NewE(i18n.T("invalid file")) + ErrFailedToUploadFile error = rerror.NewE(i18n.T("failed to upload file")) + ErrFileTooLarge error = rerror.NewE(i18n.T("file too large")) + ErrFailedToDeleteFile error = rerror.NewE(i18n.T("failed to delete file")) + ErrFileNotFound error = rerror.NewE(i18n.T("file not found")) + ErrUnsupportedOperation error = rerror.NewE(i18n.T("unsupported operation")) + ErrUnsupportedContentEncoding error = rerror.NewE(i18n.T("unsupported content encoding")) ) type FileEntry struct { - Name string - Size int64 + Name string + Size int64 + ContentType string + ContentEncoding string } type UploadAssetLink struct { - URL string - ContentType string - ContentLength int64 - Next string + URL string + ContentType string + ContentLength int64 + ContentEncoding string + Next string } type IssueUploadAssetParam struct { - UUID string - Filename string - ContentLength int64 - ExpiresAt time.Time + UUID string + Filename string + ContentLength int64 + ContentEncoding string + ExpiresAt time.Time Cursor string } diff --git a/server/internal/usecase/interactor/asset.go b/server/internal/usecase/interactor/asset.go index 79815b6085..6070d8fb7f 100644 --- a/server/internal/usecase/interactor/asset.go +++ b/server/internal/usecase/interactor/asset.go @@ -110,6 +110,10 @@ func (i *Asset) Create(ctx context.Context, inp interfaces.CreateAssetParam, op return nil, nil, interfaces.ErrFileNotIncluded } + if inp.File.ContentEncoding == "gzip" { + inp.File.Name = strings.TrimSuffix(inp.File.Name, ".gz") + } + prj, err := i.repos.Project.FindByID(ctx, inp.ProjectID) if err != nil { return nil, nil, err @@ -197,7 +201,9 @@ func (i *Asset) Create(ctx context.Context, inp interfaces.CreateAssetParam, op Name(file.Name). Path(file.Name). Size(uint64(file.Size)). - GuessContentType(). + ContentType(file.ContentType). + GuessContentTypeIfEmpty(). + ContentEncoding(file.ContentEncoding). Build() if err := i.repos.Asset.Save(ctx, a); err != nil { @@ -304,6 +310,10 @@ func (i *Asset) CreateUpload(ctx context.Context, inp interfaces.CreateAssetUplo return nil, interfaces.ErrInvalidOperator } + if inp.ContentEncoding == "gzip" { + inp.Filename = strings.TrimSuffix(inp.Filename, ".gz") + } + var param *gateway.IssueUploadAssetParam if inp.Cursor == "" { if inp.Filename == "" { @@ -314,11 +324,12 @@ func (i *Asset) CreateUpload(ctx context.Context, inp interfaces.CreateAssetUplo const week = 7 * 24 * time.Hour expiresAt := time.Now().Add(1 * week) param = &gateway.IssueUploadAssetParam{ - UUID: uuid.New().String(), - Filename: inp.Filename, - ContentLength: inp.ContentLength, - ExpiresAt: expiresAt, - Cursor: "", + UUID: uuid.New().String(), + Filename: inp.Filename, + ContentLength: inp.ContentLength, + ContentEncoding: inp.ContentEncoding, + ExpiresAt: expiresAt, + Cursor: "", } } else { wrapped, err := parseWrappedUploadCursor(inp.Cursor) @@ -333,11 +344,12 @@ func (i *Asset) CreateUpload(ctx context.Context, inp interfaces.CreateAssetUplo return nil, fmt.Errorf("unmatched project id(in=%s,db=%s)", inp.ProjectID, au.Project()) } param = &gateway.IssueUploadAssetParam{ - UUID: wrapped.UUID, - Filename: au.FileName(), - ContentLength: au.ContentLength(), - ExpiresAt: au.ExpiresAt(), - Cursor: wrapped.Cursor, + UUID: wrapped.UUID, + Filename: au.FileName(), + ContentLength: au.ContentLength(), + ContentEncoding: inp.ContentEncoding, + ExpiresAt: au.ExpiresAt(), + Cursor: wrapped.Cursor, } } @@ -369,12 +381,14 @@ func (i *Asset) CreateUpload(ctx context.Context, inp interfaces.CreateAssetUplo return nil, err } } + return &interfaces.AssetUpload{ - URL: uploadLink.URL, - UUID: param.UUID, - ContentType: uploadLink.ContentType, - ContentLength: uploadLink.ContentLength, - Next: wrapUploadCursor(param.UUID, uploadLink.Next), + URL: uploadLink.URL, + UUID: param.UUID, + ContentType: uploadLink.ContentType, + ContentLength: uploadLink.ContentLength, + ContentEncoding: uploadLink.ContentEncoding, + Next: wrapUploadCursor(param.UUID, uploadLink.Next), }, nil } @@ -492,7 +506,9 @@ func (i *Asset) UpdateFiles(ctx context.Context, aid id.AssetID, s *asset.Archiv Name(path.Base(f.Name)). Path(f.Name). Size(uint64(f.Size)). - GuessContentType(). + ContentType(f.ContentType). + GuessContentTypeIfEmpty(). + ContentEncoding(f.ContentEncoding). Build(), true }) diff --git a/server/internal/usecase/interfaces/asset.go b/server/internal/usecase/interfaces/asset.go index 958258306e..ae1a40f65e 100644 --- a/server/internal/usecase/interfaces/asset.go +++ b/server/internal/usecase/interfaces/asset.go @@ -31,8 +31,9 @@ type UpdateAssetParam struct { type CreateAssetUploadParam struct { ProjectID idx.ID[id.Project] - Filename string - ContentLength int64 + Filename string + ContentLength int64 + ContentEncoding string Cursor string } @@ -49,11 +50,12 @@ type AssetFilter struct { } type AssetUpload struct { - URL string - UUID string - ContentType string - ContentLength int64 - Next string + URL string + UUID string + ContentType string + ContentLength int64 + ContentEncoding string + Next string } type Asset interface { diff --git a/server/pkg/asset/asset.go b/server/pkg/asset/asset.go index d2e46482f3..c9b0b51150 100644 --- a/server/pkg/asset/asset.go +++ b/server/pkg/asset/asset.go @@ -24,6 +24,8 @@ type Asset struct { type URLResolver = func(*Asset) string +// getters + func (a *Asset) ID() ID { return a.id } @@ -74,6 +76,16 @@ func (a *Asset) ArchiveExtractionStatus() *ArchiveExtractionStatus { return a.archiveExtractionStatus } +func (a *Asset) Thread() ThreadID { + return a.thread +} + +func (a *Asset) FlatFiles() bool { + return a.flatFiles +} + +// setters + func (a *Asset) UpdatePreviewType(p *PreviewType) { a.previewType = util.CloneRef(p) } @@ -82,6 +94,8 @@ func (a *Asset) UpdateArchiveExtractionStatus(s *ArchiveExtractionStatus) { a.archiveExtractionStatus = util.CloneRef(s) } +// methods + func (a *Asset) Clone() *Asset { if a == nil { return nil @@ -102,11 +116,3 @@ func (a *Asset) Clone() *Asset { flatFiles: a.flatFiles, } } - -func (a *Asset) Thread() ThreadID { - return a.thread -} - -func (a *Asset) FlatFiles() bool { - return a.flatFiles -} diff --git a/server/pkg/asset/file.go b/server/pkg/asset/file.go index 5a800dc550..7fa41d1785 100644 --- a/server/pkg/asset/file.go +++ b/server/pkg/asset/file.go @@ -9,14 +9,17 @@ import ( ) type File struct { - name string - size uint64 - contentType string - path string - children []*File - files []*File + name string + size uint64 + contentType string + contentEncoding string + path string + children []*File + files []*File } +// getters + func (f *File) Name() string { if f == nil { return "" @@ -42,6 +45,13 @@ func (f *File) ContentType() string { return f.contentType } +func (f *File) ContentEncoding() string { + if f == nil { + return "" + } + return f.contentEncoding +} + func (f *File) Path() string { if f == nil { return "" @@ -60,12 +70,6 @@ func (f *File) Files() []*File { return slices.Clone(f.files) } -func (f *File) SetFiles(s []*File) { - f.files = lo.Filter(s, func(af *File, _ int) bool { - return af.Path() != f.Path() - }) -} - func (f *File) FilePaths() []string { return lo.Map(f.files, func(f *File, _ int) string { return f.path }) } @@ -74,6 +78,16 @@ func (f *File) IsDir() bool { return f != nil && f.children != nil } +// setters + +func (f *File) SetFiles(s []*File) { + f.files = lo.Filter(s, func(af *File, _ int) bool { + return af.Path() != f.Path() + }) +} + +// methods + func (f *File) AppendChild(c *File) { if f == nil { return @@ -92,11 +106,12 @@ func (f *File) Clone() *File { } return &File{ - name: f.name, - size: f.size, - contentType: f.contentType, - path: f.path, - children: children, + name: f.name, + size: f.size, + contentType: f.contentType, + path: f.path, + children: children, + contentEncoding: f.contentEncoding, } } diff --git a/server/pkg/asset/file_builder.go b/server/pkg/asset/file_builder.go index 96c4ff24a5..98689a1044 100644 --- a/server/pkg/asset/file_builder.go +++ b/server/pkg/asset/file_builder.go @@ -29,6 +29,11 @@ func (b *FileBuilder) ContentType(contentType string) *FileBuilder { return b } +func (b *FileBuilder) ContentEncoding(contentEncoding string) *FileBuilder { + b.f.contentEncoding = contentEncoding + return b +} + func (b *FileBuilder) Path(filePath string) *FileBuilder { if !strings.HasPrefix(filePath, "/") && filePath != "" { filePath = "/" + filePath @@ -58,6 +63,13 @@ func (b *FileBuilder) GuessContentType() *FileBuilder { return b } +func (b *FileBuilder) GuessContentTypeIfEmpty() *FileBuilder { + if b.f.contentType == "" { + b.detectContentType = true + } + return b +} + func (b *FileBuilder) Dir() *FileBuilder { if b.f.children == nil { b.f.children = []*File{} diff --git a/server/pkg/file/file.go b/server/pkg/file/file.go index 5c882c5915..ca83646048 100644 --- a/server/pkg/file/file.go +++ b/server/pkg/file/file.go @@ -1,6 +1,8 @@ package file import ( + "context" + "errors" "fmt" "io" "mime" @@ -15,10 +17,11 @@ import ( ) type File struct { - Content io.ReadCloser - Name string - Size int64 - ContentType string + Content io.ReadCloser + Name string + Size int64 + ContentType string + ContentEncoding string } func FromMultipart(multipartReader *multipart.Reader, formName string) (*File, error) { @@ -53,13 +56,21 @@ func FromMultipart(multipartReader *multipart.Reader, formName string) (*File, e return nil, rerror.NewE(i18n.T("file not found")) } -func FromURL(rawURL string) (*File, error) { +func FromURL(ctx context.Context, rawURL string) (*File, error) { URL, err := url.Parse(rawURL) if err != nil { return nil, err } - res, err := http.Get(URL.String()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), nil) + if err != nil { + return nil, errors.New("failed to request") + } + + // TODO: support gzip + // req.Header.Set("Accept-Encoding", "gzip") + + res, err := http.DefaultClient.Do(req) if err != nil { return nil, rerror.ErrInternalBy(err) } @@ -69,6 +80,10 @@ func FromURL(rawURL string) (*File, error) { } ct := res.Header.Get("Content-Type") + ce := res.Header.Get("Content-Encoding") + if ce != "" && ce != "gzip" { + return nil, fmt.Errorf("unsupported content encoding: %s", ce) + } fs, _ := strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64) fn := path.Base(URL.Path) @@ -78,9 +93,10 @@ func FromURL(rawURL string) (*File, error) { } return &File{ - Content: res.Body, - Name: fn, - ContentType: ct, - Size: fs, + Content: res.Body, + Name: fn, + ContentType: ct, + ContentEncoding: ce, + Size: fs, }, nil } diff --git a/server/pkg/file/file_test.go b/server/pkg/file/file_test.go index 9cc7f7acc7..bfff7d0e26 100644 --- a/server/pkg/file/file_test.go +++ b/server/pkg/file/file_test.go @@ -1,6 +1,7 @@ package file import ( + "context" "io" "mime" "net/http" @@ -14,39 +15,62 @@ import ( ) func TestFromURL(t *testing.T) { - URL := "https://cms.com/xyz/test.txt" - f := lo.Must(os.Open("testdata/test.txt")) - defer f.Close() - z := lo.Must(io.ReadAll(f)) + ctx := context.Background() httpmock.Activate() defer httpmock.Deactivate() - httpmock.RegisterResponder("GET", URL, func(r *http.Request) (*http.Response, error) { - res := httpmock.NewBytesResponse(200, z) - res.Header.Set("Content-Type", mime.TypeByExtension(path.Ext(URL))) - res.Header.Set("Content-Length", "123") - res.Header.Set("Content-Disposition", `attachment; filename="filename.txt"`) - return res, nil + t.Run("with gzip encoding", func(t *testing.T) { + URL := "https://cms.com/xyz/test.txt.gz" + f := lo.Must(os.Open("testdata/test.txt")) + defer f.Close() + z := lo.Must(io.ReadAll(f)) + + httpmock.RegisterResponder("GET", URL, func(r *http.Request) (*http.Response, error) { + res := httpmock.NewBytesResponse(200, z) + res.Header.Set("Content-Type", mime.TypeByExtension(path.Ext(URL))) + res.Header.Set("Content-Length", "123") + res.Header.Set("Content-Encoding", "gzip") + return res, nil + }) + + got, err := FromURL(ctx, URL) + assert.NoError(t, err) + assert.Equal(t, "gzip", got.ContentEncoding) }) - expected := File{Name: "filename.txt", Content: f, Size: 123} + t.Run("normal", func(t *testing.T) { + URL := "https://cms.com/xyz/test.txt" + f := lo.Must(os.Open("testdata/test.txt")) + defer f.Close() + z := lo.Must(io.ReadAll(f)) - got, err := FromURL(URL) - assert.NoError(t, err) - assert.Equal(t, expected.Name, got.Name) - assert.Equal(t, z, lo.Must(io.ReadAll(got.Content))) + httpmock.RegisterResponder("GET", URL, func(r *http.Request) (*http.Response, error) { + res := httpmock.NewBytesResponse(200, z) + res.Header.Set("Content-Type", mime.TypeByExtension(path.Ext(URL))) + res.Header.Set("Content-Length", "123") + res.Header.Set("Content-Disposition", `attachment; filename="filename.txt"`) + return res, nil + }) - httpmock.RegisterResponder("GET", URL, func(r *http.Request) (*http.Response, error) { - res := httpmock.NewBytesResponse(200, z) - res.Header.Set("Content-Type", mime.TypeByExtension(path.Ext(URL))) - return res, nil - }) + expected := File{Name: "filename.txt", Content: f, Size: 123} - expected = File{Name: "test.txt", Content: f, Size: 0} + got, err := FromURL(ctx, URL) + assert.NoError(t, err) + assert.Equal(t, expected.Name, got.Name) + assert.Equal(t, z, lo.Must(io.ReadAll(got.Content))) - got, err = FromURL(URL) - assert.NoError(t, err) - assert.Equal(t, expected.Name, got.Name) - assert.Equal(t, z, lo.Must(io.ReadAll(got.Content))) + httpmock.RegisterResponder("GET", URL, func(r *http.Request) (*http.Response, error) { + res := httpmock.NewBytesResponse(200, z) + res.Header.Set("Content-Type", mime.TypeByExtension(path.Ext(URL))) + return res, nil + }) + + expected = File{Name: "test.txt", Content: f, Size: 0} + + got, err = FromURL(ctx, URL) + assert.NoError(t, err) + assert.Equal(t, expected.Name, got.Name) + assert.Equal(t, z, lo.Must(io.ReadAll(got.Content))) + }) } diff --git a/server/pkg/integrationapi/types.gen.go b/server/pkg/integrationapi/types.gen.go index 76a6a0ea28..7357dc4a02 100644 --- a/server/pkg/integrationapi/types.gen.go +++ b/server/pkg/integrationapi/types.gen.go @@ -967,6 +967,7 @@ type AssetFilterParamsDir string // AssetCreateJSONBody defines parameters for AssetCreate. type AssetCreateJSONBody struct { + ContentEncoding *string `json:"contentEncoding,omitempty"` SkipDecompression *bool `json:"skipDecompression"` Token *string `json:"token,omitempty"` Url *string `json:"url,omitempty"` @@ -974,15 +975,18 @@ type AssetCreateJSONBody struct { // AssetCreateMultipartBody defines parameters for AssetCreate. type AssetCreateMultipartBody struct { + ContentEncoding *string `json:"contentEncoding,omitempty"` + ContentType *string `json:"contentType,omitempty"` File *openapi_types.File `json:"file,omitempty"` SkipDecompression *bool `json:"skipDecompression,omitempty"` } // AssetUploadCreateJSONBody defines parameters for AssetUploadCreate. type AssetUploadCreateJSONBody struct { - ContentLength *int `json:"contentLength,omitempty"` - Cursor *string `json:"cursor,omitempty"` - Name *string `json:"name,omitempty"` + ContentEncoding *string `json:"contentEncoding,omitempty"` + ContentLength *int `json:"contentLength,omitempty"` + Cursor *string `json:"cursor,omitempty"` + Name *string `json:"name,omitempty"` } // FieldCreateJSONBody defines parameters for FieldCreate. diff --git a/server/schemas/asset.graphql b/server/schemas/asset.graphql index f333b95288..d6f55d4147 100644 --- a/server/schemas/asset.graphql +++ b/server/schemas/asset.graphql @@ -9,6 +9,7 @@ type Asset implements Node { items: [AssetItem!] size: FileSize! previewType: PreviewType + contentEncoding: String uuid: String! thread: Thread threadId: ID! @@ -16,6 +17,7 @@ type Asset implements Node { fileName: String! archiveExtractionStatus: ArchiveExtractionStatus } + type AssetItem { itemId: ID! modelId: ID! @@ -25,6 +27,7 @@ type AssetFile { name: String! size: FileSize! contentType: String + contentEncoding: String path: String! filePaths: [String!] } @@ -54,6 +57,8 @@ input CreateAssetInput { url: String token: String skipDecompression: Boolean + # specify "gzip" if you want to uplaod a gzip file so that the server can serve it with the correct content-encoding. + contentEncoding: String } # If `cursor` is specified, both `filename` and `contentLength` will be ignored. @@ -64,6 +69,8 @@ input CreateAssetUploadInput { filename: String # The size of the file to upload. contentLength: Int + # specify "gzip" if you want to uplaod a gzip file so that the server can serve it with the correct content-encoding. + contentEncoding: String # Required if uploading in multiple parts. cursor: String @@ -111,6 +118,7 @@ type CreateAssetUploadPayload { contentType: String # The size of the upload. contentLength: Int! + contentEncoding: String # A cursor to obtain the URL for the next PUT request. next: String } diff --git a/server/schemas/integration.yml b/server/schemas/integration.yml index 924af2c8c2..f3f0d4db53 100644 --- a/server/schemas/integration.yml +++ b/server/schemas/integration.yml @@ -1400,6 +1400,10 @@ paths: file: type: string format: binary + contentType: + type: string + contentEncoding: + type: string skipDecompression: type: boolean default: false @@ -1415,6 +1419,8 @@ paths: type: boolean nullable: true default: false + contentEncoding: + type: string responses: '200': description: assets @@ -1436,7 +1442,7 @@ paths: security: - bearerAuth: [] summary: Upload an asset. - description: Issue a URL and a token to upload an asset. + description: Issue an URL and a token to upload an asset. parameters: - $ref: '#/components/parameters/projectIdParam' requestBody: @@ -1449,6 +1455,8 @@ paths: type: string contentLength: type: integer + contentEncoding: + type: string cursor: type: string responses: