建议#
创建上传器时,让它成为 AttachmentUploader 的子类
将你的上传器添加到本文档的表格中
不要添加新的对象存储桶
实现直接上传支持
如果你需要处理上传文件,请决定在哪里处理
背景信息#
CarrierWave 上传器
极狐GitLab 对 CarrierWave 的修改
文件应该存储在哪里?#
CarrierWave 上传器决定了文件的存储位置。当你创建新的上传器类时,你就在决定新功能的文件存储位置。
首先,问问自己是否真的需要一个新的上传器类。对于不同的挂载点或不同的模型,使用同一个上传器类是可以的。
如果你确实想要或需要自己的上传器类,那么你应该让它成为 AttachmentUploader 的子类。这样你可以继承该类的存储位置和目录结构。其目录结构为:
rubyFile.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
如果你浏览极狐GitLab 代码库,会发现很多上传器有自己独立的存储位置。对于对象存储而言,这意味着每个上传器都有独立的存储桶。我们现在不鼓励添加新的存储桶,原因如下:
使用新存储桶会增加开发时间,因为你需要在 GDK、Omnibus GitLab 和 CNG 中进行下游修改。
使用新存储桶需要 JihuLab.com 基础设施变更,这会延缓新功能的推出。
使用新存储桶会减慢私有化部署用户采用新功能的速度:用户必须等待本地 GitLab 管理员配置新的存储桶后才能使用你的新功能。
通过使用现有存储桶,你可以避免这些额外的工作和阻力。Gitlab.config.uploads 存储位置(即 AttachmentUploader 使用的存储位置)已经保证是配置好的。
实现直接上传支持#
下面我们概述如何实现直接上传支持。
使用直接上传并非总是必需的,但通常是个好主意。除非你的功能所处理的上传文件既稀少又小,否则你可能需要实现直接上传。一个稀少且小的上传示例是项目头像:这些图片很少更改,并且应用程序对其大小有严格限制。
如果你的功能所处理的上传并非既稀少又小,那么不实现直接上传支持意味着你在积累技术债务。至少,你应该确保以后可以添加直接上传支持。
要支持直接上传,你需要两样东西:
Rails 中的预授权端点
一个 Workhorse 路由规则
Workhorse 不知道你的上传文件应该存储在哪里。为了获取这些信息,它会发出预授权请求。它也不知道是否需要预授权请求或向哪里发出预授权请求。为此,你需要配置路由规则。
给还记得的人提个醒,Workhorse 曾经是一个独立项目:现在已经不需要将这两个步骤拆分成不同的合并请求了。实际上,在一个合并请求中完成两者可能更容易。
添加 Workhorse 路由规则#
路由规则定义在
workhorse/internal/upstream/routes.go
中。它们包括:
一个 HTTP 动词(通常是 "POST" 或 "PUT")
一个路径正则表达式
一个上传类型:MIME 多部分或 "完整请求体"
可选地,你还可以匹配 HTTP 头,如 Content-Type
例如:
gou.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader),
你应该在 workhorse/upload_test.go 的 TestAcceleratedUpload 中为你的路由规则添加测试。
你还应该手动验证,当你为新功能执行上传请求时,Workhorse 是否发出了预授权请求。你可以通过查看 Rails 访问日志来检查这一点。这是必要的,因为如果你的路由规则有误,并不会直接报错:只是最终会使用效率较低的默认路径。
添加预授权端点#
我们区分三种情况:Rails 控制器、Grape API 端点和 GraphQL 资源。
先说坏消息:目前不支持 GraphQL 的直接上传。原因是 Workhorse 不会解析 GraphQL 查询。但请注意,由于 Workhorse 不解析 GraphQL 查询,这导致直接上传不支持 GraphQL。
考虑通过 Grape 接受文件上传。
对于 Grape 预授权端点,可以查找实现了 /authorize 路由的现有示例。一个例子是
POST :id/uploads/authorize 端点。
这个特定示例使用了 FileUploader,这意味着上传文件存储在该上传器类的存储位置(存储桶)中。
对于 Rails 端点,你可以使用
WorkhorseAuthorization concern。
处理上传文件#
有些功能需要我们对上传文件进行处理,例如从上传的文件中提取元数据。有几种不同的方法可以实现这一需求。主要选择在于在哪里实现处理,或者说“谁才是处理器”。
处理器可以使用直接上传?能否拒绝 HTTP 请求?实现难度Sidekiq是否直接简单Workhorse是是复杂Rails否是容易
在 Rails 中处理看起来很有吸引力,但它往往会随着时间的推移导致扩展问题,因为你无法使用直接上传。之后你将被迫用 Workhorse 方式重建功能。因此,如果你功能的需求允许,在 Sidekiq 中处理能在复杂度和可扩展性之间取得良好的平衡。
CarrierWave 上传器#
极狐GitLab 使用了修改版的 CarrierWave 来管理上传文件。下面我们将描述我们如何使用 CarrierWave 以及我们对其所做的修改。
CarrierWave 的核心概念是 Uploader 类。Uploader 定义了文件的存储位置,并可选择包含验证和处理逻辑。要使用 Uploader,你必须将其与 ActiveRecord 模型上的文本列关联起来。这称为“挂载”,该列称为 mountpoint。例如:
rubyclass Project < ApplicationRecord
mount_uploader :avatar, AttachmentUploader
end
现在,如果你上传一个名为 tanuki.png 的图片,那么 CarrierWave 会在 projects.avatar 列中存储字符串 tanuki.png,而 AttachmentUploader 类则包含配置数据和目录结构。例如,如果项目 ID 是 123,实际文件可能位于
/var/opt/gitlab/gitlab-rails/uploads/-/system/project/avatar/123/tanuki.png。
目录 /var/opt/gitlab/gitlab-rails/uploads/-/system/project/avatar/123/ 是由 Uploader 根据配置(/var/opt/gitlab/gitlab-rails/uploads)、模型名(project)、模型 ID(123)和挂载点(avatar)来确定的。
Uploader 决定了上传文件的独立存储目录。模型中的 mountpoint 列包含文件名。
你永远不需要直接访问 mountpoint 列,因为 CarrierWave 在模型上定义了操作文件句柄对象的 getter 和 setter 方法。
可选的 Uploader 行为#
除了为上传文件确定存储目录外,CarrierWave Uploader 还可以通过回调实现其他几种行为。并非所有这些行为都能在极狐GitLab 中使用。特别是,你目前无法使用 CarrierWave 的 version 机制。你可以做的事情包括:
文件名验证
与直接上传不兼容:文件内容的一次性预处理,例如图片缩放
与直接上传不兼容:静态加密
CarrierWave 的预处理行为(如图片缩放或加密)需要本地访问上传的文件。这会迫使你通过 Ruby 上传处理后的文件。这与直接上传的理念相悖,因为直接上传的核心就是不在 Ruby 中进行上传。如果你在使用带有预处理行为的 Uploader 时启用了直接上传,那么这些预处理行为将被静默跳过。
CarrierWave 存储引擎#
CarrierWave 有两种存储引擎:
CarrierWave 类极狐GitLab 名称描述CarrierWave::Storage::FileObjectStorage::Store::LOCAL本地文件,通过 Ruby stdlib 访问CarrierWave::Storage::FogObjectStorage::Store::REMOTE云文件,通过 Fog gem 访问
极狐GitLab 根据配置同时使用这两种引擎。
在 CarrierWave 中选择存储引擎的典型方式是使用 Uploader.storage 类方法。在极狐GitLab 中我们不这样做;我们改写了 Uploader#storage 方法。这使我们能够按文件来改变存储引擎。
CarrierWave 文件生命周期#
一个 Uploader 关联了两个存储区域:常规存储和缓存存储。每个区域都有自己的存储引擎。如果你将一个文件赋值给挂载点的 setter 方法(project.avatar = File.open('/tmp/tanuki.png')),你必须通过 cache! 方法将文件拷贝/移动到缓存存储中作为副作用。要持久化该文件,你必须以某种方式调用 store! 方法。这要么通过 ActiveRecord 回调发生,要么通过在 Uploader 实例上调用 store! 发生。
通常你不需要与 cache! 和 store! 交互,但如果你需要调试极狐GitLab 对 CarrierWave 的修改,知道它们始终会被调用是很有用的。具体来说,了解 CarrierWave 预处理行为(process 等)是作为 before :cache 钩子实现的,而在直接上传的情况下,这些钩子会被忽略且不会运行,是很有好处的。
直接上传会跳过所有 CarrierWave 的 before :cache 钩子。
极狐GitLab 对 CarrierWave 的修改#
极狐GitLab 使用修改版的 CarrierWave 来实现许多功能。
在存储引擎之间迁移数据#
在 app/uploaders/object_storage.rb 中,有用于在本地存储和对象存储之间迁移用户数据的代码。这段代码的存在是因为很长一段时间以来,JihuLab.com 通过 NFS 将上传文件存储在本地存储上。后来作为基础设施迁移的一部分,我们不得不将上传文件迁移到对象存储。
这就是为什么极狐GitLab 中 CarrierWave 的 storage 会因上传而异,以及为什么我们有像 uploads.store 或 ci_job_artifacts.file_store 这样的数据库列。
通过 Workhorse 实现直接上传#
Workhorse 直接上传是一种机制,能让我们在不消耗大量 Ruby CPU 时间的情况下接受大文件上传。Workhorse 是用 Go 编写的,协程比 Ruby 线程的资源消耗要小得多。
直接上传的工作原理如下。
Workhorse 接受用户的上传请求
Workhorse 通过 Rails 对该请求进行预认证,并收到一个临时上传位置
Workhorse 将用户请求中的文件上传存储到该临时上传位置
Workhorse 将请求传播给 Rails
Rails 发出远程拷贝操作,将上传文件从临时位置拷贝到最终位置
Rails 删除临时上传文件
Workhorse 再次删除临时上传文件,以防 Rails 超时
通常,cache! 返回一个 CarrierWave::SanitizedFile 实例,然后 store! 使用 Fog 上传该文件。
在对象存储的情况下,通过针对极狐GitLab 的特定修改,从临时位置拷贝到最终位置是通过 Rails 欺骗 CarrierWave 来实现的。当 CarrierWave 尝试对上传文件执行 cache! 时,我们返回一个指向临时文件的 CarrierWave::Storage::Fog::File 文件句柄。在 store! 阶段,CarrierWave 然后将该文件拷贝到其预期位置。
表格#
可扩展性框架团队正在使对象存储和上传功能更易用且更健壮。如果你添加或修改上传器,更新此表会对我们有帮助。这有助于我们概览上传器的使用位置和方式。
功能存储桶详情#
功能上传技术上传器存储桶结构作业产物direct uploadworkhorse/artifacts/
CarrierWave 集成#
文件CarrierWave 使用情况已分类app/models/project.rbinclude Avatarable 是app/models/projects/topic.rbinclude Avatarable 是app/models/group.rbinclude Avatarable 是app/models/user.rbinclude Avatarable 是app/models/terraform/state_version.rbinclude FileStoreMounter 是app/models/ci/job_artifact.rbinclude FileStoreMounter 是app/models/ci/pipeline_artifact.rbinclude FileStoreMounter 是app/models/pages_deployment.rbinclude FileStoreMounter 是app/models/lfs_object.rbinclude FileStoreMounter 是app/models/dependency_proxy/blob.rbinclude FileStoreMounter 是app/models/dependency_proxy/manifest.rbinclude FileStoreMounter 是app/models/packages/composer/cache_file.rbinclude FileStoreMounter 是app/models/packages/package_file.rbinclude FileStoreMounter 是app/models/concerns/packages/debian/component_file.rbinclude FileStoreMounter 是ee/app/models/issuable_metric_image.rbinclude FileStoreMounteree/app/models/vulnerabilities/remediation.rbinclude FileStoreMounteree/app/models/vulnerabilities/export.rbinclude FileStoreMounterapp/models/packages/debian/project_distribution.rbinclude Packages::Debian::Distribution 是app/models/packages/debian/group_distribution.rbinclude Packages::Debian::Distribution 是app/models/packages/debian/project_component_file.rbinclude Packages::Debian::ComponentFile 是app/models/packages/debian/group_component_file.rbinclude Packages::Debian::ComponentFile 是app/models/merge_request_diff.rbmount_uploader :external_diff, ExternalDiffUploader 是app/models/note.rbmount_uploader :attachment, AttachmentUploader 是app/models/appearance.rbmount_uploader :logo, AttachmentUploader 是app/models/appearance.rbmount_uploader :header_logo, AttachmentUploader 是app/models/appearance.rbmount_uploader :favicon, FaviconUploader 是app/models/project.rbmount_uploader :bfg_object_map, AttachmentUploaderapp/models/import_export_upload.rbmount_uploader :import_file, ImportExportUploader 是app/models/import_export_upload.rbmount_uploader :export_file, ImportExportUploader 是app/models/ci/deleted_object.rbmount_uploader :file, DeletedObjectUploaderapp/models/design_management/action.rbmount_uploader :image_v432x230, DesignManagement::DesignV432x230Uploader 是app/models/concerns/packages/debian/distribution.rbmount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader 是app/models/bulk_imports/export_upload.rbmount_uploader :export_file, ExportUploader 是ee/app/models/user_permission_export_upload.rbmount_uploader :file, AttachmentUploaderapp/models/ci/secure_file.rbinclude FileStoreMounteree/app/models/security/vulnerability_scanning/sbom_scan.rbmount_uploader :sbom_file, Security::VulnerabilityScanning::SbomScanUploader 是ee/app/models/security/vulnerability_scanning/sbom_scan.rbmount_uploader :result_file, Security::VulnerabilityScanning::SbomScanUploader 是