如何使用 Golang 构建 GraphQL API,将文件上传到 DigitalOcean Spaces

作者选择了 Diversity in Tech Fund以作为 Write for Donations计划的一部分接受捐款。

介绍

对于许多应用程序来说,一个可取的功能是用户上传个人资料图像的能力,但是,构建此功能对于新的 GraphQL 开发者来说可能是一个挑战,因为它没有内置的文件上传支持。

在此教程中, 您会直接从您的后端应用程序中将图像上传到第三方存储服务 。 您将构建一个使用 S3 兼容 [AWS GO] (https://docs.aws.amazon.com/sdk-for-go/api) 的图形QL API 。 SDK从一个Go后端应用程序上传到DigitalOcean Spaces,这是一种高度可扩展的对象存储服务. Go后端应用程序将曝光一个GraphQL API,并将用户数据存储在由DigitalOcean的管理数据库服务提供的PotsgreSQL数据库中.

在本教程结束时,您将使用 Golang 构建一个 GraphQL API,可以从 多部分 HTTP 请求接收媒体文件,并将该文件上传到 DigitalOcean Spaces

前提条件

要遵循本教程,您将需要:

步骤 1 — 启动 Golang GraphQL API

在此步骤中, 您将使用 [Gqlgen] (https://github.com/99designs/gqlgen] 库来拖曳 GraphQL API 。 Gqlgen 是用于构建 GraphQL API 的 Go 库 。 Gqglen提供的两个重要特征是先行计划方法和代码生成. 使用 schema- first 方法,您首先使用 GraphQL Schema定义语言定义API的数据模型. 然后从定义的图中生成API的锅炉板代码. 使用代码生成特性,您不需要为 API 自动创建查询和突变解析器.

要开始,请执行下面的命令来安装gqlgen:

1go install github.com/99designs/gqlgen@latest

接下来,创建一个名为digitalocean的项目目录来存储该项目的文件:

1mkdir digitalocean

转到数字海洋项目目录:

1cd digitalocean

从您的项目目录中运行以下命令来创建一个 go.mod 文件,该文件在 digitalocean 项目中管理 modules:

1go mod init digitalocean

接下来,使用 nano 或您最喜欢的文本编辑器,在项目目录中创建一个名为tools.go的文件:

1nano tools.go

将以下行列添加到 tools.go 文件中作为一个 tool的项目:

1// +build tools
2
3 package tools
4
5 import _ "github.com/99designs/gqlgen"

接下来,执行 tidy命令来安装在 tools.go 文件中引入的 gqlgen 依赖性:

1go mod tidy

最后,使用已安装的 Gqlgen 库,生成 GraphQL API 所需的 boilerplate 文件:

1gqlgen init

上面的gqlgen命令会生成一个server.go文件来运行 GraphQL 服务器和一个包含包含 GraphQL API 的 Schema 定义的schema.graphqls文件的graph目录。

在此步骤中,您使用了 Gqlgen 库来启动 GraphQL API. 接下来,您将定义 GraphQL 应用程序的方案。

步骤 2 — 定义 GraphQL 应用程序方案

在此步骤中,您将通过修改在运行gqlgen init命令时自动生成的schema.graphqls文件来定义 GraphQL 应用程序的方案。

导航到graf目录并打开schema.graphqls文件,该文件定义了 GraphQL 应用程序的方案。 用以下代码块代替 boilerplate 方案,该代码块定义了用户类型,用Query来检索所有用户数据和Mutation来插入数据:

 1[label schema.graphqls]
 2
 3scalar Upload
 4
 5type User {
 6  id: ID!
 7  fullName: String!
 8  email: String!
 9  img_uri: String!
10  DateCreated: String!
11}
12
13type Query {
14  users: [User]!
15}
16
17input NewUser {
18  fullName: String!
19  email: String!
20  img_uri: String
21  DateCreated: String
22}
23
24input ProfileImage {
25  userId: String
26  file: Upload
27}
28
29type Mutation {
30  createUser(input: NewUser!): User!
31  uploadProfileImage(input: ProfileImage!): Boolean!
32}

代码块定义了两个突变类型和一个单一的查询类型来检索所有用户。 mutation 用于在 GraphQL 应用程序中插入或突变现有数据,而 query 用于检索数据,类似于 REST API 中的GET HTTP 词汇。

上面的代码块中的方案使用了 GraphQL Schema Definition Language来定义包含CreateUser类型的突变,它接受NewUser输入作为参数并返回单个用户

注意:Gqlgen自动定义上传尺度类型,并定义文件的属性. 要使用它,您只需要在方案文件的顶部声明它,就像在上面的代码块中一样。

在此时,您已经定义了应用程序的数据模型结构,下一步是使用Gqlgen的代码生成功能生成方案的查询和突变解析函数。

步骤3 - 生成应用程序解决方案

在此步骤中,您将使用 Gqlgen 的代码生成功能自动生成基于您在前一步创建的方案的 GraphQL 解析器. resolver 是解决或返回 GraphQL 字段的值的函数。

Gqlgen 套件是基于一个先于方案的方法。Gqlgen 的一个节省时间的功能是基于您在schema.graphqls文件中的定义方案生成您的应用程序的解析器的能力. 有了这个功能,您不需要手动写解析器板代码,这意味着您可以专注于执行定义的解析器。

若要使用代码生成功能,请在项目目录中执行下面的命令,以生成 GraphQL API 模型文件和解析器:

1gqlgen generate

執行「gqlgen」命令後會發生幾件事情. 將打印兩個與「schema.resolvers.go」檔案相關的驗證錯誤,產生一些新檔案,並且您的項目將具有新的文件夾結構。

运行树命令以查看新添加到您的项目的文件。

1tree *

目前的目录结构将类似于此:

 1[secondary_label Output]
 2
 3go.mod 
 4go.sum 
 5gqlgen.yml 
 6graph
 7  ├── db.go
 8  ├── generated
 9     └── generated.go
10  ├── model
11     └── models_gen.go
12  ├── resolver.go
13  ├── schema.graphqls
14  └── schema.resolvers.go
15server.go 
16tmp
17  ├── build-errors.log
18  └── main
19tools.go 
20
212 directories, 8 files

在项目文件中,一个重要的文件是schema.resolvers.go. 它包含执行在schema.graphqls文件中之前定义的突变请求类型的方法。

若要修复验证错误,请在schema.resolvers.go文件的底部删除CreateTodoTodos方法,Gqlgen将方法移动到文件的底部,因为在schema.graphqls文件中修改了类型定义。

 1[label schema.resolvers.go]
 2
 3package graph
 4
 5// This file will be automatically regenerated based on the schema, any resolver implementations
 6// will be copied through when generating and any unknown code will be moved to the end.
 7
 8import (
 9    "context"
10    "digitalocean/graph/generated"
11    "digitalocean/graph/model"
12    "fmt"
13)
14
15func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
16    panic(fmt.Errorf("not implemented"))
17}
18
19func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
20    panic(fmt.Errorf("not implemented"))
21}
22
23func (r *queryResolver) User(ctx context.Context) (*model.User, error) {
24    panic(fmt.Errorf("not implemented"))
25}
26
27// Mutation returns generated.MutationResolver implementation.
28func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
29
30// Query returns generated.QueryResolver implementation.
31func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
32
33type mutationResolver struct{ *Resolver }
34type queryResolver struct{ *Resolver }
35
36// !!! WARNING !!!
37// The code below was going to be deleted when updating resolvers. It has been copied here so you have
38// one last chance to move it out of harms way if you want. There are two reasons this happens:
39//  - When renaming or deleting a resolver the old code will be put in here. You can safely delete
40//    it when you're done.
41//  - You have helper methods in this file. Move them out to keep these resolver files clean.
42
43func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
44    panic(fmt.Errorf("not implemented"))
45}
46func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
47    panic(fmt.Errorf("not implemented"))
48}

schema.graphqls文件所定义,Gqlgen的代码生成器创建了两个突变和一个查询解析方法,这些解析器为以下目的服务:

  • `创造用户 ' : 这个突变解析器将一个新的用户记录插入到连接的Postgres数据库.
  • `上载ProfileImage': 这个突变解析器上传了从一个[多部分 HTTP 请求(https://swagger.io/docs/specification/describing-request-body/multipart-requests/)收到的媒体文件,并将文件上传到数字海洋空间内部的一桶. 文件上传后,上传文件的URL被插入到之前创建的用户的"img_uri"字段.
  • " 用户 " : 这个查询解析器查询所有现有用户的数据库,并作为查询结果返回. .

通过从突变和查询类型生成的方法,你会注意到它们会引起(https://golang.org/src/runtime/panic.go)与执行时未实施错误的恐慌。 这表明它们仍然是自动生成的 boilerplate 代码。 在本教程中,您将返回schema.resolver.go文件来实施这些生成的方法。

在此时,您将基于 schema.graphqls 文件的内容生成此应用程序的解析器. 您现在将使用 Managed Databases 服务创建一个数据库,该数据库将存储传递给突变解析器创建用户的数据。

第4步:在DigitalOcean上提供和使用管理数据库实例

在此步骤中,您将使用 DigitalOcean 控制台访问 Managed Databases 服务,并创建一个 PostgreSQL 数据库来存储来自该应用程序的数据。

虽然应用程序不会直接在数据库中存储图像,但它仍然需要一个数据库来插入每个用户的记录。

用户的记录将包括一个 Fullname ,** email** ,** dateCreated,** 和一个** img_uri** 字段的字符串数据类型.** img_uri** 字段包含指向用户通过此GraphQL API上传的图像文件的URL,并存储在DigitalOcean Spaces的桶中。

使用您的 DigitalOcean 仪表板,导航到控制台的数据库部分,创建一个新的数据库集群,然后从提供的数据库列表中选择 PostgreSQL 留下所有其他设置以其默认值,并使用底部的按钮创建此集群。

Digitalocean database cluster

数据库集群创建过程需要几分钟才能完成。

创建集群后,请遵循数据库集群页面上的 Getting Started 步骤来设置集群用于使用。

Getting Started 指南的第二步中,单击** Continue, I will do this later** text to proceed. 默认情况下,数据库集群对所有连接开放。

<$>[注] 注: 在生产准备的情况下,第二步中的** 添加可信源** 输入字段只应包含可信 IP 地址,例如运行应用程序的 DigitalOcean Droplet 的 IP 地址。

点击允许这些输入源按钮来保存并继续到下一步。

在下一个步骤中,将显示群集的连接细节,您还可以通过单击 行动 下载单元来查找群集凭证,然后选择** 连接细节** 选项。

Digitalocean database cluster credentials

在此屏幕截图中,右侧的灰色框显示了创建的演示群集的连接凭证。

digitalocean项目目录中,创建一个.env文件,并在以下格式中添加您的群集凭证,确保您用自己的凭证替换突出的位置持有人内容:

1[label .env]
2
3 DB_PASSWORD=YOUR_DB_PASSWORD
4 DB_PORT=PORT
5 DB_NAME=YOUR_DATABASE_NAME
6 DB_ADDR=HOST
7 DB_USER=USERNAME

随着连接详细信息安全地存储在.env 文件中,下一步将是获取这些凭据并将数据库集群连接到您的项目。

开始前,您需要数据库驱动程序,以便在连接 Postgres 数据库时与 Golang 的本地 SQL 软件包合作 。 [go-pg] (https://github.com/go-pg/pg)是一个戈兰格文库,用于将ORM([对象-关系映射 (https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping))查询翻译成SQL查询,用于Postgres数据库. [godotenv] (https://github.com/joho/godotenv)是一个戈兰语库,用于将环境证书从`.env'文件装入您的应用程序。 最后,go.uuid为每个用户的记录生成一个UUID(普遍独特的标识符),将插入数据库.

运行此命令来安装这些:

1go get github.com/go-pg/pg/v10 github.com/joho/godotenv github.com/satori/go.uuid

然后,导航到)连接。

首先,将代码块的内容添加到 db.go 文件中. 此函数( createSchema)在建立连接到数据库后立即在 Postgres 数据库中创建用户表。

 1[label db.go]
 2package graph
 3
 4import (
 5    "github.com/go-pg/pg/v10"
 6    "github.com/go-pg/pg/v10/orm"
 7    "digitalocean/graph/model"
 8)
 9
10func createSchema(db *pg.DB) error {
11    for _, models := range []interface{}{(*model.User)(nil)}{
12    	if err := db.Model(models).CreateTable(&orm.CreateTableOptions{
13    		IfNotExists: true,
14    	}); err != nil {
15    		panic(err)
16    	}
17    }
18
19    return nil
20}

使用)转移到)客户端或 GUI 手动创建表格,createSchema 函数则负责创建表格。

接下来,将下面的代码块的内容添加到db.go文件中,以建立连接到Postgres数据库,并在成功建立连接时执行上面的createSchema函数:

 1[label db.go]
 2
 3import (
 4      // ...
 5
 6    	 "fmt" 
 7    	 "os" 
 8    )
 9
10func Connect() *pg.DB {
11    DB_PASSWORD := os.Getenv("DB_PASSWORD")
12    DB_PORT := os.Getenv("DB_PORT")
13    DB_NAME := os.Getenv("DB_NAME")
14    DB_ADDR := os.Getenv("DB_ADDR")
15    DB_USER := os.Getenv("DB_USER")
16
17    connStr := fmt.Sprintf(
18    	"postgresql://%v:%v@%v:%v/%v?sslmode=require",
19    	DB_USER, DB_PASSWORD, DB_ADDR, DB_PORT, DB_NAME )
20
21    opt, err := pg.ParseURL(connStr); if err != nil {
22      panic(err)
23      }
24
25    db := pg.Connect(opt)
26
27    if schemaErr := createSchema(db); schemaErr != nil {
28    	panic(schemaErr)
29    }
30
31    if _, DBStatus := db.Exec("SELECT 1"); DBStatus != nil {
32    	panic("PostgreSQL is down")
33    }
34
35    return db 
36}

当执行时,上面的代码块中导出的)连接到 Postgres 数据库。

  • 首先,您在 root .env 文件中存储的数据库凭据被检索,然后创建一个变量以存储与检索的凭据格式化的字符串. 此变量在连接到数据库时将被用作连接 URI。 * 接下来,创建的连接字符串被分析,以查看已格式化的凭据是否有效。

要使用导出的连接函数,您需要将该函数添加到server.go文件中,以便在应用程序启动时执行。

要在应用程序启动后立即使用先前创建的连接函数从图形包中,并将来自.env文件的凭证加载到应用程序中,请在您偏好的代码编辑器中打开server.go文件,并添加下面的行:

<$>[注] 注: 请确保在server.go文件中替换现有的srv变量以下面突出的srv变量。

 1[label server.go]
 2 package main
 3
 4import (
 5  "log"
 6  "net/http"
 7  "os"
 8  "digitalocean/graph"
 9  "digitalocean/graph/generated"
10
11  "github.com/99designs/gqlgen/graphql/handler"
12  "github.com/99designs/gqlgen/graphql/playground"
13 "github.com/joho/godotenv"
14)
15
16const defaultPort = "8080"
17
18func main() {
19     err := godotenv.Load(); if err != nil {
20     log.Fatal("Error loading .env file")
21    } 
22
23  // ...
24
25     Database := graph.Connect()
26     srv := handler.NewDefaultServer(
27     		generated.NewExecutableSchema(
28     				generated.Config{
29     					Resolvers: &graph.Resolver{
30     						DB: Database,
31     					},
32     				}),
33     	)
34
35  // ...
36}

在此代码片段中,您通过 [Load()] 函数(https://github.com/joho/godotenv/blob/c40e9c6392b05ba58e6fea50091ce35a1ef020e7/godotenv.go# L42) 加载了存储在 .env 中的凭证。您从 db包中调用了Connect函数,并创建了Resolver对象,存储在DB` 字段中的数据库连接。 (存储的数据库连接将在本教程中稍后由解析者访问)。

目前,在 resolver.go 文件中的 boilerplate Resolver 结构不包含您在上面的代码中存储数据库连接的 DB 字段。

图表目录中,打开resolver.go文件,并修改Resolver结构,以便有一个DB字段,其类型为go-pg指针,如下所示:

 1[label resolver.go]
 2package graph
 3
 4import "github.com/go-pg/pg/v10"
 5
 6// This file will not be regenerated automatically.
 7//
 8// It serves as dependency injection for your app, add any dependencies you require here.
 9
10type Resolver struct {
11    DB *pg.DB
12}

现在,每次运行server.go输入文件时都会建立数据库连接,而go-pg包可以用作ORM来执行从解析函数对数据库的操作。

在此步骤中,您使用 DigitalOcean 上的 Managed Database 服务创建了一个 PostgreSQL 数据库,您还创建了一个具有连接函数的db.go文件,在应用程序启动时建立与 PostgreSQL 数据库的连接。

步骤5 - 实施生成解决方案

在此步骤中,您将在schema.resolvers.go文件中实施方法,该文件作为突变和查询解析器,实施的突变解析器将创建用户并上传用户的个人资料图像,而查询解析器将检索所有存储的用户细节。

实施突变解决方法

schema.graphqls文件中,产生了两个突变解析器,一个是用来插入用户的记录,另一个是处理上传的个人资料图像,但是这些突变尚未实现,因为它们是锅板代码。

打开schema.resolvers.go文件. 用突出的行修改导入和CreateUser突变,将包含用户详细信息输入的新行插入到数据库中:

 1[label schema.resolvers.go]
 2package graph
 3
 4import (
 5  "context"
 6  "fmt"
 7   "time" 
 8
 9  "digitalocean/graph/generated"
10  "digitalocean/graph/model"
11  "github.com/satori/go.uuid" 
12)
13
14func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
15     user := model.User{ 
16     	ID:          fmt.Sprintf("%v", uuid.NewV4()), 
17     	FullName:    input.FullName, 
18     	Email:       input.Email, 
19     	ImgURI:      "https://bit.ly/3mCSn2i", 
20     	DateCreated: time.Now().Format("01-02-2006"), 
21     } 
22
23     _, err := r.DB.Model(&user).Insert(); if err != nil { 
24     	return nil, fmt.Errorf("error inserting user: %v", err) 
25     } 
26
27     return &user, nil 
28}

在)。 其次,每个行中的ImgURI字段都有一个定位图像URL作为默认值。 这将是所有记录的默认值,并且在用户上传新图像时会更新。

接下来,您将测试此时已构建的应用程序. 从项目目录中,使用以下命令运行 server.go 文件:

1go run ./server.go

现在,通过您的 Web 浏览器导航到 http://localhost:8080 以访问您的 GraphQL API 内置的 GraphQL 游戏场地. 将下面的代码块中的 GraphQL 突变粘贴到游戏场地编辑器中以插入新的用户记录。

 1[label graphql]
 2
 3mutation createUser {
 4  createUser(
 5    input: {
 6      email: "[email protected]"
 7      fullName: "John Doe"
 8    }
 9  ) {
10    id
11  }
12}

右侧的输出将看起来像这样:

A create user mutation on the GraphQL Playround

您执行了CreateUser突变,以创建一个名为John Doe的测试用户,并作为突变结果返回了新插入的用户记录的id

<$>[注] 注: 复制从执行的 GraphQL 查询中返回的 id 值。

在此时,您有第二个UploadProfileImage突变解析函数要执行,但在执行此函数之前,您需要先执行查询解析器,因为每个上传都与特定用户相关联,这就是为什么您在上传图像之前检索了特定用户的ID。

应用 Query Resolver 方法

正如schema.resolvers.graphqls文件中所定义的那样,一个查询解析器被生成以获取所有创建的用户。

打开scheme.resolvers.go并用突出的行修改生成的用户查询解决方案.下面的用户方法中的新代码将查询所有用户行的 Postgres 数据库并返回结果。

 1[label schema.resolvers.go]
 2package graph
 3
 4func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
 5  var users []*model.User
 6
 7  err := r.DB.Model(&users).Select()
 8    if err != nil {
 9    	return nil, err
10    } 
11
12  return users, nil 
13}

在上面的用户解析函数中,通过在用户模型上使用 go-pg 的选择方法来获取用户表中的所有记录,而不会将WHERELIMIT条款传入查询中。

注意:对于一个较大的应用程序,从查询中返回许多记录,重要的是考虑页化返回的数据以提高性能。

要从您的浏览器中测试此查询解决方案,请导航到 http://localhost:8080 访问 GraphQL 播放场。 将下面的 GraphQL 查询粘贴到播放场编辑器中,以获取所有创建的用户记录。

1[label graphql]
2
3query fetchUsers {
4  users {
5      fullName
6      id
7      img_uri
8  }
9}

右侧的输出将看起来像这样:

Query result GraphQL playground

在返回的结果中,您可以看到一个具有数组值的用户对象被返回。 目前,仅在用户数组中返回了先前创建的用户,因为这是表中唯一的记录。 如果您用新的细节执行CreateUser突变,则在用户数组中会返回更多的用户。

在这一点上,您现在已经实现了) SDK 将所接收的图像上传到 DigitalOcean Spaces 的桶中。

步骤6 — 将图像上传到数字海洋空间

在此步骤中,您将使用第二个UploadProfileImage突变中的强大的API来上传图像到您的空间。

要开始,请导航到您的 DigitalOcean 主机的空间部分,在那里您将创建一个新的桶来存储从您的后端应用程序上传的文件。

点击创建新空间按钮,将设置留在默认值,并为新空间指定一个独特的名称:

Digitalocean spaces

创建新空间后,导航到设置卡并复制空间的终端点、名称和区域,并将其添加到 GraphQL 项目中的 .env 文件中,以此格式:

1[label .env]
2SPACE_ENDPOINT=BUCKET_ENDPOINT
3DO_SPACE_REGION=DO_SPACE_REGION
4DO_SPACE_NAME=DO_SPACE_NAME

例如,下面的屏幕截图显示了):

Victory-space endpoint, name, and region

作为前提的一部分,您为您的空间创建了一个空间访问密钥和秘密密钥,将您的访问密钥和秘密密密钥粘贴到 GraphQL 应用程序中的 .env 文件中,以以下格式:

1[label .env]
2ACCESS_KEY=YOUR_SPACE_ACCESS_KEY
3SECRET_KEY=YOUR_SPACE_SECRET_KEY

在此时,您需要使用CTRL + C 键组合来停止 GraphQL 服务器,然后执行下面的命令,以重新启动 GraphQL 应用程序,并将新的凭证加载到应用程序中。

1go run ./server.go

现在你的空间凭据已被加载到应用程序中,你将创建上传逻辑在) SDK 以连接到您的 DigitalOcean Space。

Spaces中,您可以通过使用兼容的 AWS SDK 来编程操作。 AWS Go SDK 是一个开发套件,提供了一组图书馆,可供 Go 开发人员使用。 SDK 提供的图书馆可由 Go 编写的应用程序在执行与 AWS 资源的操作时使用,例如将文件传输到 S3 库。

The DigitalOcean Spaces documentation提供了您可以使用 AWS SDK 在 Spaces API 上执行的操作列表,我们将使用 aws-sdk-go SDK 连接到您的 DigitalOcean Space。

运行go get命令,将aws-sdk-go SDK 安装到应用程序中:

1go get github.com/aws/aws-sdk-go

在接下来的几块代码中,您将逐步在UploadProfileImage突变解析器中组合上传逻辑。

首先,打开schema.resolvers.go文件. 添加突出的行以配置 AWS SDK 与存储的凭据,并与您的 DigitalOcean Space 建立连接:

注意:下面的代码块中的代码不完整,因为你正在逐步将上传逻辑整合在一起。

 1[label schema.resolvers.go]
 2package graph
 3
 4import (
 5   ...
 6
 7   "os"
 8
 9   "github.com/aws/aws-sdk-go/aws"
10   "github.com/aws/aws-sdk-go/aws/credentials"
11   "github.com/aws/aws-sdk-go/aws/session"
12   "github.com/aws/aws-sdk-go/service/s3"
13)
14
15func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
16
17    SpaceRegion := os.Getenv("DO_SPACE_REGION")
18    accessKey := os.Getenv("ACCESS_KEY")
19    secretKey := os.Getenv("SECRET_KEY")
20
21    s3Config := &aws.Config{
22    	Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
23    	Endpoint:    aws.String(os.Getenv("SPACE_ENDPOINT")),
24    	Region:      aws.String(SpaceRegion),
25    }
26
27    newSession := session.New(s3Config)
28    s3Client := s3.New(newSession)
29
30}

现在 SDK 已配置,下一步是将发送的文件上传到 多部分 HTTP 请求

处理发送的文件的一种方法是读取多部分请求的内容,暂时将内容保存到内存中的新文件中,使用aws-SDK-go库上传临时文件,然后在上传后删除它。

要做到这一点,在schema.resolvers.go文件中的UploadProfileImage突变解析器中的现有代码中添加了突出的行:

 1[label schema.resolvers.go]
 2
 3package graph
 4
 5import (
 6   ...
 7
 8   "io/ioutil"
 9   "bytes"
10
11)
12
13func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
14...
15
16SpaceName := os.Getenv("DO_SPACE_NAME")
17
18...
19
20  userFileName := fmt.Sprintf("%v-%v", input.UserID, input.File.Filename)
21  stream, readErr := ioutil.ReadAll(input.File.File)
22    if readErr != nil {
23    	fmt.Printf("error from file %v", readErr)
24    }
25
26    fileErr := ioutil.WriteFile(userFileName, stream, 0644); if fileErr != nil {
27    	fmt.Printf("file err %v", fileErr)
28    }
29
30    file, openErr := os.Open(userFileName); if openErr != nil {
31    	fmt.Printf("Error opening file: %v", openErr)
32    }
33
34    defer file.Close()
35
36    buffer := make([]byte, input.File.Size)
37
38_, _ = file.Read(buffer)
39
40    fileBytes := bytes.NewReader(buffer)
41
42    object := s3.PutObjectInput{
43    	Bucket: aws.String(SpaceName),
44    	Key:    aws.String(userFileName),
45    	Body:   fileBytes,
46    	ACL:    aws.String("public-read"),
47    }
48
49    if _, uploadErr := s3Client.PutObject(&object); uploadErr != nil {
50    	return false, fmt.Errorf("error uploading file: %v", uploadErr)
51    }
52
53    _ = os.Remove(userFileName)
54
55return true, nil
56}

使用 ReadAll方法从上面的代码块中的 io包中,您首先读取了添加到发送到GraphQL API的多部分请求的文件内容,然后创建了一个临时文件,将此内容投放到其中。

接下来,使用 PutObjectInput结构,您创建了要上传的文件结构,通过指定 BucketKeyACLBody 字段为临时存储的文件的内容。

<$>[注] 注: 访问控制列表 (ACL)字段在 PutObjectInput struct 有一个 public-read 值,使所有上传的文件可通过互联网查看。 如果您的应用程序要求上传的数据保持私密,您可以删除此字段。

创建PutObjectInput结构后,使用PutObject方法进行PUT操作,将PutObjectInput结构的值发送到桶中。

要测试上传突变解析器,你可以使用Sammy the Shark的图像,DigitalOcean的马斯科特。

1wget https://html.sammy-codes.com/images/small-profile.jpeg

接下来,执行下面的 cURL 命令,将 HTTP 请求发送到 GraphQL API 以上传 Sammy 的图像,该图像已被添加到请求表单体中。

注意: 如果您使用的是 Windows 操作系统,建议您使用 Git Bash壳执行 cURL命令,因为背后逃避。

1curl localhost:8080/query  -F operations='{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image userId : $userId}) }", "variables": { "image": null, "userId" : "12345" } }' -F map='{ "0": ["variables.image"] }'  -F 0=@small-profile.jpeg

注意:我们在上面的请求中使用随机的 userId 值,因为更新用户记录的过程尚未实现。

输出将类似于此,表明文件上传成功:

1[secondary_label Output]
2{"data": { "uploadProfileImage": true }}

在 DigitalOcean 控制台的空间部分中,您将找到从您的终端上传的图像:

A bucket within Digitalocean showing a list of uploaded files

在此时,应用程序内部的文件上传正在工作;然而,这些文件与执行上传的用户相关联,每个文件上传的目标是将文件上传到存储桶,然后通过更新用户的img_uri字段来链接到用户。

图表目录中打开resolver.go文件并添加下面的代码块,它包含两种方法:一种是通过指定字段从数据库中获取用户,另一种是更新用户的记录。

 1[label resolver.go]
 2
 3import (
 4...
 5
 6  "digitalocean/graph/model"
 7  "fmt"
 8)
 9
10...
11
12func (r *mutationResolver) GetUserByField(field, value string) (*model.User, error) {
13    user := model.User{}
14
15    err := r.DB.Model(&user).Where(fmt.Sprintf("%v = ?", field), value).First()
16
17    return &user, err
18}
19
20func (r *mutationResolver) UpdateUser(user *model.User) (*model.User, error) {
21    _, err := r.DB.Model(user).Where("id = ?", user.ID).Update()
22    return user, err
23}

上面的第一个GetUserByField函数接受一个字段和一个参数,两者都是字符串类型。

代码块中的第二个) 方法,在UPDATE 语句中添加一个包含条件的WHERE条款,仅更新具有相同ID传入函数的行。

现在你可以使用UploadProfileImage突变中的两种方法,将下面的突出代码块的内容添加到UploadProfileImage突变中,在schema.resolvers.go文件中。

<$>[注] 注: 将突出的代码放置在现有返回声明上方的行上,在UploadProfileImage突变中。

 1[label schema.resolvers.go]
 2
 3package graph
 4
 5func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
 6  _ = os.Remove(userFileName)
 7
 8    user, userErr := r.GetUserByField("ID", *input.UserID)
 9
10    if userErr != nil {
11    	return false, fmt.Errorf("error getting user: %v", userErr)
12    }
13
14   fileUrl := fmt.Sprintf("https://%v.%v.digitaloceanspaces.com/%v", SpaceName, SpaceRegion, userFileName)
15
16    user.ImgURI = fileUrl
17
18    if _, err := r.UpdateUser(user); err != nil {
19    	return false, fmt.Errorf("err updating user: %v", err)
20    }
21
22  return true, nil
23}

从新代码添加到schema.resolvers.go文件中,将一个ID字符串和用户ID传递到GetUserByField辅助函数,以获取执行突变的用户的记录。

然后创建一个新的变量,并给出一个字符串的值格式化,以使最近上传的文件的链接在 https://BUCKET_NAME.SPACE_REGION.digitaloceanspaces.com/USER_ID-FILE_NAME 的格式中。

将下面的 curl 命令粘贴到您的终端中,并在命令中替换突出的 USER_ID 位存者,以通过 GraphQL 游戏场所创建的用户userId

1curl localhost:8080/query  -F operations='{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image userId : $userId}) }", "variables": { "image": null, "userId" : "USER_ID" } }' -F map='{ "0": ["variables.image"] }'  -F 0=@small-profile.jpeg

结果将看起来像这样:

1[secondary_label Output]
2{"data": { "uploadProfileImage": true }}

为了进一步确认用户的img_uri已更新,您可以使用浏览器中的fetchUsers查询来获取用户的详细信息。如果更新成功,您将看到img_uri字段中的https://bit.ly/3mCSn2i默认位置地址已更新到上传图像的值。

右侧的输出将看起来像这样:

A query mutation to retrieve an updated user record using the GraphQL Playground

在返回的结果中,从查询返回的第一个用户对象中的img_uri具有相当于在 DigitalOcean Spaces 中上传到桶的文件的值。

要通过 ACL 选项测试上传文件的权限,您可以在浏览器中打开img_uri链接. 由于上传图像上的默认元数据,它将自动下载到您的计算机作为图像文件。

Downloaded view of the uploaded file

img_uri链接上的图像将是从命令行上传的相同图像,表明resolver.go文件中的方法被正确执行,并且UploadProfileImage突变中的整个文件上传逻辑按预期工作。

在此步骤中,您使用UploadProfileImage突变解决方案的 AWS SDK for Go 将图像上传到 DigitalOcean Space。

结论

在本教程中,您使用Golang的AWS SDK从GraphQL应用程序中的突变解析器上传到DigitalOcean Space上的创建桶。

作为下一步,您可以部署本教程内构建的应用程序。 Go Dev 指南提供了一个初学者友好的指南,如何部署一个 Golang 应用程序到 DigitalOcean 的 应用平台,这是构建,部署和管理您的应用程序从各种编程语言的完全管理的解决方案。

Published At
Categories with 技术
comments powered by Disqus