Help us learn about your current experience with the documentation. Take the survey.

上传指南:添加新上传

建议

背景信息

你应该将文件存储在哪里?

CarrierWave 上传器决定文件的存储位置。当你创建新的上传器类时,你就是在决定新功能的文件存储位置。

首先,问问自己是否需要新的上传器类。为不同的挂载点或不同的模型使用相同的上传器类是可以的。

如果你确实需要自己的上传器类,那么应该让它成为 AttachmentUploader 的子类。这样你就可以继承该类的存储位置和目录结构。目录结构如下:

File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)

如果你在 GitLab 代码库中搜索,会发现许多上传器都有自己的存储位置。对于对象存储,这意味着每个上传器都有自己的桶。我们现在不鼓励添加新桶,原因如下:

  • 使用新桶会增加开发时间,因为你需要在 GDKOmnibus GitLabCNG 中进行下游更改
  • 使用新桶需要 GitLab.com 基础设施更改,这会减慢新功能的发布速度
  • 使用新桶会减慢 GitLab 自托管版用户对新功能的采用:用户需要等到本地 GitLab 管理员配置好新桶后才能使用你的新功能

通过使用现有桶,你可以避免所有这些额外工作和摩擦。AttachmentUploader 使用的 Gitlab.config.uploads 存储位置已经确保被配置好了。

实现直接上传支持

下面我们概述如何实现 直接上传 支持。

使用直接上传并不总是必要的,但通常是个好主意。除非你的功能处理的上传既不频繁也不大,否则你可能需要实现直接上传。小且不频繁上传的例子是项目头像:这些头像很少更改,并且应用对它们施加了严格的尺寸限制。

如果你的功能处理的上传不是既不频繁也不大,那么不实现直接上传支持就意味着你正在承担技术债务。至少,你应该确保你_可以_稍后添加直接上传支持。

要支持直接上传,你需要两样东西:

  1. Rails 中的预授权端点
  2. 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.storeci_job_artifacts.file_store 这样的数据库列。

通过 Workhorse 直接上传

Workhorse 直接上传是一种机制,让我们可以接受大上传而不会花费大量 Ruby CPU 时间。Workhorse 是用 Go 编写的,goroutine 的资源占用比 Ruby 线程低得多。

直接上传的工作方式如下。

  1. Workhorse 接受用户上传请求
  2. Workhorse 使用 Rails 预授权请求,并接收临时上传位置
  3. Workhorse 将文件上传存储到用户请求的临时上传位置
  4. Workhorse 将请求传播到 Rails
  5. Rails 发起远程复制操作,将上传的文件从其临时位置复制到最终位置
  6. Rails 删除临时上传
  7. 如果 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 check-circle Yes
app/models/projects/topic.rb include Avatarable check-circle Yes
app/models/group.rb include Avatarable check-circle Yes
app/models/user.rb include Avatarable check-circle Yes
app/models/terraform/state_version.rb include FileStoreMounter check-circle Yes
app/models/ci/job_artifact.rb include FileStoreMounter check-circle Yes
app/models/ci/pipeline_artifact.rb include FileStoreMounter check-circle Yes
app/models/pages_deployment.rb include FileStoreMounter check-circle Yes
app/models/lfs_object.rb include FileStoreMounter check-circle Yes
app/models/dependency_proxy/blob.rb include FileStoreMounter check-circle Yes
app/models/dependency_proxy/manifest.rb include FileStoreMounter check-circle Yes
app/models/packages/composer/cache_file.rb include FileStoreMounter check-circle Yes
app/models/packages/package_file.rb include FileStoreMounter check-circle Yes
app/models/concerns/packages/debian/component_file.rb include FileStoreMounter check-circle 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 check-circle Yes
app/models/packages/debian/group_distribution.rb include Packages::Debian::Distribution check-circle Yes
app/models/packages/debian/project_component_file.rb include Packages::Debian::ComponentFile check-circle Yes
app/models/packages/debian/group_component_file.rb include Packages::Debian::ComponentFile check-circle Yes
app/models/merge_request_diff.rb mount_uploader :external_diff, ExternalDiffUploader check-circle Yes
app/models/note.rb mount_uploader :attachment, AttachmentUploader check-circle Yes
app/models/appearance.rb mount_uploader :logo, AttachmentUploader check-circle Yes
app/models/appearance.rb mount_uploader :header_logo, AttachmentUploader check-circle Yes
app/models/appearance.rb mount_uploader :favicon, FaviconUploader check-circle Yes
app/models/project.rb mount_uploader :bfg_object_map, AttachmentUploader
app/models/import_export_upload.rb mount_uploader :import_file, ImportExportUploader check-circle Yes
app/models/import_export_upload.rb mount_uploader :export_file, ImportExportUploader check-circle Yes
app/models/ci/deleted_object.rb mount_uploader :file, DeletedObjectUploader
app/models/design_management/action.rb mount_uploader :image_v432x230, DesignManagement::DesignV432x230Uploader check-circle Yes
app/models/concerns/packages/debian/distribution.rb mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader check-circle Yes
app/models/bulk_imports/export_upload.rb mount_uploader :export_file, ExportUploader check-circle Yes
ee/app/models/user_permission_export_upload.rb mount_uploader :file, AttachmentUploader
app/models/ci/secure_file.rb include FileStoreMounter