- 권장 사항
- 배경 정보
- 파일을 어디에 저장해아 하는가?
- 직접 업로드 지원 구현
- 파일 업로드 처리
- CarrierWave 업로더
- CarrierWave에 대한 GitLab의 수정 사항
- 테이블
업로드 가이드: 새로운 업로드 추가
권장 사항
- 업로더를 생성할 때,
AttachmentUploader
의 하위 클래스로 만드세요 - 이 문서의 테이블에 업로더를 추가하세요
- 새로운 오브젝트 스토리지 버킷을 추가하지 마세요
- 직접 업로드를 구현하세요
- 업로드를 처리해야 한다면, 어디서 처리할지 결정하세요
배경 정보
파일을 어디에 저장해아 하는가?
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
저장 위치는 이미 구성되어 있다는 것이 보장됩니다.
직접 업로드 지원 구현
아래에서 Workhorse를 통한 직접 업로드를 구현하는 방법을 설명합니다.
직접 업로드를 사용하는 것이 항상 필요한 것은 아니지만 일반적으로 좋은 생각입니다. 기능에서 처리하는 업로드가 드물고 작은 경우가 아니라면 직접 업로드를 구현하는 것이 좋습니다. 드물고 용량이 작은 업로드를 다루는 경우를 제외하고는 직접 업로드 지원을 구현하지 않으면 기술적 부채를 안게 됩니다. 적어도 나중에 직접 업로드 지원을 추가할 수 있는지 확인해야 합니다.
직접 업로드를 지원하기 위해서는 두 가지가 필요합니다:
- Rails에서 사전 승인 엔드포인트
- Workhorse 라우팅 규칙
Workhorse는 업로드를 저장할 위치를 모릅니다. 이를 알기 위해 사전 승인 요청을 수행합니다. 또한 어디에서 사전 승인 요청을 만들지에 대해서도 모릅니다. 이에 대한 라우팅 규칙이 필요합니다.
Workhorse가 이전에는 별도의 프로젝트였습니다 가 아닙니다: 이제 두 단계를 별도의 Merge Request으로 나눌 필요가 없습니다. 사실, 한 번에 두 가지를 모두 처리하는 것이 더 쉬울 것입니다.
Workhorse 라우팅 규칙 추가
라우팅 규칙은 workhorse/internal/upstream/routes.go 에서 정의되어 있습니다. 이는 다음으로 이루어져 있습니다:
- HTTP 동사(보통 “POST” 또는 “PUT”)
- 경로 정규 표현식
- 업로드 유형: MIME multipart 또는 “전체 요청 본문”
- 선택적으로
Content-Type
과 같은 HTTP 헤더와 일치시킬 수도 있습니다
예시:
u.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader),
workhorse/upload_test.go
의 TestAcceleratedUpload
에 귀하의 라우팅 규칙에 대한 테스트를 추가해야 합니다.
또한 새로운 기능을 위해 업로드 요청을 수행할 때 Workhorse가 사전 승인 요청을 만드는지 매뉴얼으로 확인해야 합니다. Rails 액세스 로그를 확인하여 이를 확인할 수 있습니다. 이는 라우팅 규칙에 오류가 있다면 엄격한 실패가 아니라 효율적이지 않은 기본 경로를 사용하게 됨에 따라 중요합니다.
사전 승인 엔드포인트 추가
우리는 Rails 컨트롤러, Grape API 엔드포인트 및 GraphQL 리소스를 구분합니다.
제일 먼저 안좋은 소식에 대해 말씀 드리자면, 현재 GraphQL에 대한 직접 업로드는 지원되지 않습니다. 이는 Workhorse가 GraphQL 쿼리를 구문 분석하지 않기 때문입니다. 또한 이슈 #280819도 참조하세요. 그러므로 Grape를 통해 파일 업로드를 수락하는 것을 고려해보세요.
Grape의 사전 승인 엔드포인트의 경우, /authorize
라우트를 구현하는 기존 예제를 찾아보세요. 한 예는
POST :id/uploads/authorize
엔드포인트
입니다. 이 특정 예제에서는 FileUploader를 사용하고 있으므로 해당 업로드는 해당 업로더 클래스의 저장 위치(버킷)에 저장됩니다.
Rails 엔드포인트의 경우 WorkhorseAuthorization concern 를 사용할 수 있습니다.
파일 업로드 처리
일부 기능은 파일 업로드를 처리해야 할 때가 있습니다. 예를 들어 업로드된 파일로부터 메타데이터를 추출해야 할 수 있습니다. 이를 구현하는 여러 가지 방법이 있습니다. 주요 선택 사항은 처리를 어디에 구현할지 또는 “누가 처리자인지”에 대한 것입니다.
처리자 | 직접 업로드 가능? | HTTP 요청 거부 가능? | 구현 |
---|---|---|---|
Sidekiq | 예 | 아니요 | 직관적 |
Workhorse | 예 | 예 | 복잡 |
Rails | 아니요 | 예 | 간단 |
Rails에서 처리되는 것이 유혹적으로 보일 수 있지만, 직접 업로드를 사용할 수 없기 때문에 나중에 확장 문제로 이어질 수 있습니다. 따라서 해당 기능의 요구 사항이 허용하는 경우에는 Sidekiq에서 처리하는 것이 복잡성과 확장 가능성 사이에 좋은 균형을 유지하는 것이 좋습니다.
CarrierWave 업로더
GitLab은 업로드를 관리하기 위해 CarrierWave의 수정 버전을 사용합니다. 아래에서는 CarrierWave의 사용 방법과 GitLab에서 어떻게 수정하는지에 대해 설명합니다.
CarrierWave의 중심 개념은 Uploader 클래스입니다. 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
)을 사용하여 선택합니다.
Uploader는 업로드의 개별 저장 디렉터리를 결정합니다. 모델의
마운트 지점
열에 파일 이름이 포함됩니다.
CarrierWave는 또한 여러 콜백을 통해 업로드에 대한 저장 디렉터리를 결정하는 것 외에도 여러 다른 동작을 구현할 수 있습니다. 이러한 동작 중 일부는 GitLab에서 사용할 수 없습니다. 특히 현재 CarrierWave의 version
메커니즘을 사용할 수 없습니다. 수행할 수 있는 작업은 다음과 같습니다:
- 파일 이름 유효성 검사
- 직접 업로드와 호환되지 않음: 파일 내용의 일회성 사전 처리, 예를 들어 이미지 크기 조정
- 직접 업로드와 호환되지 않음: 정지 상태에서의 암호화
이미지 크기 조정 또는 암호화와 같은 CarrierWave 사전 처리 동작은 업로드된 파일에 로컬로 액세스해야 합니다. 이로 인해 해당 파일을 Ruby에서 업로드해야 합니다. 이는 바로 업로드를 Ruby에서 수행하지 않는 것이 핵심인 직접 업로드를 방해합니다. Uploader가 사전 처리 동작을 가진 상태에서 직접 업로드를 사용하는 경우에는 사전 처리 동작이 음성으로 무시됩니다.
CarrierWave 저장 엔진
CarrierWave에는 두 가지 저장 엔진이 있습니다.
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 파일 생명 주기
Uploader는 두 개의 저장 영역과 캐시 저장 공간과 관련이 있습니다. 각각에는 자체 저장 엔진이 있습니다. project.avatar = File.open('/tmp/tanuki.png')
와 같이 마운트 지점 setter에 파일을 할당하면 부작용으로 cache!
메서드를 통해 파일을 캐시 저장 공간으로 복사/이동해야 합니다. 파일을 지속적으로 유지하려면 store!
메서드를 호출해야 합니다. 이는 ActiveRecord 콜백을 통해 발생하거나 Uploader 인스턴스에서 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의 직접 업로드는 많은 Ruby CPU 시간을 소비하지 않고 큰 업로드를 수락할 수 있는 메커니즘입니다. Workhorse는 Go로 작성되었으며 고루틴의 리소스 풋프린트가 Ruby 스레드보다 훨씬 낮습니다.
직접 업로드는 다음과 같이 작동합니다.
- Workhorse가 사용자 업로드 요청을 수락합니다.
- Workhorse가 Rails로 사전 인증을 수행하고 임시 업로드 위치를 받습니다.
- Workhorse가 사용자의 요청에 파일 업로드를 임시 업로드 위치에 저장합니다.
- Workhorse가 요청을 Rails로 전파합니다.
- Rails가 업로드된 파일을 임시 위치에서 최종 위치로 복사하는 원격 복사 작업을 수행합니다.
- Rails가 임시 업로드를 삭제합니다.
- Rails가 시간 초과될 경우 Workhorse가 임시 업로드를 두 번째로 삭제합니다.
일반적으로 cache!
는 CarrierWave::SanitizedFile
의 인스턴스를 반환하고, 그런 다음 store!
는 Fog를 사용하여 해당 파일을 업로드합니다.
GitLab의 특정 수정을 통해 객체 리포지터리의 경우, 임시 위치에서 최종 위치로 복사하는 것은 Rails가 CarrierWave를 속이도록 구현됩니다. CarrierWave가 업로드를 cache!
할 때 임시 파일을 가리키는 CarrierWave::Storage::Fog::File
파일 핸들을 반환하고, store!
단계에서 CarrierWave가 이 파일을 의도한 위치로 복사합니다.
테이블
스케일러빌러티::프레임워크 팀은 오브젝트 저장과 업로드를 더 쉽고 견고하게 만들고 있습니다. 업로더를 추가하거나 변경하면 표를 업데이트해주시면 도움이 됩니다. 이를 통해 어디에 어떻게 업로더가 사용되는지 파악할 수 있습니다.
기능 버킷 세부 정보
기능 | 업로드 기술 | 업로더 | 버킷 구조 |
---|---|---|---|
작업 아티팩트 | 직접 업로드
| 워크호스
| /artifacts/<proj_id_hash>/<date>/<job_id>/<artifact_id>
|
파이프라인 아티팩트 | 캐리어웨이브
| 사이드킥
| /artifacts/<proj_id_hash>/pipelines/<pipeline_id>/artifacts/<artifact_id>
|
라이브 작업 트레이스 | 포그
| 사이드킥
| /artifacts/tmp/builds/<job_id>/chunks/<chunk_index>.log
|
작업 트레이스 아카이브 | 캐리어웨이브
| 사이드킥
| /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 | 직접 업로드
| 워크호스
| /lfs-objects/<lfs_obj_oid[0:2]>/<lfs_obj_oid[2:2]>
|
디자인 관리 썸네일 | 캐리어웨이브
| 사이드킥
| /uploads/design_management/action/image_v432x230/<model_id>/<original_lfs_obj_oid[2:2]
|
일반 파일 업로드 | 직접 업로드
| 워크호스
| /uploads/@hashed/[0:2]/[2:4]/<hash1>/<hash2>/file
|
일반 파일 업로드 - 개인 스니펫 | 직접 업로드
| 워크호스
| /uploads/personal_snippet/<snippet_id>/<filename>
|
글로벌 외관 설정 | 디스크 버퍼링
| Rails 컨트롤러
| /uploads/appearance/...
|
토픽스 | 디스크 버퍼링
| Rails 컨트롤러
| /uploads/projects/topic/...
|
아바타 이미지 | 직접 업로드
| 워크호스
| /uploads/[user,group,project]/avatar/<model_id>
|
가져오기 | 직접 업로드
| 워크호스
| /uploads/import_export_upload/import_file/<model_id>/<file_name>
|
내보내기 | 캐리어웨이브
| 사이드킥
| /uploads/import_export_upload/export_file/<model_id>/<timestamp>_<namespace>-<project_name>_export.tag.gz
|
GitLab 마이그레이션 | 캐리어웨이브
| 사이드킥
| /uploads/bulk_imports/???
|
MR 차이 | 캐리어웨이브
| 사이드킥
| /external-diffs/merge_request_diffs/mr-<mr_id>/diff-<diff_id>
|
패키지 관리자 자산 (NPM 제외) | 직접 업로드
| 워크호스
| /packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id>
|
NPM 패키지 관리자 자산 | 캐리어웨이브
| grape API
| /packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id>
|
데비안 패키지 관리자 자산 | 직접 업로드
| 워크호스
| /packages/<group_id or project_id_hash>/debian_*/<group_id or project_id or distribution_file_id>
|
의존성 프록시 캐시 | send_dependency
| 워크호스
| /dependency-proxy/<group_id_hash>/dependency_proxy/<group_id>/files/<blob_id or manifest_id>
|
Terraform 상태 파일 | 캐리어웨이브
| Rails 컨트롤러
| /terraform/<proj_id_hash>/<terraform_state_id>
|
페이지 콘텐츠 아카이브 | 캐리어웨이브
| 사이드킥
| /gitlab-gprd-pages/<proj_id_hash>/pages_deployments/<deployment_id>/
|
보안 파일 | 캐리어웨이브
| 사이드킥
| /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
|