上传指南:添加新上传
建议
背景信息
你应该将文件存储在哪里?
CarrierWave 上传器决定文件的存储位置。当你创建新的上传器类时,你就是在决定新功能的文件存储位置。
首先,问问自己是否需要新的上传器类。为不同的挂载点或不同的模型使用相同的上传器类是可以的。
如果你确实需要自己的上传器类,那么应该让它成为 AttachmentUploader 的子类。这样你就可以继承该类的存储位置和目录结构。目录结构如下:
File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)如果你在 GitLab 代码库中搜索,会发现许多上传器都有自己的存储位置。对于对象存储,这意味着每个上传器都有自己的桶。我们现在不鼓励添加新桶,原因如下:
- 使用新桶会增加开发时间,因为你需要在 GDK、Omnibus GitLab 和 CNG 中进行下游更改
- 使用新桶需要 GitLab.com 基础设施更改,这会减慢新功能的发布速度
- 使用新桶会减慢 GitLab 自托管版用户对新功能的采用:用户需要等到本地 GitLab 管理员配置好新桶后才能使用你的新功能
通过使用现有桶,你可以避免所有这些额外工作和摩擦。AttachmentUploader 使用的 Gitlab.config.uploads 存储位置已经确保被配置好了。
实现直接上传支持
下面我们概述如何实现 直接上传 支持。
使用直接上传并不总是必要的,但通常是个好主意。除非你的功能处理的上传既不频繁也不大,否则你可能需要实现直接上传。小且不频繁上传的例子是项目头像:这些头像很少更改,并且应用对它们施加了严格的尺寸限制。
如果你的功能处理的上传不是既不频繁也不大,那么不实现直接上传支持就意味着你正在承担技术债务。至少,你应该确保你_可以_稍后添加直接上传支持。
要支持直接上传,你需要两样东西:
- Rails 中的预授权端点
- Workhorse 路由规则
Workhorse 不知道你的上传应该存储在哪里。为了找出它,它会进行预授权请求。它也不知道是否或在哪里进行预授权请求。为此你需要路由规则。
给还记得的人一个提示:Workhorse 曾经是一个独立的项目:现在不再需要将这两个步骤分成单独的合并请求了。实际上,在一个合并请求中同时做这两件事可能更容易。
添加 Workhorse 路由规则
路由规则在 workhorse/internal/upstream/routes.go 中定义。它们包括:
- HTTP 动词(通常是 “POST” 或 “PUT”)
- 路径正则表达式
- 上传类型:MIME multipart 或 “完整请求体”
- 可选地,你也可以根据 HTTP 头部(如
Content-Type)进行匹配
示例:
u.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader),你应该为你的路由规则添加测试到 TestAcceleratedUpload 中,该测试位于 workhorse/upload_test.go。
你还应该手动验证当你为新功能执行上传请求时,Workhorse 会进行预授权请求。你可以通过查看 Rails 访问日志来检查这一点。这是必要的,因为如果你在路由规则中出错,你不会得到硬性失败:你只是最终使用了效率较低的默认路径。
添加预授权端点
我们区分三种情况:Rails 控制器、Grape API 端点和 GraphQL 资源。
先说坏消息:目前不支持 GraphQL 的直接上传。原因是 Workhorse 不解析 GraphQL 查询。另请参阅 问题 #280819。考虑通过 Grape 接受你的文件上传。
对于 Grape 预授权端点,寻找实现 /authorize 路由的现有示例。一个例子是 POST :id/uploads/authorize 端点。这个特定示例使用的是 FileUploader,这意味着上传存储在该上传器类的存储位置(桶)中。
对于 Rails 端点,你可以使用 WorkhorseAuthorization 关注点。
处理上传
某些功能需要我们处理上传,例如从上传的文件中提取元数据。你可以通过几种不同的方式实现这一点。主要的选择是在哪里实现处理,或者说"谁是处理器"。
| 处理器 | 支持直接上传? | 可以拒绝 HTTP 请求? | 实现方式 |
|---|---|---|---|
| Sidekiq | 是 | 否 | 简单 |
| Workhorse | 是 | 是 | 复杂 |
| Rails | 否 | 是 | 容易 |
在 Rails 中处理看起来很有吸引力,但它往往会在将来导致扩展问题,因为你不能使用直接上传。然后你被迫用 Workhorse 处理重建你的功能。所以如果你的功能需求允许,在 Sidekiq 中处理可以在复杂性和可扩展性之间取得良好的平衡。
CarrierWave 上传器
GitLab 使用修改版的 CarrierWave 来管理上传。下面我们描述我们如何使用 CarrierWave 以及我们如何修改它。
CarrierWave 的核心概念是 Uploader 类。Uploader 定义文件存储的位置,并可选地包含验证和处理逻辑。要使用 Uploader,你必须将其与 ActiveRecord 模型上的文本列关联起来。这称为"挂载",该列称为 mountpoint。例如:
class Project < ApplicationRecord
mount_uploader :avatar, AttachmentUploader
end现在,如果你上传一个名为 tanuki.png 的头像,那么在你的项目的 projects.avatar 列中,CarrierWave 存储字符串 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。
可选的上传器行为
除了决定上传的存储目录外,CarrierWave 上传器还可以通过回调实现几种其他行为。并非所有这些行为在 GitLab 中都可用。特别是,你目前不能使用 CarrierWave 的 version 机制。你可以做的事情包括:
- 文件名验证
- 与直接上传不兼容:文件内容的一次性预处理,例如图像调整大小
- 与直接上传不兼容:静态加密
CarrierWave 预处理行为(如图像调整大小或加密)需要本地访问上传的文件。这迫使你从 Ruby 上传处理后的文件。这与直接上传背道而驰,直接上传的核心就是不在 Ruby 中进行上传。如果你使用直接上传与具有预处理行为的 Uploader,那么预处理行为会被静默跳过。
CarrierWave 存储引擎
CarrierWave 有 2 个存储引擎:
| CarrierWave 类 | GitLab 名称 | 描述 |
|---|---|---|
CarrierWave::Storage::File |
ObjectStorage::Store::LOCAL |
本地文件,通过 Ruby stdlib 访问 |
CarrierWave::Storage::Fog |
ObjectStorage::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 中有用于在本地存储和对象存储之间迁移用户数据的代码。此代码存在是因为长期以来,GitLab.com 通过 NFS 将上传存储在本地存储中。当我们作为基础设施迁移的一部分必须将上传移动到对象存储时,这发生了变化。
这就是为什么在 GitLab 中 CarrierWave storage 因上传而异,以及为什么我们有像 uploads.store 或 ci_job_artifacts.file_store 这样的数据库列。
通过 Workhorse 直接上传
Workhorse 直接上传是一种机制,让我们可以接受大上传而不会花费大量 Ruby CPU 时间。Workhorse 是用 Go 编写的,goroutine 的资源占用比 Ruby 线程低得多。
直接上传的工作方式如下。
- Workhorse 接受用户上传请求
- Workhorse 使用 Rails 预授权请求,并接收临时上传位置
- Workhorse 将文件上传存储到用户请求的临时上传位置
- Workhorse 将请求传播到 Rails
- Rails 发起远程复制操作,将上传的文件从其临时位置复制到最终位置
- Rails 删除临时上传
- 如果 Rails 超时,Workhorse 会第二次删除临时上传
通常,cache! 返回 CarrierWave::SanitizedFile 的实例,然后 store! 使用 Fog 上传该文件。
在对象存储的情况下,使用 GitLab 特定的修改,从临时位置到最终位置的复制是通过 Rails 欺骗 CarrierWave 实现的。当 CarrierWave 尝试 cache! 上传时,我们返回一个指向临时文件的 CarrierWave::Storage::Fog::File 文件句柄。在 store! 阶段,CarrierWave 然后复制该文件到其预期位置。
表格
Scalability::Frameworks 团队正在使对象存储和上传更易于使用且更健壮。如果你添加或更改上传器,如果你也更新此表,对我们会有帮助。这有助于我们了解上传器的使用位置和方式。
功能桶详情
| 功能 | 上传技术 | 上传器 | 桶结构 |
|---|---|---|---|
| Job artifacts | direct upload |
workhorse |
/artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id> |
| Pipeline artifacts | carrierwave |
sidekiq |
/artifacts/<proj_id_hash>/pipelines/<pipeline_id>/artifacts/<artifact_id> |
| Live job traces | fog |
sidekiq |
/artifacts/tmp/builds/<job_id>/chunks/<chunk_index>.log |
| Job traces archive | carrierwave |
sidekiq |
/artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id>/job.log |
| Autoscale runner caching | Not applicable | gitlab-runner |
/gitlab-com-[platform-]runners-cache/??? |
| Backups | Not applicable | s3cmd, awscli, or gcs |
/gitlab-backups/??? |
| Git LFS | direct upload |
workhorse |
/lfs-objects/<lfs_obj_oid[0:2]>/<lfs_obj_oid[2:2]> |
| Design management thumbnails | carrierwave |
sidekiq |
/uploads/design_management/action/image_v432x230/<model_id>/<original_lfs_obj_oid[2:2]> |
| Generic file uploads | direct upload |
workhorse |
/uploads/@hashed/[0:2]/[2:4]/<hash1>/<hash2>/file |
| Generic file uploads - personal snippets | direct upload |
workhorse |
/uploads/personal_snippet/<snippet_id>/<filename> |
| Global appearance settings | disk buffering |
rails controller |
/uploads/appearance/... |
| Topics | disk buffering |
rails controller |
/uploads/projects/topic/... |
| Avatar images | direct upload |
workhorse |
/uploads/[user,group,project]/avatar/<model_id> |
| Import | direct upload |
workhorse |
/uploads/import_export_upload/import_file/<model_id>/<file_name> |
| Export | carrierwave |
sidekiq |
/uploads/import_export_upload/export_file/<model_id>/<timestamp>_<namespace>-<project_name>_export.tag.gz |
| Placeholder reassignment CSVs | direct_upload |
workhorse |
/uploads/-/system/group/<model_id>/placeholder_reassignment_csv/<file_name> |
| GitLab Migration | carrierwave |
sidekiq |
/uploads/bulk_imports/??? |
| MR diffs | carrierwave |
sidekiq |
/external-diffs/merge_request_diffs/mr-<mr_id>/diff-<diff_id> |
| Package manager assets (except for NPM) | direct upload |
workhorse |
/packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id> |
| NPM Package manager assets | carrierwave |
grape API |
/packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id> |
| Debian Package manager assets | direct upload |
workhorse |
/packages/<group_id or project_id_hash>/debian_*/<group_id or project_id or distribution_file_id> |
| Dependency Proxy cache | send_dependency |
workhorse |
/dependency-proxy/<group_id_hash>/dependency_proxy/<group_id>/files/<blob_id or manifest_id> |
| Terraform state files | carrierwave |
rails controller |
/terraform/<proj_id_hash>/<terraform_state_id> |
| Pages content archives | carrierwave |
sidekiq |
/gitlab-gprd-pages/<proj_id_hash>/pages_deployments/<deployment_id>/ |
| Secure Files | carrierwave |
sidekiq |
/ci-secure-files/<proj_id_hash>/secure_files/<secure_file_id>/ |
CarrierWave 集成
| 文件 | CarrierWave 使用 | 分类 |
|---|---|---|
app/models/project.rb |
include Avatarable |
Yes |
app/models/projects/topic.rb |
include Avatarable |
Yes |
app/models/group.rb |
include Avatarable |
Yes |
app/models/user.rb |
include Avatarable |
Yes |
app/models/terraform/state_version.rb |
include FileStoreMounter |
Yes |
app/models/ci/job_artifact.rb |
include FileStoreMounter |
Yes |
app/models/ci/pipeline_artifact.rb |
include FileStoreMounter |
Yes |
app/models/pages_deployment.rb |
include FileStoreMounter |
Yes |
app/models/lfs_object.rb |
include FileStoreMounter |
Yes |
app/models/dependency_proxy/blob.rb |
include FileStoreMounter |
Yes |
app/models/dependency_proxy/manifest.rb |
include FileStoreMounter |
Yes |
app/models/packages/composer/cache_file.rb |
include FileStoreMounter |
Yes |
app/models/packages/package_file.rb |
include FileStoreMounter |
Yes |
app/models/concerns/packages/debian/component_file.rb |
include FileStoreMounter |
Yes |
ee/app/models/issuable_metric_image.rb |
include FileStoreMounter |
|
ee/app/models/vulnerabilities/remediation.rb |
include FileStoreMounter |
|
ee/app/models/vulnerabilities/export.rb |
include FileStoreMounter |
|
app/models/packages/debian/project_distribution.rb |
include Packages::Debian::Distribution |
Yes |
app/models/packages/debian/group_distribution.rb |
include Packages::Debian::Distribution |
Yes |
app/models/packages/debian/project_component_file.rb |
include Packages::Debian::ComponentFile |
Yes |
app/models/packages/debian/group_component_file.rb |
include Packages::Debian::ComponentFile |
Yes |
app/models/merge_request_diff.rb |
mount_uploader :external_diff, ExternalDiffUploader |
Yes |
app/models/note.rb |
mount_uploader :attachment, AttachmentUploader |
Yes |
app/models/appearance.rb |
mount_uploader :logo, AttachmentUploader |
Yes |
app/models/appearance.rb |
mount_uploader :header_logo, AttachmentUploader |
Yes |
app/models/appearance.rb |
mount_uploader :favicon, FaviconUploader |
Yes |
app/models/project.rb |
mount_uploader :bfg_object_map, AttachmentUploader |
|
app/models/import_export_upload.rb |
mount_uploader :import_file, ImportExportUploader |
Yes |
app/models/import_export_upload.rb |
mount_uploader :export_file, ImportExportUploader |
Yes |
app/models/ci/deleted_object.rb |
mount_uploader :file, DeletedObjectUploader |
|
app/models/design_management/action.rb |
mount_uploader :image_v432x230, DesignManagement::DesignV432x230Uploader |
Yes |
app/models/concerns/packages/debian/distribution.rb |
mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader |
Yes |
app/models/bulk_imports/export_upload.rb |
mount_uploader :export_file, ExportUploader |
Yes |
ee/app/models/user_permission_export_upload.rb |
mount_uploader :file, AttachmentUploader |
|
app/models/ci/secure_file.rb |
include FileStoreMounter |