diff --git a/api/.golangci.yaml b/api/.golangci.yaml index 3ced8a3..80d942e 100644 --- a/api/.golangci.yaml +++ b/api/.golangci.yaml @@ -7,3 +7,7 @@ linters: - gofumpt - gci - exhaustivestruct +linters-settings: + lll: + line-length: 128 + tab-width: 2 diff --git a/api/cmd/iris/api/main.go b/api/cmd/iris/api/main.go index fd49494..f1ac396 100644 --- a/api/cmd/iris/api/main.go +++ b/api/cmd/iris/api/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "time" "iris/api/internal/config" "iris/api/internal/graph/generated" @@ -16,6 +17,7 @@ import ( "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" "github.com/go-chi/chi" + "github.com/linxGnu/goseaweedfs" "github.com/rs/cors" ) @@ -32,6 +34,13 @@ func main() { panic(err) } + seaweed, err := goseaweedfs.NewSeaweed( + cfg.CDN.URL, nil, cfg.CDN.ChunkSize, + &http.Client{Timeout: time.Duration(cfg.CDN.Timeout) * time.Minute}) + if err != nil { + panic(err) + } + router := chi.NewRouter() router.Use(cors.Default().Handler) @@ -39,6 +48,7 @@ func main() { Resolvers: &resolvers.Resolver{ Config: &cfg, DB: db, + CDN: seaweed, }, } @@ -46,6 +56,7 @@ func main() { srv.AddTransport(transport.Options{}) srv.AddTransport(transport.POST{}) srv.AddTransport(transport.GET{}) + srv.AddTransport(transport.MultipartForm{}) srv.Use(extension.Introspection{}) router.Handle("/", playground.Handler("graphql playground", "/graphql")) diff --git a/api/go.mod b/api/go.mod index d56fd53..2bff76b 100644 --- a/api/go.mod +++ b/api/go.mod @@ -10,6 +10,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/kelseyhightower/envconfig v1.4.0 github.com/klauspost/compress v1.13.5 // indirect + github.com/linxGnu/goseaweedfs v0.1.5 // indirect github.com/rs/cors v1.8.0 github.com/vektah/gqlparser/v2 v2.1.0 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect diff --git a/api/go.sum b/api/go.sum index 38ff991..cbe27aa 100644 --- a/api/go.sum +++ b/api/go.sum @@ -81,6 +81,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/linxGnu/goseaweedfs v0.1.5 h1:7dChPdq8+fsPH0yqxKEofPhiosaar4LWePm4M+1Taz0= +github.com/linxGnu/goseaweedfs v0.1.5/go.mod h1:Zwe/7H7FJaPQyMTNKXgv6fhVDw6qi34MMJQp1K0VLNc= +github.com/linxGnu/gumble v1.0.0 h1:OAJud8Hy4rmV9I5p/KTRiVpwwklMTd9Ankza3Mz7a4M= +github.com/linxGnu/gumble v1.0.0/go.mod h1:iyhNJpBHvJ0q2Hr41iiZRJyj6LLF47i2a9C9zLiucVY= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= @@ -111,6 +115,7 @@ github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/scryner/lfreequeue v0.0.0-20121212074822-473f33702129/go.mod h1:0OrdloYlIayHGsgKYlwEnmdrPWmuYtbdS6Dm71PprFM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= @@ -136,6 +141,7 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns= github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= diff --git a/api/internal/config/config.go b/api/internal/config/config.go index 41996de..8bbf0cb 100644 --- a/api/internal/config/config.go +++ b/api/internal/config/config.go @@ -1,6 +1,8 @@ package config -import "github.com/kelseyhightower/envconfig" +import ( + "github.com/kelseyhightower/envconfig" +) type ( Database struct { @@ -8,8 +10,15 @@ type ( Name string `enconfig:"DB_NAME" default:"iris"` } + CDN struct { + URL string `envconfig:"CDN_URL" default:"http://storage-master:5020"` + ChunkSize int64 `envconfig:"CDN_CHUNK_SIZE" default:"1048576"` + Timeout int64 `envconfig:"CDN_TIMEOUT" default:"5"` + } + Config struct { Database + CDN Port int `envconfig:"PORT" default:"5001"` } ) diff --git a/api/internal/graph/generated/generated.go b/api/internal/graph/generated/generated.go index 722bd54..abe6e56 100644 --- a/api/internal/graph/generated/generated.go +++ b/api/internal/graph/generated/generated.go @@ -10,6 +10,7 @@ import ( "strconv" "sync" "sync/atomic" + "time" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" @@ -60,6 +61,7 @@ type ComplexityRoot struct { CreatedAt func(childComplexity int) int Description func(childComplexity int) int FileName func(childComplexity int) int + FileSize func(childComplexity int) int ID func(childComplexity int) int ImageURL func(childComplexity int) int MediaMetadata func(childComplexity int) int @@ -199,6 +201,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MediaItem.FileName(childComplexity), true + case "MediaItem.fileSize": + if e.complexity.MediaItem.FileSize == nil { + break + } + + return e.complexity.MediaItem.FileSize(childComplexity), true + case "MediaItem.id": if e.complexity.MediaItem.ID == nil { break @@ -462,6 +471,7 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er var sources = []*ast.Source{ {Name: "schema.graphql", Input: `scalar Upload +scalar Time type MediaItem { id: String! @@ -469,9 +479,10 @@ type MediaItem { imageUrl: String! mimeType: String! fileName: String! + fileSize: Int! mediaMetadata: MediaMetaData! - createdAt: String! - updatedAt: String! + createdAt: Time! + updatedAt: Time! } type MediaMetaData { @@ -1136,6 +1147,41 @@ func (ec *executionContext) _MediaItem_fileName(ctx context.Context, field graph return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _MediaItem_fileSize(ctx context.Context, field graphql.CollectedField, obj *models.MediaItem) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MediaItem", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.FileSize, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int64) + fc.Result = res + return ec.marshalNInt2int64(ctx, field.Selections, res) +} + func (ec *executionContext) _MediaItem_mediaMetadata(ctx context.Context, field graphql.CollectedField, obj *models.MediaItem) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1201,9 +1247,9 @@ func (ec *executionContext) _MediaItem_createdAt(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) _MediaItem_updatedAt(ctx context.Context, field graphql.CollectedField, obj *models.MediaItem) (ret graphql.Marshaler) { @@ -1236,9 +1282,9 @@ func (ec *executionContext) _MediaItem_updatedAt(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) _MediaItemConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *models.MediaItemConnection) (ret graphql.Marshaler) { @@ -3196,6 +3242,11 @@ func (ec *executionContext) _MediaItem(ctx context.Context, sel ast.SelectionSet if out.Values[i] == graphql.Null { invalids++ } + case "fileSize": + out.Values[i] = ec._MediaItem_fileSize(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "mediaMetadata": out.Values[i] = ec._MediaItem_mediaMetadata(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -3759,6 +3810,21 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) unmarshalNInt2int64(ctx context.Context, v interface{}) (int64, error) { + res, err := graphql.UnmarshalInt64(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int64(ctx context.Context, sel ast.SelectionSet, v int64) graphql.Marshaler { + res := graphql.MarshalInt64(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + } + return res +} + func (ec *executionContext) marshalNMediaItem2irisᚋapiᚋinternalᚋmodelsᚐMediaItem(ctx context.Context, sel ast.SelectionSet, v models.MediaItem) graphql.Marshaler { return ec._MediaItem(ctx, sel, &v) } @@ -3812,6 +3878,21 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } +func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) { + res, err := graphql.UnmarshalTime(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNTime2timeᚐTime(ctx context.Context, sel ast.SelectionSet, v time.Time) graphql.Marshaler { + res := graphql.MarshalTime(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + } + return res +} + func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v interface{}) (graphql.Upload, error) { res, err := graphql.UnmarshalUpload(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/api/internal/graph/resolvers/resolver.go b/api/internal/graph/resolvers/resolver.go index 7870935..8945a44 100644 --- a/api/internal/graph/resolvers/resolver.go +++ b/api/internal/graph/resolvers/resolver.go @@ -3,6 +3,8 @@ package resolvers import ( "iris/api/internal/config" "iris/api/pkg/mongo" + + "github.com/linxGnu/goseaweedfs" ) // This file will not be regenerated automatically. @@ -12,4 +14,5 @@ import ( type Resolver struct { Config *config.Config DB *mongo.Connection + CDN *goseaweedfs.Seaweed } diff --git a/api/internal/graph/resolvers/schema.resolvers.go b/api/internal/graph/resolvers/schema.resolvers.go index a73e1fa..ece35fd 100644 --- a/api/internal/graph/resolvers/schema.resolvers.go +++ b/api/internal/graph/resolvers/schema.resolvers.go @@ -6,8 +6,10 @@ package resolvers import ( "context" "errors" + "fmt" "iris/api/internal/graph/generated" "iris/api/internal/models" + "time" "github.com/99designs/gqlgen/graphql" "go.mongodb.org/mongo-driver/bson" @@ -16,7 +18,28 @@ import ( ) func (r *mutationResolver) Upload(ctx context.Context, file graphql.Upload) (bool, error) { - return false, nil + result, err := r.CDN.Upload(file.File, file.Filename, file.Size, "", "") + if err != nil { + return false, err + } + + // later(omkar): Calculate media metadata + + _, err = r.DB.Collection(models.ColMediaItems).InsertOne(ctx, bson.D{ + {Key: "imageUrl", Value: fmt.Sprintf("http://%s/%s", result.Server, result.FileID)}, + {Key: "description", Value: nil}, + {Key: "mimeType", Value: result.MimeType}, + {Key: "fileName", Value: result.FileName}, + {Key: "fileSize", Value: result.FileSize}, + {Key: "mediaMetadata", Value: nil}, + {Key: "createdAt", Value: time.Now()}, + {Key: "updatedAt", Value: time.Now()}, + }) + if err != nil { + return false, err + } + + return true, nil } func (r *mutationResolver) UpdateEntity(ctx context.Context, id string, name string) (bool, error) { @@ -98,8 +121,7 @@ func (r *queryResolver) MediaItems(ctx context.Context, page *int, limit *int) ( }, nil } -func (r *queryResolver) Search(ctx context.Context, q string, - page *int, limit *int) (*models.MediaItemConnection, error) { +func (r *queryResolver) Search(ctx context.Context, q string, page *int, limit *int) (*models.MediaItemConnection, error) { return nil, nil } @@ -107,8 +129,7 @@ func (r *queryResolver) Explore(ctx context.Context) (*models.ExploreResponse, e return nil, nil } -func (r *queryResolver) Entity(ctx context.Context, id string, - page *int, limit *int) (*models.MediaItemConnection, error) { +func (r *queryResolver) Entity(ctx context.Context, id string, page *int, limit *int) (*models.MediaItemConnection, error) { return nil, nil } diff --git a/api/internal/models/mediaitem.go b/api/internal/models/mediaitem.go index 29a9b79..87093f5 100644 --- a/api/internal/models/mediaitem.go +++ b/api/internal/models/mediaitem.go @@ -1,5 +1,7 @@ package models +import "time" + const ColMediaItems = "mediaitems" type ( @@ -9,9 +11,10 @@ type ( ImageURL string `json:"imageUrl"` MimeType string `json:"mimeType"` FileName string `json:"fileName"` + FileSize int64 `json:"fileSize"` MediaMetadata *MediaMetaData `json:"mediaMetadata"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } MediaMetaData struct { diff --git a/api/schema.graphql b/api/schema.graphql index f4dc7fd..791d070 100644 --- a/api/schema.graphql +++ b/api/schema.graphql @@ -1,4 +1,5 @@ scalar Upload +scalar Time type MediaItem { id: String! @@ -6,9 +7,10 @@ type MediaItem { imageUrl: String! mimeType: String! fileName: String! + fileSize: Int! mediaMetadata: MediaMetaData! - createdAt: String! - updatedAt: String! + createdAt: Time! + updatedAt: Time! } type MediaMetaData { diff --git a/docker-compose.yaml b/docker-compose.yaml index 2345fce..1d11a02 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -37,13 +37,15 @@ services: image: chrislusf/seaweedfs ports: - '5020:5020' - command: "master -ip=storage-master -port=5020" + - '15020:15020' + command: 'master -ip=storage-master -port=5020' storage-volume: container_name: storage-volume image: chrislusf/seaweedfs ports: - '5021:5021' - command: 'volume -mserver="storage-master:5020" -port=5021' + - '15021:15021' + command: 'volume -ip=storage-volume -mserver="storage-master:5020" -port=5021' depends_on: - storage-master networks: