Active Storage+GraphQL:直接上传

2019-07-17  本文已影响0人  乐哈网

Active Storage之前的生活

首先,让我告诉你我们如何在Rails 4中处理文件上传.TwoQL规范和graphqlRuby gem 都没有指定正确烹饪文件上传的方法。

有一个开源规范,它有不同语言的实现,包括Ruby。它“描述”了Upload标量类型,做了一些Rack中间件魔术来传递上传的文件作为变量,并且有点透明地工作。

听起来像是“即插即用”。理论上。在实践中,它转变为“plug-n-play-n-fail-n-fix-n-fail-n-fix”:

没有惊喜(也没有警报,),我们决定摆脱这种黑客并使用一个好的旧REST来上传文件。

这里有Active Storage直接上传。

指导上传🎥

什么是“直接上传”顺便说一下?

该术语通常与云存储服务(例如,Amazon S3)结合使用,并且意味着以下内容:客户端使用API​​服务器上载文件,而不是使用API​​服务器生成的凭证将其直接上载到云存储

直接上传图

好消息 - Active Storage提供了一个服务器端API来处理直接上传和一个开箱即用的前端JS客户端。

另一个好消息 - 这个API是抽象的,适用于Active Storage支持的任何服务(即文件系统,S3,GCloud,Azure)。这很棒:你可以在本地使用文件系统,在生产中使用S3而不需要if-s和else-s。

不过,好消息很少没有坏消息。坏消息是Active Storage(和Rails一般)对GraphQL一无所知,并依赖自己的REST API来检索直接上传凭证。

在GraphQL中我们需要做什么?

首先,能够使用GraphQL API(通过变异)获得直接上传凭证。

其次,从框架中尽可能多地重用JavaScript代码以避免重新发明轮子会很棒。

createDirectUpload 突变...

GraphiQL中的变异预览

不幸的是,Rails没有任何服务器端直接上传实现的文档。
所有我们已经是源代码DirectUploadsController

def create
  blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
  render json: direct_upload_json(blob)
end

private

def blob_args
  params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :metadata).to_h.symbolize_keys
end

def direct_upload_json(blob)
  blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
    url: blob.service_url_for_direct_upload,
    headers: blob.service_headers_for_direct_upload
  })
end

看一下checksum参数:这是Active Storage的一个隐藏的宝石 - 一个内置的文件内容验证。

当客户端请求直接上载时,它可以指定文件的校验和(MD5哈希编码为Base64),并且服务(例如,Active Storage本身或S3)稍后将使用此校验和来验证上载的文件内容。

让我们回到GraphQL。

GraphQL突变与Rails控制器非常相似,因此将上述代码转换为突变非常简单:

class CreateDirectUpload < GraphQL::Schema::Mutation
  class CreateDirectUploadInput < GraphQL::Schema::InputObject
    description "File information required to prepare a direct upload"

    argument :filename, String, "Original file name", required: true
    argument :byte_size, Int, "File size (bytes)", required: true
    argument :checksum, String, "MD5 file checksum as base64", required: true
    argument :content_type, String, "File content type", required: true
  end

  argument :input, CreateDirectUploadInput, required: true

  class DirectUpload < GraphQL::Schema::Object
    description "Represents direct upload credentials"

    field :url, String, "Upload URL", null: false
    field :headers, String,
          "HTTP request headers (JSON-encoded)",
          null: false
    field :blob_id, ID, "Created blob record ID", null: false
    field :signed_blob_id, ID,
          "Created blob record signed ID",
          null: false
  end

  field :direct_upload, DirectUpload, null: false

  def resolve(input:)
    blob = ActiveStorage::Blob.create_before_direct_upload!(input.to_h)

    {
      direct_upload: {
        url: blob.service_url_for_direct_upload,
        # NOTE: we pass headers as JSON since they have no schema
        headers: blob.service_headers_for_direct_upload.to_json,
        blob_id: blob.id,
        signed_blob_id: blob.signed_id
      }
    }
  end
end


# add this mutation to your Mutation type
field :create_direct_upload, mutation: CreateDirectUpload

现在,要从服务器检索直接上载有效负载,GraphQL客户端必须执行以下请求:

mutation {
  createDirectUpload(input: {
    filename: "dev.to", # file name
    contentType: "image/jpeg", # file content type
    checksum: "Z3Yzc2Q5iA5eXIgeTJn", # checksum
    byteSize: 2019 # size in bytes
  }) {
    directUpload {
      signedBlobId
    }
  }
}

......还有一些JavaScript
免责声明:下面的JS实现只是一个草图,并没有在现实中进行测试(因为在我的项目中我们不使用任何Rails的JS代码)。我检查的只是它编译。

要上载文件,客户端必须执行以下步骤:

对于第1步和第3步,我们可以重用一些随Rails一起提供的JS库中的代码(不要忘记添加"@rails/activestorage"到您的代码中package.json)。

我们来写一个getFileMetadata函数:

import { FileChecksum } from "@rails/activestorage/src/file_checksum";

function calculateChecksum(file) {
  return new Promise((resolve, reject) => {
    FileChecksum.create(file, (error, checksum) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(checksum);
    });
  });
}


export const getFileMetadata = (file) => {
  return new Promise((resolve) => {
    calculateChecksum(file).then((checksum) => {
      resolve({
        checksum,
        filename: file.name,
        content_type: file.type,
        byte_size: file.size
      });    
    });
  });
};

FileChecksumclass负责计算所需的校验和,并由Active Storage在DirectUpload课堂中使用。

现在您可以使用此函数来构建GraphQL查询负载:

// pseudo code
getFileMetadata(file).then((input) => {
  return performQuery(
    CREATE_DIRECT_UPLOAD_QUERY,
    variables: { input }
  );
});

现在是时候编写一个函数来直接将文件上传到存储服务!

import { BlobUpload } from "@rails/activestorage/src/blob_upload";

export const directUpload = (url, headers, file) => {
  const upload = new BlobUpload({ file, directUploadData: { url, headers } });
  return new Promise((resolve, reject) => {
    upload.create(error => {
      if (error) {
        reject(error);
      } else {
        resolve();
      }
    })
  });
};

我们完整的客户端代码示例如下:

getFileMetadata(file).then((input) => {
  return performQuery(
    CREATE_DIRECT_UPLOAD_QUERY,
    variables: { input }
  ).then(({ directUpload: { url, headers, signedBlobId }) => {
    return directUpload(url, JSON.parse(headers), file).then(() => {
      // do smth with signedBlobId – our file has been uploaded!
    });
  });
});

看起来我们做到了!希望能帮助您构建令人敬畏的新Rails + GraphQL项目)

有关更实际的示例,请查看我们的React Native应用程序中的此代码段:https//gist.github.com/Saionaro/7ee0e2c02749e2729dc429c9e9bfa7f3

在结论中,或者如何处理 signedBlobId

让我提供一个快速示例,说明我们如何在应用程序中使用带符号的blob ID - attachProfileAvatar突变:

class AttachProfileAvatar < GraphQL::Schema::Mutation
  description <<~DESC
   Update the current user's avatar
   (by attaching a blob via signed ID)
  DESC

  argument :blob_id, String,
            "Signed blob ID generated via `createDirectUpload` mutation",
            required: true

  field :user, Types::User, null: true

  def resolve(blob_id:)
    # Active Storage retrieves the blob data from DB
    # using a signed_id and associates the blob with the attachment (avatar)
    current_user.avatar.attach(blob_id)
    {user: current_user}
  end
end

上一篇下一篇

猜你喜欢

热点阅读