업로드 가이드: 새로운 업로드 추가
권장 사항
- 업로더를 만들 때,
AttachmentUploader
의 서브클래스로 만들어보세요 - 이 문서의 테이블에 업로더를 추가하세요
- 새로운 오브젝트 스토리지 버킷을 추가하지 마세요
- 직접 업로드 구현하기
- 업로드를 처리할 필요가 있다면, 어디서 처리할지 결정하세요
배경 정보
파일을 어디에 저장해야 하나요?
CarrierWave 업로더는 파일이 저장되는 위치를 결정합니다. 새로운 업로더 클래스를 만들 때는 새로운 기능의 파일을 저장할 위치를 결정하게 됩니다.
우선, 새로운 업로더 클래스가 필요한지 스스로에게 물어보세요. 동일한 업로더 클래스를 서로 다른 마운트 지점이나 모델에 사용하는 것은 괜찮습니다.
만약 새로운 업로더 클래스가 필요하다면, 반드시 이를 AttachmentUploader
의 서브클래스로 만들어야 합니다. 그러면 해당 클래스로부터 저장 위치 및 디렉토리 계획을 상속받습니다. 디렉토리 계획은 다음과 같습니다:
File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
GitLab 코드베이스를 살펴보면, 자체 저장 위치를 갖는 여러 업로더를 찾을 수 있습니다. 객체 저장을 위한 경우, 이는 업로더가 자체 버킷을 갖는 것을 의미합니다. 이제 다음과 같은 이유로 새로운 버킷 추가를 권장하지 않습니다:
- 새 버킷을 추가하면 GDK, Omnibus GitLab, CNG에서 하위 변경 사항을 적용해야 하므로 개발 시간이 소요됩니다.
- 새로운 버킷 사용은 GitLab.com 인프라 변경을 필요로 하며, 이로 인해 새로운 기능의 롤아웃이 늦어집니다.
- 새 버킷 사용은 Self-Managed GitLab 설치에 대한 새로운 기능의 채택을 늦춥니다: 사용자들은 로컬 GitLab 관리자가 새 버킷을 구성할 때까지 새 기능을 사용할 수 없습니다.
기존 버킷을 사용함으로써 모든 이러한 추가 작업과 마찰을 피할 수 있습니다. AttachmentUploader
가 사용하는 Gitlab.config.uploads
저장 위치는 이미 구성되어 있음이 보장됩니다.
직접 업로드 지원 구현
아래에서 직접 업로드 지원을 구현하는 방법을 설명합니다.
직접 업로드를 사용하는 것이 항상 필요한 것은 아니지만 일반적으로 좋은 아이디어입니다. 기능에서 처리하는 업로드가 드물고 작은 경우를 제외하고는 직접 업로드를 구현하려고 할 것입니다. 드물고 더 작은 업로드를 다루는 기능의 예는 프로젝트 아바타입니다: 이들은 거의 변경되지 않으며 애플리케이션은 그들에 대해 엄격한 크기 제한을 부과합니다.
만약 기능이 드물고 작은 업로드가 아닌 경우, 직접 업로드 지원을 구현하지 않는다면 기술 부채를 안게 됩니다. 최소한, 나중에 간접 업로드 지원을 추가할 수 있는지 확인해야 합니다.
직접 업로드를 지원하려면 두 가지가 필요합니다:
- Rails에서 사전 승인 엔드포인트
- Workhorse 라우팅 규칙
Workhorse는 업로드를 저장할 위치를 모릅니다. 이를 알아내기 위해 사전 승인 요청을 수행합니다. 또한 사전 승인 요청을 수행하는 위치나 방법을 모릅니다. 이를 위해 라우팅 규칙이 필요합니다.
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가 사전 승인 요청을 수행하는지 수동으로 확인해야 합니다. 이는 라우팅 규칙에서 실수를 하면 강력한 실패를 받지 않고 덜 효율적인 기본 경로를 사용하게 됩니다.
사전 승인 엔드포인트 추가
우리는 Rails 컨트롤러, Grape API 엔드포인트 및 GraphQL 리소스를 구별합니다.
먼저 안타까운 소식부터 전합니다: 현재는 GraphQL에 대한 직접 업로드가 지원되지 않습니다. 그 이유는 Workhorse가 GraphQL 쿼리를 구문 분석하지 않기 때문입니다. 문제 #280819도 참조하세요. 대신 Grape을 통해 파일 업로드를 허용하는 것을 고려해보세요.
Grape 사전 인증 엔드포인트의 경우, /authorize
루트를 구현하는 기존 예제를 찾아보세요. 하나의 예는 POST :id/uploads/authorize
엔드포인트입니다. 이 특정 예는 FileUploader를 사용하는데, 이는 업로드가 해당 Uploader 클래스의 저장 위치(버킷)에 저장되어 있음을 의미합니다.
Rails 엔드포인트의 경우 WorkhorseAuthorization concern을 사용할 수 있습니다.
업로드 처리
일부 기능은 업로드를 처리해야 할 필요가 있습니다. 예를 들어 업로드된 파일에서 메타데이터를 추출해야 하는 경우가 있습니다. 이를 구현하는 여러 가지 방법이 있습니다. 주요 선택은 처리를 구현할 위치, 또는 “처리자는 누구인가”입니다.
처리자 | 직접 업로드 가능? | HTTP 요청 거부 가능? | 구현 |
---|---|---|---|
Sidekiq | 예 | 아니요 | 간단함 |
Workhorse | 예 | 예 | 복잡함 |
Rails | 아니요 | 예 | 쉬움 |
Rails에서의 처리는 매력적으로 보이지만, 직접 업로드를 사용할 수 없어 나중에 확장 문제로 이어질 수 있습니다. 그럴 경우 처리를 Workhorse에서 수행하도록 강요받습니다. 따라서 기능 요구 사항이 허용하는 경우, Sidekiq에서 처리하는 것은 복잡성과 확장 가능성 사이에 좋은 균형을 유지할 수 있습니다.
CarrierWave 업로더
GitLab은 업로드를 관리하기 위해 CarrierWave의 수정 버전을 사용합니다. 아래에서는 CarrierWave를 어떻게 사용하고 수정했는지에 대해 설명합니다.
CarrierWave의 핵심 개념은 업로더(Uploader) 클래스입니다. 업로더는 파일이 저장되는 위치를 정의하고, 옵션으로 유효성 검사 및 처리 로직을 포함할 수 있습니다. 업로더를 사용하려면 해당 업로더를 ActiveRecord 모델의 텍스트 열에 연결해야 합니다. 이를 “마운팅”이라고 하며, 해당 열을 마운트포인트(mountpoint)
라고 합니다. 예를 들어:
class Project < ApplicationRecord
mount_uploader :avatar, AttachmentUploader
end
이제 avatar
라는 이름의 아바타를 업로드하면, 프로젝트의 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/
는 업로더가 설정 (예: /var/opt/gitlab/gitlab-rails/uploads
), 모델 이름 (project
), 모델 ID (123
) 및 마운트 포인트 (avatar
) 등으로 선택했습니다.
업로더는 업로드의 개별 저장 디렉터리를 결정합니다. 모델의
마운트포인트
열에는 파일 이름이 포함됩니다.
CarrierWave는 모델의 getter 및 setter를 정의하여 마운트포인트
열에 직접 액세스하지 않습니다.
선택적 업로더 동작
업로드의 저장 디렉터리를 결정하는 것 외에도, CarrierWave 업로더는 콜백을 통해 여러 가지 다른 동작을 구현할 수 있습니다. 이러한 동작 중 일부는 GitLab에서 사용할 수 없습니다. 특히, 현재 CarrierWave의 version
메커니즘을 사용할 수 없습니다. 할 수 있는 일에는 다음이 포함됩니다:
- 파일 이름 유효성 검사
- 직접 업로드와 호환되지 않음: 예를 들어 이미지 크기 조정과 같은 파일 내용의 일회성 사전 처리
- 직접 업로드와 호환되지 않음: 휴식 중 암호화
CarrierWave의 이미지 크기 조정 및 암호화와 같은 사전 처리 동작은 업로드된 파일에 대한 로컬 액세스를 필요로 합니다. 이는 사전 처리 동작을 루비에서 업로드해야 한다는 것을 의미합니다. 이는 업로드를 루비에서 수행하지 않는 것에 반대되므로 직접 업로드와 다소 충돌합니다. 따라서 사전 처리 동작이 있는 Uploader로 직접 업로드를 사용하는 경우, 사전 처리 동작은 무음으로 건너뜁니다.
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 파일 라이프사이클
업로더에는 일반 스토리지와 캐시 스토리지 두 영역이 연결되어 있습니다. 각각에는 고유한 스토리지 엔진이 있습니다. 파일을 마운트 지점 세터(project.avatar = File.open('/tmp/tanuki.png')
)에 할당하면 캐시 스토리지로 파일을 부착하여 cache!
메소드를 통해 부착할 파일을 복사하거나 이동해야 합니다. 파일을 지속시키려면 반드시 store!
메소드를 호출해야 합니다. 이는 ActiveRecord 콜백을 통해 발생하거나 업로더 인스턴스에서 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 스토리지
가 달라지고 uploads.store
또는 ci_job_artifacts.file_store
와 같은 데이터베이스 열이 있는 이유입니다.
Workhorse를 통한 직접 업로드
Workhorse 직접 업로드는 많은 양의 루비 CPU 시간을 소비하지 않고도 대용량 업로드를 허용하는 메커니즘입니다. Workhorse는 Go로 작성되었으며 고루틴은 루비 스레드보다 훨씬 적은 리소스를 사용합니다.
직접 업로드는 다음과 같이 작동합니다.
- Workhorse가 사용자 업로드 요청을 받습니다.
- Workhorse가 레일즈와 사전 인증을 하고 임시 업로드 위치를 받습니다.
- Workhorse가 사용자의 요청에 파일 업로드를 임시 업로드 위치에 저장합니다.
- Workhorse가 요청을 레일즈로 전파합니다.
- 레일즈가 업로드된 파일을 원하는 위치로 원격 복사 연산을 수행합니다.
- 레일즈가 임시 업로드를 삭제합니다.
- 레일즈가 시간 초과될 경우를 대비하여 임시 업로드를 두 번째로 삭제합니다.
일반적으로 cache!
는 CarrierWave::SanitizedFile
인스턴스를 반환하고, 그런 다음 store!
는 Fog를 사용하여 해당 파일을 업로드합니다.
GitLab의 특정 수정사항이 적용된 객체 스토리지의 경우, 임시 위치에서 최종 위치로 복사하는 것은 레일즈가 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]>
|
Design management thumbnails | 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 컨트롤러
| /uploads/appearance/...
|
토픽 | 디스크 버퍼링
| rails 컨트롤러
| /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 Migration | 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 컨트롤러
| /terraform/<proj_id_hash>/<terraform_state_id>
|
Pages 컨텐츠 아카이브 | 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
| 예 |
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
|