アップロードガイド新規アップロードの追加
推奨
- アップローダを作成する際は、そのアップローダを
AttachmentUploader
- アップローダをこのドキュメントのテーブルに追加します。
- 新しいオブジェクトストレージのバケットを追加しないでください。
- 直接アップロードの実装
- アップロードを処理する必要がある場合は、その処理場所を決定します。
背景情報
ファイルを保存する場所
CarrierWave Uploader は、ファイルの保存場所を決定します。新しいアップローダクラスを作成する際、新機能のファイルをどこに保存するかを決定します。
まず最初に、新しいUploaderクラスが必要かどうかを自問してください。異なるマウントポイントや異なるモデルに同じUploaderクラスを使用しても問題ありません。
もし独自のUploaderクラスが必要であれば、AttachmentUploader
のサブクラスにしてください。 その後、そのクラスから保存場所とディレクトリスキームを継承します。ディレクトリスキームは次のとおりです:
File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
GitLabのコードベースを見回すと、独自の保存場所を持つUploaderがたくさんあります。オブジェクトストレージの場合、これは Uploaders が独自のバケットを持つことを意味します。私たちは現在、以下の理由から、 新しいバケットを追加することを推奨していません:
- 新しいバケットを使うと、GDK、Omnibus GitLab、CNGのダウンストリームを変更する必要があるため、開発時間が長くなります。
- 新しいバケットを使うと、GitLab.comインフラストラクチャの変更が必要になり、新機能のロールアウトが遅くなります。
- 新しいバケットを使うことで、セルフマネジメントのGitLabインストールでの新機能の導入が遅くなります:ローカルのGitLab管理者が新しいバケットを設定するまで、人々はあなたの新機能を使い始めることができません。
既存のバケットを使うことで、このような余分な作業や摩擦を避けることができます。AttachmentUploader
が使っているGitlab.config.uploads
ストレージロケーションは、すでに設定されていることが保証されています。
ダイレクトアップロードの実装
以下では、ダイレクトアップロードの実装方法について説明します。
ダイレクトアップロードの使用は常に必要というわけではありませんが、通常は良いアイデアです。アップロードの頻度が低く、アップロードの回数が少ない機能でない限り、ダイレクトアップロードを実装することをお勧めします。アップロードの頻度が低くて少ない機能の例としては、プロジェクトのアバターがあります。アバターはめったに変更されませんし、アプリケーションはアバターのサイズに厳しい制限を課しています。
アップロードの頻度が少なく、かつ、アップロードのサイズが小さくない機能を扱う場合、ダイレクトアップロードのサポートを実装しないことは、技術的負債を背負うことを意味します。少なくとも、後で直接アップロードのサポートを追加_できる_ようにするべきです。
ダイレクトアップロードをサポートするには、2つのことが必要です:
- Railsの事前承認エンドポイント
- 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リクエストを拒否できますか? | 実施 |
---|---|---|---|
Sidekiq | yes | いいえ | ストレート |
ワークホース | yes | yes | コンプレックス |
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::File | ObjectStorage::Store::LOCAL | ローカルファイルへのアクセスはRubyのstdlib
|
CarrierWave::Storage::Fog | ObjectStorage::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
フックとして実装されており、直接アップロードする場合はこれらのフックは無視され実行されないことを知っておくとよいでしょう。
直接アップロードでは、すべてのCarrierWave
before :cache
フックをスキップします。
GitLabによるCarrierWaveの修正
GitLabはCarrierWaveの改良版を使って様々なことを可能にしています。
ストレージエンジン間でのデータのマイグレーション
app/uploaders/object_storage.rbには、ローカルストレージとオブジェクトストレージの間でユーザーデータをマイグレーションするコードがあります。このコードが存在するのは、長い間 GitLab.com が NFS 経由でローカルストレージにアップロードを保存していたからです。これが変わったのは、インフラのマイグレーションの一環としてアップロードをオブジェクトストレージに移さなければならなくなったからです。
これが、CarrierWavestorage
が GitLab のアップロードごとに異なる理由であり、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は2回目の一時アップロードを削除します。
通常、cache!
はCarrierWave::SanitizedFile
のインスタンスを返し、store!
はFog を使ってそのファイルをアップロードします。
オブジェクトストレージの場合、GitLab特有の修正により、一時的な場所から最終的な場所へのコピーはRailsがCarrierWaveを欺くことで実装されます。CarrierWaveがcache!
、一時ファイルを指すCarrierWave::Storage::Fog::File
ファイルハンドルを返します。store!
、CarrierWaveはこのファイルを目的の場所にコピーします。
テーブル
Scalability::Frameworksチームは、オブジェクトストレージとアップロードをより使いやすく、より堅牢なものにしています。アップローダーを追加または変更する場合は、このテーブルも更新していただけると助かります。これは、アップローダがどこでどのように使用されているかの概要を把握するのに役立ちます。
フィーチャーバケットの詳細
機能 | アップロード技術 | アップローダー | バケット構造 |
---|---|---|---|
ジョブアーティファクト | direct upload | workhorse | /artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id> |
パイプラインアーティファクト | carrierwave | sidekiq | /artifacts/<proj_id_hash>/pipelines/<pipeline_id>/artifacts/<artifact_id> |
ライブジョブトレース | fog | sidekiq | /artifacts/tmp/builds/<job_id>/chunks/<chunk_index>.log |
ジョブトレースアーカイブ | carrierwave | sidekiq | /artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id>/job.log |
オートスケールランナーキャッシュ | 該当なし | gitlab-runner | /gitlab-com-[platform-]runners-cache/??? |
バックアップ | 該当なし |
s3cmd awscli またはgcs
| /gitlab-backups/??? |
Git LFS | direct upload | workhorse | /lfs-objects/<lfs_obj_oid[0:2]>/<lfs_obj_oid[2:2]> |
デザイン管理ファイル | disk buffering | rails controller | /lsf-objects/<lfs_obj_oid[0:2]>/<lfs_obj_oid[2:2]> |
デザイン管理サムネイル | carrierwave | sidekiq | /uploads/design_management/action/image_v432x230/<model_id>/<original_lfs_obj_oid[2:2] |
汎用ファイルアップロード | direct upload | workhorse | /uploads/@hashed/[0:2]/[2:4]/<hash1>/<hash2>/file |
一般的なファイルのアップロード - 個人的なスニペット | direct upload | workhorse | /uploads/personal_snippet/<snippet_id>/<filename> |
グローバルな外観設定 | disk buffering | rails controller | /uploads/appearance/... |
トピックス | disk buffering | rails controller | /uploads/projects/topic/... |
アバター画像 | direct upload | workhorse | /uploads/[user,group,project]/avatar/<model_id> |
インポート | direct upload | workhorse | /uploads/import_export_upload/import_file/<model_id>/<file_name> |
エクスポート | carrierwave | sidekiq | /uploads/import_export_upload/export_file/<model_id>/<timestamp>_<namespace>-<project_name>_export.tag.gz |
GitLabマイグレーション | carrierwave | sidekiq | /uploads/bulk_imports/??? |
MR の差分 | carrierwave | sidekiq | /external-diffs/merge_request_diffs/mr-<mr_id>/diff-<diff_id> |
パッケージマネージャ資産(npmを除く) | direct upload | workhorse | /packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id> |
NPM パッケージマネージャ資産 | carrierwave | grape API | /packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id> |
Debian パッケージマネージャ資産 | direct upload | workhorse | /packages/<group_id or project_id_hash>/debian_*/<group_id or project_id or distribution_file_id> |
依存プロキシキャッシュ | send_dependency | workhorse | /dependency-proxy/<group_id_hash>/dependency_proxy/<group_id>/files/<blob_id or manifest_id> |
Terraform ステートファイル | carrierwave | rails controller | /terraform/<proj_id_hash>/<terraform_state_id> |
コンテンツアーカイブ | carrierwave | sidekiq | /gitlab-gprd-pages/<proj_id_hash>/pages_deployments/<deployment_id>/ |
セキュアファイル | carrierwave | sidekiq | /ci-secure-files/<proj_id_hash>/secure_files/<secure_file_id>/ |
CarrierWaveインテグレーション
ファイル | CarrierWaveの使い方 | カテゴリー |
---|---|---|
app/models/project.rb | include Avatarable | {チェックサークル}はい |
app/models/projects/topic.rb | include Avatarable | {チェックサークル}はい |
app/models/group.rb | include Avatarable | {チェックサークル}はい |
app/models/user.rb | include Avatarable | {チェックサークル}はい |
app/models/terraform/state_version.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/ci/job_artifact.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/ci/pipeline_artifact.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/pages_deployment.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/lfs_object.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/dependency_proxy/blob.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/dependency_proxy/manifest.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/packages/composer/cache_file.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/packages/package_file.rb | include FileStoreMounter | {チェックサークル}はい |
app/models/concerns/packages/debian/component_file.rb | include FileStoreMounter | {チェックサークル}はい |
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 | {チェックサークル}はい |
app/models/packages/debian/group_distribution.rb | include Packages::Debian::Distribution | {チェックサークル}はい |
app/models/packages/debian/project_component_file.rb | include Packages::Debian::ComponentFile | {チェックサークル}はい |
app/models/packages/debian/group_component_file.rb | include Packages::Debian::ComponentFile | {チェックサークル}はい |
app/models/merge_request_diff.rb | mount_uploader :external_diff, ExternalDiffUploader | {チェックサークル}はい |
app/models/note.rb | mount_uploader :attachment, AttachmentUploader | {チェックサークル}はい |
app/models/appearance.rb | mount_uploader :logo, AttachmentUploader | {チェックサークル}はい |
app/models/appearance.rb | mount_uploader :header_logo, AttachmentUploader | {チェックサークル}はい |
app/models/appearance.rb | mount_uploader :favicon, FaviconUploader | {チェックサークル}はい |
app/models/project.rb | mount_uploader :bfg_object_map, AttachmentUploader | |
app/models/import_export_upload.rb | mount_uploader :import_file, ImportExportUploader | {チェックサークル}はい |
app/models/import_export_upload.rb | mount_uploader :export_file, ImportExportUploader | {チェックサークル}はい |
app/models/ci/deleted_object.rb | mount_uploader :file, DeletedObjectUploader | |
app/models/design_management/action.rb | mount_uploader :image_v432x230, DesignManagement::DesignV432x230Uploader | {チェックサークル}はい |
app/models/concerns/packages/debian/distribution.rb | mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader | {チェックサークル}はい |
app/models/bulk_imports/export_upload.rb | mount_uploader :export_file, ExportUploader | {チェックサークル}はい |
ee/app/models/user_permission_export_upload.rb | mount_uploader :file, AttachmentUploader | |
app/models/ci/secure_file.rb | include FileStoreMounter |