如何在带有DigitalOceanSpaces的Rails6中使用ActiveStorage

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech 基金 来接受捐赠。

介绍

当您构建允许用户上传和存储文件的 Web 应用程序时,您需要使用可扩展的文件存储解决方案。 这样,如果您的应用程序广受欢迎,您就不会面临空间不足的危险。 毕竟,这些上传的内容可以是从个人资料图片到房屋照片再到 PDF 报告的任何内容。 您还希望您的文件存储解决方案可靠,这样您就不会丢失重要的客户文件,并且速度快,这样您的访问者就不会等待文件传输。 您也希望这一切都可以负担得起。

DigitalOcean Spaces 可以满足所有这些需求。 因为它与 Amazon 的 S3 服务兼容,所以您可以使用 Rails 6 附带的新 ActiveStorage 库将其快速集成到 Ruby on Rails 应用程序中。

在本指南中,您将配置一个 Rails 应用程序,以便它使用 ActiveStorage 和 DigitalOcean Spaces。 然后,您将完成必要的配置,以使用直接上传和 Spaces 的内置 CDN(内容交付网络)快速上传和下载。

完成后,您就可以将带有 DigitalOcean 空间的文件存储集成到您自己的 Rails 应用程序中了。

先决条件

在开始本指南之前,您需要以下内容:

  • 一个 DigitalOcean 帐户
  • Ruby on Rails 的开发环境。 关注 如何在 macOS 上使用 Rbenv 安装 Ruby on Rails 或 如何在 Ubuntu 上使用 Rbenv 安装 Ruby on Rails,具体取决于您使用的操作系统。
  • 一些初步的 Ruby on Rails 知识。 如何构建 Ruby on Rails 应用程序 之类的教程可以对此有所帮助,或者您可以按照 官方入门指南 进行操作。
  • Git 安装在你的开发机器上。 您可以按照教程 贡献开源:Git 入门 在您的计算机上安装和设置 Git。
  • 安装了Node.js,你可以按照【X49X】如何安装Node.js并创建本地开发环境【X118X】来完成。 本教程使用 Node.js 版本 14。
  • 已安装 Yarn 包管理器,您可以使用 npm install -g yarn 进行安装。
  • 已安装 SQLite,您可以按照 如何构建 Ruby on Rails 应用程序 的第 1 步来完成。
  • Imagemagick 已安装,您可以按照教程 How To Resize Images with ImageMagick 进行安装。
  • (可选)如果您想试用 Spaces CDN,您需要一个域名以及更改该域的 DNS 记录的能力。 您可以按照 如何添加域 教程使用 DigitalOcean 管理您的域。
  • (可选)如果您想使用直接上传,您需要配置 s3cmd 以使用 DigitalOcean Spaces。 按照 使用 DigitalOcean Spaces 设置 s3cmd 2.x 进行设置。

第 1 步 — 运行示例应用程序

您无需从头开始构建完整的 Rails 应用程序,而是克隆一个使用 ActiveStorage 的现有 Rails 6 应用程序,并将其修改为使用 DigitalOcean Spaces 作为其图像存储后端。 您将使用的应用程序是 Space Puppies,这是一个图片库,可以让人们上传和查看他们最喜欢的小狗的照片。 该应用程序如下图所示:

打开终端并使用以下命令从 GitHub 克隆应用程序:

git clone https://github.com/do-community/space-puppies

您将看到类似于此的输出:

OutputCloning into 'space-puppies'...
remote: Enumerating objects: 122, done.
remote: Counting objects: 100% (122/122), done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 122 (delta 3), reused 122 (delta 3), pack-reused 0
Receiving objects: 100% (122/122), 163.17 KiB | 1018.00 KiB/s, done.
Resolving deltas: 100% (3/3), done.

接下来,检查您的 Ruby 版本。 Space Puppies 使用 Ruby 2.7.1,所以运行 rbenv versions 来检查你安装了哪个版本:

rbenv versions

如果您遵循了先决条件教程,那么该列表中将只有 Ruby 2.5.1,并且您的输出将如下所示:

Output*  system
   2.5.1

如果该列表中没有 Ruby 2.7.1,请使用 ruby-build 安装它:

rbenv install 2.7.1

根据您机器的速度和操作系统,这可能需要一段时间。 您将看到如下所示的输出:

OutputDownloading ruby-2.7.1.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.1.tar.bz2
Installing ruby-2.7.1...
Installed ruby-2.7.1 to /root/.rbenv/versions/2.7.1

切换到 space-puppies 目录:

cd space-puppies

rbenv 会在你进入目录时自动改变你的 Ruby 版本。 验证版本:

ruby --version

您将看到类似于以下内容的输出:

Outputruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]

接下来,您将安装应用程序运行所需的 Ruby gem 和 JavaScript 包。 然后,您将进行 Space Puppies 应用程序运行所需的数据库迁移。

使用 bundle 命令安装所有必要的 gem:

bundle install

然后,要告诉 rbenv Bundler 安装的任何新二进制文件,请使用 rehash 命令:

rbenv rehash

接下来,告诉 yarn 安装必要的 JavaScript 依赖项:

yarn install

现在使用 Rails 的内置迁移工具创建数据库模式:

rails db:migrate

安装所有库并创建数据库后,使用以下命令启动内置 Web 服务器:

rails s

注意: 默认情况下,rails s 只绑定本地环回地址,意味着您只能从运行该命令的同一台计算机访问服务器。 如果你在 Droplet 上运行并且你想从本地机器上运行的浏览器访问你的服务器,你需要告诉 Rails 服务器通过绑定到 0.0.0.0 来响应远程请求。 您可以使用以下命令执行此操作:

rails s -b 0.0.0.0

您的服务器启动,您将收到如下输出:

Output=> Booting Puma
=> Rails 6.0.3.2 application starting in development
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

现在您可以在 Web 浏览器中访问您的应用程序。 如果您在本地计算机上运行应用程序,请导航到 http://localhost:3000。 如果您在 Droplet 或其他远程服务器上运行,请导航到 http://your_server_ip:3000

您将看到应用程序的界面,只是这次没有任何小狗。 尝试通过单击 New Puppy 按钮添加几个图像。

如果您需要小狗照片用于测试,Unsplash 有一个广泛的列表 可用于测试。 如果您计划在项目中使用这些图像,请查看 Unsplash 许可证

在继续之前,让我们浏览应用程序的每一层,看看 ActiveStorage 如何与每个部分一起工作,以便您可以对 DigitalOcean Spaces 进行必要的更改。 如需更详细地了解 ActiveStorage,请阅读 Rails 官方文档中的 Active Storage Overview 页面。

首先,查看模型,它表示您存储在数据库中的应用程序中的一个对象。 您将在 app/models/puppy.rb 中找到 Puppy 型号。 在文本编辑器中打开此文件,您将看到以下代码:

应用程序/模型/puppy.rb

class Puppy < ApplicationRecord

  has_one_attached :photo

end

您会在模型中找到 has_one_attached 宏,这表明每个 Puppy 模型实例都附加了一张照片。 这些照片将通过 ActiveStorage::Attached::One 代理存储为 ActiveStorage::Blob 实例。

关闭此文件。

堆栈的下一层是控制器。 在 Rails 应用程序中,控制器负责控制对数据库模型的访问并响应用户的请求。 Puppy 型号对应的控制器是 PuppiesController,您可以在 app/controllers/puppies_controller.rb 中找到它。 在编辑器中打开此文件,您将看到以下代码:

应用程序/控制器/puppies_controller.rb

class PuppiesController < ApplicationController

  def index
    @puppies = Puppy.with_attached_photo
  end

  # ... snipped other actions ...

end

文件中的所有内容都是标准 Rails 代码,除了 with_attached_photo 调用。 当您获取 Puppy 模型列表时,此调用会导致 ActiveRecord 加载所有关联的 ActiveStorage::Blob 关联。 这是 ActiveStorage 提供的 范围 ,可帮助您避免昂贵的 N+1 数据库查询

最后,让我们看一下视图,它们生成您的应用程序将发送到用户浏览器的 HTML。 此应用程序中有一些视图,但您需要关注负责显示上传的小狗图像的视图。 您将在 app/views/puppies/_puppy.html.erb 找到此文件。 在编辑器中打开它,你会看到如下代码:

应用程序/视图/小狗/_puppy.html.erb

<div class="puppy">
  <%= image_tag puppy.photo.variant(resize_to_fill: [250, 250]) %>
</div>

ActiveStorage 旨在与 Rails 一起使用,因此您可以使用内置的 image_tag 帮助器生成指向附加照片的 URL,无论它碰巧存储在哪里。 在这种情况下,应用程序对图像使用 变体支持 。 当用户第一次请求这个变体时,ActiveStorage 将通过 image_processing gem 自动使用 ImageMagick,生成符合我们要求的修改后的图像。 在这种情况下,它将创建一张填充 250x250 像素框的小狗照片。 变体将存储在与原始照片相同的位置,这意味着您只需生成每个变体一次。 Rails 将在后续请求中提供生成的版本。

注意: 生成图像变体可能会很慢,并且您可能不希望您的用户等待。 如果您知道您将需要一个特定的变体,您可以使用 .processed 方法急切地生成它:

puppy.photo.variant(resize_to_fill: [250, 250]).processed

当您部署到生产环境时,最好在后台作业中进行此类处理。 探索 Active Job 并创建一个任务来调用 processed 以提前生成图像。


现在您的应用程序在本地运行,并且您知道所有代码片段是如何组合在一起的。 接下来,是时候设置一个新的 DigitalOcean Space 了,这样您就可以将上传的内容移至云端。

第 2 步 — 设置您的 DigitalOcean 空间

目前,您的 Space Puppies 应用程序将图像存储在本地,这对于开发或测试来说很好,但您几乎肯定不想在生产中使用这种模式。 为了通过添加更多应用程序服务器实例来水平扩展应用程序,您需要在每台服务器上复制每个图像。

在此步骤中,您将创建一个 DigitalOcean Space 以用于您的应用程序的图像。

登录您的DigitalOcean管理控制台,点击右上角的Create,然后选择Spaces

选择任何数据中心并暂时禁用 CDN; 你稍后会回到这个。 确保文件列表设置为 限制文件列表

为您的空间选择一个名称。 请记住,这在所有 Spaces 用户中必须是唯一的,因此请选择一个唯一的名称,例如 yourname-space-puppies。 点击创建空间

警告: 小心访问您代表客户存储的文件。 由于文件存储配置错误, 有许多数据泄露和黑客攻击的例子。 默认情况下,只有在生成经过身份验证的 URL 时才能访问 ActiveStorage 文件,但在处理客户数据时需要保持警惕。


然后,您将看到全新的 Space。

单击 设置 选项卡并记下您的 Space 的 端点 。 当您配置 Rails 应用程序时,您将需要它。

接下来,您将配置 Rails 应用程序以在此空间中存储 ActiveStorage 文件。 要安全地执行此操作,您需要创建一个新的空间访问密钥和秘密。

点击左侧导航中的API,然后点击右下角的Generate New Key。 给你的新密钥起一个描述性的名字,比如“Development Machine”。 您的秘密只会出现一次,因此请务必暂时将其复制到安全的地方。

在您的 Rails 应用程序中,您需要一种安全的方式来存储该访问令牌,因此您将使用 Rails 的安全凭证管理功能。 要编辑您的凭据,请在终端中执行以下命令:

EDITOR="nano -w" rails credentials:edit

这会生成一个主密钥并启动 nano 编辑器,以便您可以编辑值。

nano 中,使用您的 API 密钥和来自 DigitalOcean 的密钥将以下内容添加到您的 credentials.yml 文件中:

配置/凭证.yml

digitalocean:
  access_key: YOUR_API_ACCESS_KEY
  secret: YOUR_API_ACCESS_SCRET

保存并关闭文件(Ctrl+X,然后 Y,然后 Enter),Rails 将在 [X158X 中安全地提交到源代码管理的加密版本存储]。

您将看到如下输出:

Output
Adding config/master.key to store the encryption key: RANDOM_HASH_HERE

Save this in a password manager your team can access.

If you lose the key, no one, including you, can access anything encrypted with it.

      create  config/master.key

File encrypted and saved.

现在您已经配置了您的凭据,您已准备好将您的应用程序指向您的新 Spaces 存储桶。

在编辑器中打开文件 config/storage.yml 并将以下定义添加到该文件的底部:

配置/存储.yml

digitalocean:
  service: S3
  endpoint: https://your-spaces-endpoint-here
  access_key_id: <%= Rails.application.credentials.dig(:digitalocean, :access_key) %>
  secret_access_key: <%= Rails.application.credentials.dig(:digitalocean, :secret) %>
  bucket: your-space-name-here
  region: unused

请注意,该服务显示 S3 而不是 Spaces。 Spaces 有一个与 S3 兼容的 API,Rails 原生支持 S3。 您的端点是 https://,后面是您之前复制的 Space 的 endpoint,而 bucket 名称是您在创建时输入的 Space 的名称。 当您查看您的空间时,存储桶名称也会作为标题显示在您的控制面板中。

此配置文件将未加密存储,因此您无需输入您的访问密钥和秘密,而是引用您刚刚在 credentials.yml.enc 中安全输入的那些。

:DigitalOcean 使用端点指定区域。 但是,您需要提供区域,否则 ActiveStorage 会报错。 由于 DigitalOcean 会忽略它,因此您可以将其设置为您想要的任何值。 示例代码中的值 unused 清楚地表明您没有使用它。


保存配置文件。

现在,您需要告诉 Rails 使用 Spaces 作为文件存储后端,而不是本地文件系统。 在编辑器中打开 config/environments/development.rb 并将 config.active_storage.service 条目从 :local: 更改为 :digitalocean

配置/环境/development.rb

  # ...

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :digitalocean

  # ... 

保存文件并退出编辑器。 现在再次启动您的服务器:

rails s -b 0.0.0.0

再次在浏览器中访问http://localhost:3000http://your server ip:3000

上传一些图像,应用程序会将它们存储在您的 DigitalOcean Space 中。 您可以通过访问 DigitalOcean 控制台 中的空间来查看此信息。 您将看到列出的上传文件和变体:

ActiveStorage 默认使用随机文件名,这有助于保护上传的客户数据。 元数据(包括原始文件名)存储在您的数据库中。

注意: 如果您获得的是 Aws::S3::Errors::SignatureDoesNotMatch,这可能意味着您的凭据不正确。 再次运行 rails credentials:edit 并仔细检查它们。


Rails 将文件的名称和一些元数据存储为 ActiveStorage::Blob 记录。 通过调用以附件命名的访问器方法,您可以访问任何记录的 ActiveStorage::Blob。 在这种情况下,附件称为 photo

试试看。 在终端中启动 Rails 控制台:

rails c

从您上传的最后一张小狗照片中获取 blob:

> Puppy.last.photo.blob
#=> => #<ActiveStorage::Blob ...>

您现在有一个 Rails 应用程序将上传内容存储在可扩展、可靠且价格合理的对象存储中。

在接下来的两个步骤中,您将探索可以对应用程序进行的两个可选添加,这将有助于为您的用户提高此解决方案的性能和速度。

第 3 步 — 配置 Spaces CDN(可选)

注意: 对于这一步,您需要一个域名服务器指向 DigitalOcean。 您可以按照 如何添加域 指南来执行此操作。


使用 内容交付网络 (CDN) 将允许您通过将文件副本定位到更靠近用户的位置,为用户提供更快的文件下载。

您可以使用 Uptrends CDN 性能检查 之类的工具来研究 CDN 性能。 如果您为您在上一步中上传的一张照片添加 URL,如果您碰巧在附近,您会发现速度很快,但随着您远离地理位置,速度会变慢。 您可以使用浏览器中的开发人员工具获取 URL,或者通过启动 Rails 控制台 (rails c) 并在附件上调用 service_url 来获取 URL。

> Puppy.last.photo.service_url

这是一个上升趋势报告示例,其中包含位于旧金山数据中心的文件。 请注意,时间会根据与旧金山的距离而减少。 圣地亚哥的时间很短,而巴黎的时间要长得多:

您可以通过启用 Spaces 的内置 CDN 来提高速度。 转到 DigitalOcean 控制面板中的 Spaces,然后单击您在步骤 2 中创建的空间的名称。 接下来,选择设置选项卡并单击CDN(内容交付网络)旁边的编辑,然后单击启用CDN

现在您需要为您的 CDN 选择一个域并为该域创建一个 SSL 证书。 您可以使用 Let's Encrypt 自动执行此操作。 单击使用自定义子域下拉菜单,然后单击添加新的子域证书

找到您要使用的域,然后选择创建子域的选项。 cdn.yourdomain.com 之类的东西是标准命名约定。 然后,您可以为证书命名并单击“生成证书并使用子域”按钮。

按下 CDN (Content Delivery Network) 下的 Save 按钮。

您的 CDN 现在已启用,但您需要告诉 Rails 应用程序使用它。 在这个版本的 Rails 中,它没有内置到 ActiveStorage 中,因此您将覆盖一些内置的 Rails 框架方法以使其工作。

创建一个名为 config/initializers/active_storage_cdn.rb 的新 Rails 初始化程序,并添加以下代码来重写 URL:

配置/初始化程序/active_storage_cdn.rb

Rails.application.config.after_initialize do
  require "active_storage/service/s3_service"

  module SimpleCDNUrlReplacement
    CDN_HOST = "cdn.yourdomain.com"
  
    def url(...)
      url = super
      original_host = "#{bucket.name}.#{client.client.config.endpoint.host}"      
      url.gsub(original_host, CDN_HOST)
    end
  end

  ActiveStorage::Service::S3Service.prepend(SimpleCDNUrlReplacement)
end

每次您的应用程序从 ActiveStorage::Service::S3Service 提供程序请求 URL 时,此初始化程序都会运行。 然后它将原始的非 CDN 主机替换为您的 CDN 主机,定义为 CDN_HOST 常量。

您现在可以重新启动服务器,您会注意到您的每张照片都来自 CDN。 您无需重新上传它们,因为 DigitalOcean 将负责将内容从您设置空间的数据中心转发到边缘节点。

您可能想将现在在 Uptrends 的 性能检查站点 上访问您的一张照片的速度与 CDN 之前的速度进行比较。 下面是在旧金山 Space 上使用 CDN 的示例。 您可以看到显着的全局速度提升。

接下来,您将配置应用程序以直接从浏览器接收文件。

第 4 步 — 设置直接上传(可选)

您可能要考虑的最后一个 ActiveStorage 功能称为 Direct Upload。 现在,当您的用户上传文件时,数据会发送到您的服务器,由 Rails 处理,然后转发到您的 Space。 如果您同时有许多用户,或者如果您的用户正在上传大文件,这可能会导致问题,因为每个文件(在大多数情况下)在整个上传期间都将使用单个应用服务器线程。

相比之下,直接上传将直接进入您的 DigitalOcean Space,中间没有 Rails 服务器跳跃。 为此,您将启用 Rails 附带的一些内置 JavaScript 并配置跨域资源共享([CORS]((https://developer.mozilla.org/en-US/docs/Web /HTTP/CORS) 在您的 Space 上,以便您可以安全地直接向 Space 发送请求,尽管它们来自不同的地方。

首先,您将为您的空间配置 CORS。 您将使用 s3cmd 来执行此操作,如果您尚未将其配置为使用 Spaces,则可以按照 Setting Up s3cmd 2.x with DigitalOcean Spaces 进行操作。

创建一个名为 cors.xml 的新文件并将以下代码添加到文件中,将 your_domain 替换为您用于开发的域。 如果您在本地机器上开发,您将使用 http://localhost:3000。 如果您在 Droplet 上开发,这将是您的 Droplet IP 地址:

cors.xml

<CORSConfiguration>
 <CORSRule>
   <AllowedOrigin>your_domain</AllowedOrigin>
   <AllowedMethod>PUT</AllowedMethod>
   <AllowedHeader>*</AllowedHeader>
   <ExposeHeader>Origin</ExposeHeader>
   <ExposeHeader>Content-Type</ExposeHeader>
   <ExposeHeader>Content-MD5</ExposeHeader>
   <ExposeHeader>Content-Disposition</ExposeHeader>
   <MaxAgeSeconds>3600</MaxAgeSeconds>
 </CORSRule>
</CORSConfiguration>

然后,您可以使用 s3cmd 将其设置为您的空间的 CORS 配置:

s3cmd setcors cors.xml s3://your-space-name-here

此命令成功运行时没有输出,但您可以通过查看 DigitalOcean 控制面板中的空间来检查它是否有效。 选择 Spaces,然后选择您的 Space 的名称,然后选择 Settings 选项卡。 您将在 CORS Configurations 标题下看到您的配置:

注意: 目前您需要使用 s3cmd 而不是控制面板来为“localhost”域配置 CORS,因为控制面板将这些域视为无效域。 如果您使用的是非 localhost 域(如 Droplet IP),则可以在此处进行操作。


现在您需要告诉 Rails 使用直接上传,您可以通过将 direct_upload 选项传递给 file_field 助手来实现。 在编辑器中打开 app/views/puppies/new.html.erb 并修改 file_field 助手:

应用程序/视图/小狗/new.html.erb

<h2>New Puppy</h2>

<%= form_with(model: @puppy) do |f| %>

  <div class="form-item">
    <%= f.label :photo %>
    <%= f.file_field :photo, accept: "image/*", direct_upload: true %>
  </div>

  <div class="form-item">
    <%= f.submit "Create puppy", class: "btn", data: { disable_with: "Creating..." } %>
  </div>

<% end %>

保存文件并再次启动服务器:

rails s -b 0.0.0.0

当您上传新照片时,您的照片会直接上传到 DigitalOcean Spaces。 您可以通过查看单击 创建小狗 按钮时发出的 PUT 请求来验证这一点。 您可以通过查看浏览器的 Web 控制台或阅读 Rails 服务器日志来找到请求。 您会注意到图像上传速度明显更快,尤其是对于较大的图像。

结论

在本文中,您使用 ActiveStorage 修改了一个基本的 Rails 应用程序,以在 DigitalOcean Spaces 上安全、快速且可扩展地存储文件。 无论您的用户位于何处,您都为快速下载配置了 CDN,并且您实现了直接上传,这样您的应用服务器就不会被淹没。

您现在可以获取此代码和配置并对其进行调整以适合您自己的 Rails 应用程序。