アップロードガイド新規アップロードの追加

推奨

背景情報

ファイルを保存する場所

CarrierWave Uploader は、ファイルの保存場所を決定します。新しいアップローダクラスを作成する際、新機能のファイルをどこに保存するかを決定します。

まず最初に、新しいUploaderクラスが必要かどうかを自問してください。異なるマウントポイントや異なるモデルに同じUploaderクラスを使用しても問題ありません。

もし独自のUploaderクラスが必要であれば、AttachmentUploaderサブクラスにしてください。 その後、そのクラスから保存場所とディレクトリスキームを継承します。ディレクトリスキームは次のとおりです:

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

GitLabのコードベースを見回すと、独自の保存場所を持つUploaderがたくさんあります。オブジェクトストレージの場合、これは Uploaders が独自のバケットを持つことを意味します。私たちは現在、以下の理由から、 新しいバケットを追加することを推奨していません:

  • 新しいバケットを使うと、GDKOmnibus GitLabCNGのダウンストリームを変更する必要があるため、開発時間が長くなります。
  • 新しいバケットを使うと、GitLab.comインフラストラクチャの変更が必要になり、新機能のロールアウトが遅くなります。
  • 新しいバケットを使うことで、セルフマネジメントのGitLabインストールでの新機能の導入が遅くなります:ローカルのGitLab管理者が新しいバケットを設定するまで、人々はあなたの新機能を使い始めることができません。

既存のバケットを使うことで、このような余分な作業や摩擦を避けることができます。AttachmentUploader が使っているGitlab.config.uploads ストレージロケーションは、すでに設定されていることが保証されています。

ダイレクトアップロードの実装

以下では、ダイレクトアップロードの実装方法について説明します。

ダイレクトアップロードの使用は常に必要というわけではありませんが、通常は良いアイデアです。アップロードの頻度が低く、アップロードの回数が少ない機能でない限り、ダイレクトアップロードを実装することをお勧めします。アップロードの頻度が低くて少ない機能の例としては、プロジェクトのアバターがあります。アバターはめったに変更されませんし、アプリケーションはアバターのサイズに厳しい制限を課しています。

アップロードの頻度が少なく、かつ、アップロードのサイズが小さくない機能を扱う場合、ダイレクトアップロードのサポートを実装しないことは、技術的負債を背負うことを意味します。少なくとも、後で直接アップロードのサポートを追加_できる_ようにするべきです。

ダイレクトアップロードをサポートするには、2つのことが必要です:

  1. Railsの事前承認エンドポイント
  2. Workhorseのルーティングルール

Workhorseはあなたのアップロードをどこに保存すればいいのかわかりません。それを知るために事前承認要求を行います。また、どこで事前承認リクエストを行うかどうかもわかりません。そのためにはルーティングルールが必要です。

Workhorseが以前は別のプロジェクトであったことを覚えている人へのメモです:これらの2つのステップを別々のマージリクエストに分ける必要はもうありません。実際、1つのマージリクエストで両方を行う方が簡単でしょう。

Workhorseルーティングルールの追加

ルーティングルールはworkhorse/internal/upstream/routes.goで定義されます。ルーティングルールの構成は以下の通りです:

  • HTTP 動詞 (通常は “POST” または “PUT”)
  • パスの正規表現
  • アップロードタイプ:MIMEマルチパートまたは “フルリクエストボディ”
  • オプションで、以下のような HTTP ヘッダをマッチさせることもできます。Content-Type

使用例:

u.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader),

workhorse/upload_test.goの TestAcceleratedUpload 、ルーティングルールのテストを追加してください。

また、新機能のアップロードリクエストを実行したときに、Workhorseが事前承認リクエストを行っていることを手動で確認してください。これはRailsのアクセスログを見ることで確認できます。ルーティングルールにミスがあったとしても、ハードエラーにはならないため、これは必要なことです。

事前承認エンドポイントの追加

ここでは3つのケースを区別します:Railsコントローラ、Grape APIエンドポイント、GraphQLリソースです。

悪いニュースから始めると、GraphQLの直接アップロードは現在サポートされていません。WorkhorseがGraphQLクエリを解析しないからです。イシュー#280819も参照してください。代わりにGrape経由でファイルアップロードを受け付けることを検討してください。

Grapeの事前承認エンドポイントについては、/authorize ルートを実装した既存の例を探してください。その一例がPOST:id/uploads/authorize エンドポイントです。この特定の例はFileUploaderを使用しており、アップロードはそのUploaderクラスの保存場所(バケット)に保存されることを意味します。

Railsエンドポイントでは、WorkhorseAuthorizationを使用できます。

アップロードの処理

機能によっては、アップロードされたファイルからメタデータを抽出するなど、アップロードを処理する必要があります。これを実装する方法はいくつかあります。主な選択肢は、_どこで_処理を実装するか、または「誰が処理するか」です。

処理者直接アップロードは可能ですか?HTTPリクエストを拒否できますか?実施
Sidekiqyesいいえストレート
ワークホースyesyesコンプレックス
Railsいいえyes簡単

Railsでの処理は魅力的に見えますが、直接アップロードできないため、スケーリングの問題につながりがちです。その場合、Workhorseでの処理で機能を再構築することを余儀なくされます。そのため、機能の要件が許すのであれば、Sidekiqで処理を行うことが、複雑さとスケーリング能力の間で良いバランスを取ることになります。

CarrierWaveアップローダー

GitLabでは、CarrierWaveの修正版を使ってアップロードを管理しています。以下では、CarrierWave の使い方とその修正方法を説明します。

CarrierWave の中心となる概念はUploaderクラスです。Uploaderはファイルの保存場所を定義し、オプションでバリデーションと処理ロジックを含みます。Uploaderを使用するには、ActiveRecordモデルのテキストカラムに関連付ける必要があります。これは「マウント」と呼ばれ、カラムはmountpoint と呼ばれます。例えば

class 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/ は、設定 (/var/opt/gitlab/gitlab-rails/uploads)、モデル名 (project)、モデル ID (123)、およびマウントポイント (avatar) などを使用してアップローダによって選択されます。

アップローダは、アップロードの個々の保存ディレクトリを決定します。モデルのmountpoint 列にはファイル名が含まれています。

CarrierWaveはファイルハンドルオブジェクトをオペレーションするゲッターとセッターをモデルに定義しているため、mountpoint 列に直接アクセスすることはありません。

オプションのアップローダの動作

アップロード先のディレクトリを決定する以外にも、CarrierWave Uploaderはコールバックを介していくつかの動作を実装することができます。これらの振る舞いのすべてが GitLab で使えるわけではありません。特に、現在のところ CarrierWave のversion メカニズムを使うことはできません。できることは次のとおりです:

  • ファイル名の検証
  • 直接アップロードとは互換性がありません:画像のリサイズなど、ファイル内容の1回限りの前処理
  • 直接アップロードとは互換性がありません:静止時の暗号化

画像のリサイズや暗号化などのCarrierWaveの前処理には、アップロードされたファイルへのローカルアクセスが必要です。このため、Rubyから処理済みファイルをアップロードする必要があります。これは、Rubyでアップロードを_行わない_ダイレクトアップロードに反します。前処理ビヘイビアを持つアップローダで直接アップロードを使用する場合、前処理ビヘイビアは無言でスキップされます。

CarrierWaveストレージエンジン

CarrierWaveには2つのストレージエンジンがあります:

CarrierWaveクラスGitLab名説明
CarrierWave::Storage::FileObjectStorage::Store::LOCALローカルファイルへのアクセスはRubyのstdlib
CarrierWave::Storage::FogObjectStorage::Store::REMOTE Fog gemからアクセスするクラウドファイル

GitLabは設定によってこれらの両方のエンジンを使用します。

CarrierWaveでストレージエンジンを選択する一般的な方法は、Uploader.storage クラスメソッドを使うことです。GitLabではこれを行わず、Uploader#storage をオーバーライドしています。これにより、ファイルごとにストレージエンジンを変えることができます。

CarrierWaveファイルのライフサイクル

アップローダーは、通常のストレージとキャッシュストレージの2つのストレージ領域に関連付けられています。それぞれに独自のストレージ・エンジンがあります。マウントポイントセッター (project.avatar = File.open('/tmp/tanuki.png')) にファイルを割り当てる場合、cache! メソッドを介して、副作用としてキャッシュストレージにファイルをコピー/移動する必要があります。ファイルを永続化するには、何らかの方法でstore! メソッドを store!呼び出す必要があります。これは、store! ActiveRecordコールバックを経由するか、 store!Uploaderインスタンスで呼び出すことで行われます。

通常、cache!store! とやり取りする必要はありませんが、GitLab CarrierWave の変更をデバッグする必要がある場合は、これらのメソッドが存在し、常に呼び出されることを知っておくと便利です。具体的には、CarrierWave の前処理動作 (process など) はbefore :cache フックとして実装されており、直接アップロードする場合はこれらのフックは無視され実行されないことを知っておくとよいでしょう。

直接アップロードでは、すべてのCarrierWavebefore :cache フックをスキップします。

GitLabによるCarrierWaveの修正

GitLabはCarrierWaveの改良版を使って様々なことを可能にしています。

ストレージエンジン間でのデータのマイグレーション

app/uploaders/object_storage.rbには、ローカルストレージとオブジェクトストレージの間でユーザーデータをマイグレーションするコードがあります。このコードが存在するのは、長い間 GitLab.com が NFS 経由でローカルストレージにアップロードを保存していたからです。これが変わったのは、インフラのマイグレーションの一環としてアップロードをオブジェクトストレージに移さなければならなくなったからです。

これが、CarrierWavestorage が GitLab のアップロードごとに異なる理由であり、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は2回目の一時アップロードを削除します。

通常、cache!CarrierWave::SanitizedFile のインスタンスを返し、store!Fog を使ってそのファイルをアップロードします。

オブジェクトストレージの場合、GitLab特有の修正により、一時的な場所から最終的な場所へのコピーはRailsがCarrierWaveを欺くことで実装されます。CarrierWaveがcache! 、一時ファイルを指すCarrierWave::Storage::Fog::File ファイルハンドルを返しますstore! 、CarrierWaveはこのファイルを目的の場所にコピーします。

テーブル

Scalability::Frameworksチームは、オブジェクトストレージとアップロードをより使いやすく、より堅牢なものにしています。アップローダーを追加または変更する場合は、このテーブルも更新していただけると助かります。これは、アップローダがどこでどのように使用されているかの概要を把握するのに役立ちます。

フィーチャーバケットの詳細

機能アップロード技術アップローダーバケット構造
ジョブアーティファクトdirect uploadworkhorse/artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id>
パイプラインアーティファクトcarrierwavesidekiq/artifacts/<proj_id_hash>/pipelines/<pipeline_id>/artifacts/<artifact_id>
ライブジョブトレースfogsidekiq/artifacts/tmp/builds/<job_id>/chunks/<chunk_index>.log
ジョブトレースアーカイブcarrierwavesidekiq/artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id>/job.log
オートスケールランナーキャッシュ該当なしgitlab-runner/gitlab-com-[platform-]runners-cache/???
バックアップ該当なし s3cmd awscli またはgcs /gitlab-backups/???
Git LFSdirect uploadworkhorse/lfs-objects/<lfs_obj_oid[0:2]>/<lfs_obj_oid[2:2]>
デザイン管理ファイルdisk bufferingrails controller/lsf-objects/<lfs_obj_oid[0:2]>/<lfs_obj_oid[2:2]>
デザイン管理サムネイルcarrierwavesidekiq/uploads/design_management/action/image_v432x230/<model_id>/<original_lfs_obj_oid[2:2]
汎用ファイルアップロードdirect uploadworkhorse/uploads/@hashed/[0:2]/[2:4]/<hash1>/<hash2>/file
一般的なファイルのアップロード - 個人的なスニペットdirect uploadworkhorse/uploads/personal_snippet/<snippet_id>/<filename>
グローバルな外観設定disk bufferingrails controller/uploads/appearance/...
トピックスdisk bufferingrails controller/uploads/projects/topic/...
アバター画像direct uploadworkhorse/uploads/[user,group,project]/avatar/<model_id>
インポートdirect uploadworkhorse/uploads/import_export_upload/import_file/<model_id>/<file_name>
エクスポートcarrierwavesidekiq/uploads/import_export_upload/export_file/<model_id>/<timestamp>_<namespace>-<project_name>_export.tag.gz
GitLabマイグレーションcarrierwavesidekiq/uploads/bulk_imports/???
MR の差分carrierwavesidekiq/external-diffs/merge_request_diffs/mr-<mr_id>/diff-<diff_id>
パッケージマネージャ資産(npmを除く)direct uploadworkhorse/packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id>
NPM パッケージマネージャ資産carrierwavegrape API/packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id>
Debian パッケージマネージャ資産direct uploadworkhorse/packages/<group_id or project_id_hash>/debian_*/<group_id or project_id or distribution_file_id>
依存プロキシキャッシュsend_dependencyworkhorse/dependency-proxy/<group_id_hash>/dependency_proxy/<group_id>/files/<blob_id or manifest_id>
Terraform ステートファイルcarrierwaverails controller/terraform/<proj_id_hash>/<terraform_state_id>
コンテンツアーカイブcarrierwavesidekiq/gitlab-gprd-pages/<proj_id_hash>/pages_deployments/<deployment_id>/
セキュアファイルcarrierwavesidekiq/ci-secure-files/<proj_id_hash>/secure_files/<secure_file_id>/

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 FileStoreMounter 
ee/app/models/vulnerabilities/remediation.rbinclude FileStoreMounter 
ee/app/models/vulnerabilities/export.rbinclude FileStoreMounter 
app/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, AttachmentUploader 
app/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, DeletedObjectUploader 
app/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, AttachmentUploader 
app/models/ci/secure_file.rbinclude FileStoreMounter