Active Storage+GraphQL:直接上传

Active Storage之前的生活

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

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

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

  • Buggy客户端实现(特别是对于React Native)
  • 由非严格Upload类型引起的副作用(不关心实际的对象类型
  • Apollo依赖(是的,我们在新版本中向Apollo说“再见!”;但这是另一个故事)。

没有惊喜(也没有警报,),我们决定摆脱这种黑客并使用一个好的旧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 突变...

不幸的是,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代码)。我检查的只是它编译。

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

  • 获取文件元数据(文件名,大小,内容类型和校验和)
  • 通过API - createDirectUpload突变请求直接上传凭证和blob ID
  • 使用凭据上传文件(不涉及GraphQL,HTTP PUT请求)。

对于第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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容