업로드 가이드: 새 업로드 추가

권장 사항

배경 정보

파일을 어디에 저장해야 하나요?

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 저장 위치는 이미 구성되어 있는 것이 보장됩니다.

직접 업로드 지원 구현

아래에서 직접 업로드 지원을 구현하는 방법을 설명합니다.

직접 업로드를 사용하는 것이 항상 필요한 것은 아니지만 일반적으로 좋은 생각입니다. 기능에서 다루는 업로드가 드물고 작을 경우에만 직접 업로드를 구현하려는 경우가 있을 수 있습니다. 예를 들어, 프로젝트 아바타와 같이 드물고 사이즈 제한이 엄격하게 적용되는 경우가 그 예입니다.

만약 기능에서 듬성듬성하거나 작지 않은 업로드를 다룬다면, 직접 업로드 지원을 구현하지 않는다는 것은 기술적 부채를 져야 한다는 것을 의미합니다. 적어도 나중에 직접 업로드 지원을 추가할 수 있는지 확인해야 합니다.

직접 업로드를 지원하기 위해 두 가지가 필요합니다:

  1. Rails 내 사전 인가 엔드포인트
  2. Workhorse 라우팅 규칙

Workhorse는 업로드를 저장할 위치를 알지 못합니다. 이를 알아내기 위해 사전 인가 요청을 수행합니다. 또한 어디에 또는 언제 사전 인가 요청을 수행해야 하는지에 대해도 알지 못합니다. 이를 위해 라우팅 규칙이 필요합니다.

Workhorse가 별도의 프로젝트였던 과거를 기억하는 사람들을 위한 참고: 이제 이러한 두 단계를 별도의 머지 요청으로 분할할 필요가 없습니다. 사실, 두 단계를 하나의 머지 요청으로 수행하는 것이 더 쉬울 것입니다.

Workhorse 라우팅 규칙 추가

라우팅 규칙은 workhorse/internal/upstream/routes.go에 정의되어 있습니다. 다음으로 구성되어 있습니다:

  • HTTP 동사 (“POST” 또는 “PUT”이 일반적으로 사용됨)
  • 경로 정규식
  • 업로드 유형: MIME 멀티파트 또는 “전체 요청 본문”
  • 선택적으로 Content-Type과 같은 HTTP 헤더와 일치시킬 수 있습니다.

예시:

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

라우팅 규칙에 대한 테스트를 TestAcceleratedUpload에 추가해야 합니다. 이를 workhorse/upload_test.go에서 수행할 수 있습니다.

또한 새로운 기능에 대한 업로드 요청을 수행할 때 Workhorse가 사전 인가 요청을 수행하는지 매뉴얼으로 확인해야 합니다. 이를 확인하는 이유는 라우팅 규칙에 오류가 있다면 강력한 실패가 발생하지 않고 전혀 다른 경로를 사용할 수 있기 때문입니다.

사전 인가 엔드포인트 추가

우리는 세 가지 경우를 구별합니다: Rails 컨트롤러, Grape API 엔드포인트, 그리고 GraphQL 리소스입니다.

먼저 나쁜 소식으로 시작하자면, 현재는 GraphQL에 대한 직접 업로드가 지원되지 않습니다. 그 이유는 Workhorse가 GraphQL 쿼리를 파싱하지 않기 때문입니다. 또한 이슈 #280819를 참조하시기 바랍니다. 그러므로 Grape 대신 GraphQL을 통해 파일 업로드를 허용하는 것을 고려하십시오.

Grape 사전 인가 엔드포인트의 경우, /authorize 라우트를 구현한 기존 예제를 찾아보세요. 한 예로는 POST :id/uploads/authorize 엔드포인트가 있습니다. 이 특정 예제는 FileUploader를 사용하고 있으므로, 업로드가 해당 Uploader 클래스의 저장 위치(버킷)에 저장되는 것을 의미합니다.

Rails 엔드포인트의 경우, WorkhorseAuthorization concern를 사용할 수 있습니다.

업로드 처리

일부 기능은 업로드를 처리해야 하는 경우가 있습니다. 예를 들어, 업로드된 파일에서 메타데이터를 추출해야 할 필요가 있습니다. 이를 구현할 수 있는 몇 가지 다른 방법이 있습니다. 주요한 선택 사항은 처리를 구현하는 위치 또는 “처리자는 누구인가”입니다.

처리자 직접 업로드 가능 여부 HTTP 요청 거부 가능 여부 구현
Sidekiq 아니요 직관적입니다
Workhorse 복잡합니다
Rails 아니요 쉽습니다

Rails에서 처리를 하면 유망해 보이지만 직접 업로드를 사용할 수 없으므로 적용을 계속하면 이후에 스케일링 문제로 이어질 수 있습니다. 그럼에도 불구하고 지원 가능한 경우, Sidekiq에서 처리하는 것은 복잡성과 스케일링 능력 사이의 좋은 균형을 유지합니다.

CarrierWave 업로더

GitLab은 파일 관리를 위해 CarrierWave의 수정 버전을 사용합니다. 아래에서 CarrierWave의 사용 방법과 어떻게 수정되었는지 설명하겠습니다.

CarrierWave의 핵심 개념은 업로더 클래스입니다. 업로더는 파일이 어디에 저장되는지 정의하고 선택적으로 유효성 검사 및 처리 로직을 포함합니다. 업로더를 사용하려면 해당 업로더를 ActiveRecord 모델의 텍스트 칼럼에 연결해야 합니다. 이를 “마운트(mount)”하고 칼럼은 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 경로에 있을 수 있습니다. 실제 디렉터리는 Uploader가 구성(var/opt/gitlab/gitlab-rails/uploads), 모델 이름(project), 모델 ID(123), 그리고 마운트 지점(avatar)을 사용하여 선택되었습니다.

업로더는 당신의 업로드의 개별 저장 디렉터리를 결정합니다. 당신의 모델에 있는 mountpoint 칼럼은 파일 이름을 포함합니다.

CarrierWave는 모델에 내 mountpoint 칼럼을 직접 액세스하지 않도록 정의합니다.왜냐하면 CarrierWave는 파일 핸들 객체에서 동작하는 모델의 게터와 세터를 정의하기 때문입니다.

선택 사항 업로더 동작

업로드의 저장 디렉터리를 결정하는 것 외에도 CarrierWave Uploader는 콜백을 통해 여러 다른 동작을 구현할 수 있습니다. 모든 이러한 동작이 GitLab에서 사용 가능한 것은 아닙니다. 특히 현재 CarrierWave의 version 메커니즘을 사용할 수 없습니다. 할 수 있는 일은 다음과 같습니다:

  • 파일 이름 유효성 검사
  • 직접 업로드와 호환되지 않음: 예를 들어 이미지 크기 조정 등 파일 내용의 일회성 전처리
  • 직접 업로드와 호환되지 않음: 취약한 환경에서의 암호화

CarrierWave의 사전 처리 동작(예: 이미지 크기 조정 또는 암호화)은 업로드된 파일을 로컬에서 액세스해야 합니다. 따라서 이로 인해 처리된 파일을 루비(Ruby)에서 업로드해야 합니다. 이것은 직접 업로드의 취지에 어긋나는데, 직접 업로드는 바로 루비에서 업로드하는 것을 회피하는 것이 핵심입니다. 따라서 사전 처리 동작을 가진 업로더로 직접 업로드를 사용하면 사전 처리 동작이 묵묵히 건너뜁니다.

CarrierWave 저장 엔진

CarrierWave에는 2가지 저장 엔진이 있습니다:

CarrierWave 클래스 GitLab 이름 설명
CarrierWave::Storage::File ObjectStorage::Store::LOCAL 루비의 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!CarrierWave::SanitizedFile의 인스턴스를 반환하며, store!는 그 파일을 Fog를 사용하여 업로드합니다.

오브젝트 스토리지 솔루션의 경우, GitLab에 특화된 수정을 가미한 파일의 임시 위치로부터 최종 위치로의 복사 작업은 레일스에서 CarrierWave를 속이면서 구현됩니다. CarrierWave가 업로드를 캐시하려고 시도할 때, 우리는 임시 파일을 가리키는 CarrierWave::Storage::Fog::File 파일 핸들을 반환하고, 저장 단계에서 CarrierWave는 이 파일을 그 위치로 복사합니다.

테이블

Scalability::Frameworks 팀은 오브젝트 스토리지 및 업로드를 보다 사용하기 쉽고 견고하게 만들고 있습니다. 업로더를 추가하거나 변경하는 경우 테이블을 업데이트하면 우리가 어디에서 어떻게 업로더를 사용하는지 파악하는 데 도움이 됩니다.

기능 버킷 세부 정보

기능 업로드 기술 업로더 버킷 구조
작업 아티팩트 직접 업로드 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
Auto Scale 러너 캐싱 해당 없음 gitlab-runner /gitlab-com-[platform-]runners-cache/???
백업 해당 없음 s3cmd, awscli, 또는 gcs /gitlab-backups/???
Git LFS 직접 업로드 workhorse /lfs-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]
제네릭 파일 업로드 직접 업로드 workhorse /uploads/@hashed/[0:2]/[2:4]/<hash1>/<hash2>/file
제네릭 파일 업로드 - 개인 스니펫 직접 업로드 workhorse /uploads/personal_snippet/<snippet_id>/<filename>
전역 외형 설정 디스크 버퍼링 rails controller /uploads/appearance/...
주제 디스크 버퍼링 rails controller /uploads/projects/topic/...
아바타 이미지 직접 업로드 workhorse /uploads/[user,group,project]/avatar/<model_id>
Import 직접 업로드 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
GitLab 마이그레이션 carrierwave sidekiq /uploads/bulk_imports/???
MR 차이 carrierwave sidekiq /external-diffs/merge_request_diffs/mr-<mr_id>/diff-<diff_id>
패키지 관리자 자산(NPM 제외) 직접 업로드 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>
데비안 패키지 관리자 자산 직접 업로드 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>/
Secure 파일 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