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

추천 사항

배경 정보

파일을 어디에 저장해야 하는가?

CarrierWave 업로더는 파일을 저장할 위치를 결정합니다. 새 업로더 클래스를 만들 때 새로운 기능의 파일을 저장할 위치를 결정하게 됩니다.

우선, 새로운 업로더 클래스가 필요한지 스스로에게 물어보세요. 동일한 업로더 클래스를 다른 마운트 지점이나 다른 모델에 사용하는 것은 괜찮습니다.

새로운 업로더 클래스가 필요하거나 원한다면, 해당 클래스를 AttachmentUploader의 하위 클래스로 만들어야 합니다. 그러면 해당 클래스로부터 저장 위치 및 디렉터리 체계를 상속받습니다. 디렉터리 체계는 다음과 같습니다:

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

GitLab 코드베이스를 둘러보면 여러 업로더가 고유한 저장 위치를 갖고 있음을 알 수 있습니다. 객체 저장을 위해, 이는 업로더가 고유한 버킷을 갖게 됨을 의미합니다. 이제 다음과 같은 이유로 새 버킷을 추가하는 것을 거부합니다:

  • 새 버킷을 사용하면 GDK, Omnibus GitLabCNG에서 하위 변경을 수행해야 하므로 개발 시간이 소요됩니다.
  • 새 버킷 사용은 GitLab.com 인프라 변경을 필요로 하며, 이는 새로운 기능의 전개를 늦춥니다.
  • 새 버킷의 사용은 셀프-매니지드 GitLab 설치의 새로운 기능의 채택을 늦춥니다: 사용자들은 로컬 GitLab 관리자가 새 버킷을 구성하기 전까지 새 기능을 사용할 수 없습니다.

기존 버킷을 사용함으로써 모든 이러한 추가 작업과 마찰을 피할 수 있습니다. 즉, AttachmentUploader가 사용하는 Gitlab.config.uploads 저장 위치는 이미 구성되어 있다는 것이 보장됩니다.

직접 업로드 지원 구현

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

직접 업로드를 사용하는 것이 항상 필요한 것은 아니지만 일반적으로 좋은 아이디어입니다. 기능에서 다루는 업로드가 드물고 작은 경우를 제외하고는 대부분의 경우 직접 업로드를 구현하는 것이 좋습니다. 드물고 크기가 작은 업로드를 다루는 기능의 예는 프로젝트 아바타입니다. 이러한 아바타는 드물게 변경되고 응용 프로그램은 그에 대한 엄격한 크기 제한을 부과합니다.

기능이 드물지 않고 작지 않은 업로드를 다룬다면, 직접 업로드 지원을 구현하지 않으면 기술적 부채를 갖게 되는 것입니다. 적어도 나중에 직접 업로드 지원을 추가할 수 있는지 확인해야 합니다.

직접 업로드를 지원하려면 두 가지가 필요합니다:

  1. 레일스에서 사전 승인 엔드포인트
  2. Workhorse 라우팅 규칙

Workhorse는 업로드를 저장할 위치를 알지 못합니다. 이를 알아내기 위해 사전 승인 요청을 수행합니다. 또한 사전 승인 요청을 수행할 위치나 방법을 모릅니다. 이를 위해 라우팅 규칙이 필요합니다.

workhorse/internal/upstream/routes.go에서 라우팅 규칙을 정의합니다. 이는 다음으로 구성됩니다:

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

예시:

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

workhorse/upload_test.go에서 TestAcceleratedUpload에 대한 규칙을 추가해야 합니다.

새로운 기능의 업로드 요청을 수행할 때, Workhorse가 사전 승인 요청을 수행하는지 수동으로 확인해야 합니다. 정의된 라우팅 규칙에 실수가 있는 경우 강력한 실패가 아닌 덜 효율적인 기본 경로를 사용하게 됩니다.

사전 승인 엔드포인트 추가

우리는 세 가지 경우로 구분합니다: 레일스 컨트롤러, 그레이프 API 엔드포인트 및 GraphQL 리소스.

안좋은 소식에서 시작해보겠습니다: 현재 GraphQL의 직접 업로드는 지원되지 않습니다. 이는 Workhorse가 GraphQL 쿼리를 구문 분석하지 않기 때문입니다. 또한 이슈 #280819를 참조하세요. 따라서 그대로 GraphQL을 통해 파일을 업로드 받는 대신 Grape를 통해 업로드를 수락하는 것을 고려하세요.

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

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

업로드 처리

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

처리자 직접 업로드 가능? HTTP 요청 거부 가능? 구현
Sidekiq 아니요 간단함
Workhorse 복잡함
Rails 아니요 쉬움

Rails에서 처리하는 것은 매력적으로 보이지만 직접 업로드를 사용할 수 없으므로 나준에 스케일링 문제로 이어질 가능성이 있습니다. 그러면 나중에 Workhorse에서 처리하도록 기능을 다시 구축해야 합니다. 따라서 기능 요구사항이 허용하는 경우, Sidekiq에서 처리하는 것은 복잡성과 확장 가능성 사이의 좋은 균형을 이룹니다.

CarrierWave 업로더

GitLab은 업로드를 관리하기 위해 CarrierWave의 수정 버전을 사용합니다. 아래에서 CarrierWave 사용 방법과 수정한 내용에 대해 설명합니다.

CarrierWave의 중심 개념은 Uploader 클래스입니다. Uploader는 파일 저장 위치를 정의하고 선택적으로 유효성 검사 및 처리 논리를 포함할 수 있습니다. Uploader를 사용하려면 ActiveRecord 모델의 텍스트 열과 연관시켜야 합니다. 이를 “마운팅”이라고 하며 열은 마운트포인트라고 합니다. 예를 들면:

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)입니다.

업로더는 업로드의 개별 저장 디렉터리를 결정합니다. 모델의 mountpoint 열에는 파일 이름이 포함되어 있습니다.

CarrierWave는 다른 콜백을 통해 업로드의 저장 디렉터리를 결정할 수 있습니다. GitLab에서 사용할 수 없는 콜백도 구현할 수 있습니다. 특히 현재 CarrierWave의 version 매커니즘을 사용할 수 없습니다. 할 수 있는 작업은 다음과 같습니다:

  • 파일 이름 유효성 검사
  • 직접 업로드와 호환되지 않음: 파일 내용의 일회성 전처리(예: 이미지 크기 조정)
  • 직접 업로드와 호환되지 않음: 정지된 시 파일의 암호화

이미지 크기 조정 또는 암호화와 같은 CarrierWave의 전처리 작업은 업로드된 파일에 로컬 액세스가 필요합니다. 따라서 처리된 파일을 루비에서 업로드해야 합니다. 이것은 바로 업로드를 루비에서 수행하지 않는 직접 업로드와 상반됩니다. 업로더가 전처리 기능을 가진 직접 업로드를 사용하면 전처리 기능이 무음으로 건너뜁니다.

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는 일반 저장소와 캐시 저장소 두 곳과 연관됩니다. 각각에는 고유한 저장 엔진이 있습니다. 마운트 포인트 세터(project.avatar = File.open('/tmp/tanuki.png'))에 파일을 할당하면 cache! 메서드를 통해 파일을 캐시 저장소로 복사/이동해야 합니다. 파일을 유지하려면 ActiveRecord callbacks를 통해 store! 메서드를 호출해야 합니다. 이것은 항상 호출되는 메서드입니다. 일반적으로 cache!store!와 상호작용할 필요는 없지만, GitLab CarrierWave 수정을 디버깅해야 하는 경우 이러한 메서드가 존재하고 항상 호출된다는 것을 알아둘 필요가 있습니다. 특히 CarrierWave의 전처리 작업(process 등)은 before :cache 훅으로 구현됩니다. 직접 업로드의 경우 이러한 훅은 무시되고 실행되지 않습니다.

직접 업로드는 모든 CarrierWave before :cache 훅을 건너뜁니다.

CarrierWave의 GitLab 수정

GitLab은 여러 기능을 가능하게 하기 위해 CarrierWave의 수정 버전을 사용합니다.

저장 엔진 간 데이터 마이그레이션

app/uploaders/object_storage.rb에는 로컬 저장소와 객체 저장소 간에 사용자 데이터를 마이그레이션하기 위한 코드가 있습니다. GitLab.com은 오랜 시간 NFS를 통해 로컬 저장소에 업로드를 저장했습니다. 이것은 인프라 마이그레이션의 일환으로 업로드를 객체 저장소로 이동해야 했기 때문에 변경되었습니다.

이것이 GitLab에서 업로드마다 CarrierWave storage가 달라지고 uploads.store 또는 ci_job_artifacts.file_store와 같은 데이터베이스 열이 있는 이유입니다.

Workhorse를 통한 직접 업로드

Workhorse의 직접 업로드는 많은 러비 CPU 시간을 소비하지 않고 큰 업로드를 수락할 수 있는 메커니즘입니다. Workhorse는 Go로 작성되었으며 고루틴은 루비 스레드보다 자원 풋트프린트가 훨씬 작습니다.

직접 업로드는 다음과 같이 작동합니다.

  1. Workhorse가 사용자 업로드 요청을 수락함
  2. Workhorse는 임시 업로드 위치를 사전 인증과정을 수행하고 받음
  3. Workhorse가 사용자의 요청으로 파일 업로드를 임시 업로드 위치에 저장함
  4. Workhorse가 요청을 Rails로 전파함
  5. Rails는 업로드된 파일을 임시 위치에서 최종 위치로 복사하는 원격 복사 작업을 수행함
  6. Rails는 임시 업로드를 삭제함
  7. Rails는 경우에 따라 작업 처리시간을 초과해도 두 번째로 임시 업로드를 삭제함

일반적으로 cache!CarrierWave::SanitizedFile 인스턴스를 반환하고, 그런 다음 store!은 이 파일을 Fog를 사용하여 업로드합니다.

GitLab에서는 GitLab에 특화된 수정으로 객체 저장소의 경우 임시 위치에서 최종 위치로 복사하는 것이 Rails가 CarrierWave를 속여 구현하였음을 알아두는 것이 유용합니다. CarrierWave가 업로드를 cache!할 때 CarrierWave::Storage::Fog::File을 반환하여 임시 파일을 가리키는 파일 핸들을 반환합니다. store! 단계에서 CarrierWave는 이 파일을 의도한 위치로 복사합니다.

테이블

스케일러빌리티::프레임워크 팀은 객체 저장소와 업로드를 더 쉽고 견고하게 만들고 있습니다. 업로더를 추가하거나 변경하는 경우, 해당 테이블을 업데이트해 주시면 도움이 됩니다. 이를 통해 업로더가 어디에서 어떻게 사용되는지 파악하는 데 도움이 됩니다.

기능 버킷 세부 정보

기능 업로드 기술 업로더 버킷 구조
Job artifacts 직접 업로드 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 해당 없음 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>
가져오기 직접 업로드 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 diffs 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>/
보안 파일 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