업로드 가이드: 새로운 업로드 추가
권장 사항
- 업로더를 생성할 때,
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
저장 위치는 이미 구성되었음을 보장합니다.
직접 업로드 지원 구현하기
아래에서는 직접 업로드 지원 구현 방법을 설명합니다.
직접 업로드를 사용하는 것이 항상 필요한 것은 아니지만, 보통 좋은 아이디어입니다. 기능에서 처리하는 업로드가 둘 다 드물고 작지 않은 경우, 직접 업로드를 구현하고 싶을 것입니다. 작은 업로드가 드물게 발생하는 기능의 예로는 프로젝트 아바타가 있습니다: 이는 거의 변경되지 않으며 애플리케이션이 엄격한 크기 제한을 부과합니다.
기능이 드물고 작지 않은 업로드를 처리하는 경우, 직접 업로드 지원을 구현하지 않는 것은 기술적 부채를 지는 것입니다. 최소한, 나중에 직접 업로드 지원을 추가할 수 있음을 확실히 해야 합니다.
직접 업로드를 지원하기 위해 두 가지가 필요합니다:
- Rails의 사전 인가 엔드포인트
- 워크호스 라우팅 규칙
워크호스는 업로드를 저장할 위치를 모릅니다. 이를 찾기 위해 사전 인가 요청을 수행합니다. 또한, 사전 인가 요청을 만들지 말아야 할지 모릅니다. 이를 위해 라우팅 규칙이 필요합니다.
여기서 기억하는 사람들을 위해, 워크호스는 별도의 프로젝트였던 적이 있습니다: 이제는 이 두 단계를 별도의 병합 요청으로 나눌 필요가 없습니다. 사실, 두 가지를 하나의 병합 요청으로 처리하는 것이 아마 더 쉬울 것입니다.
워크호스 라우팅 규칙 추가하기
라우팅 규칙은 workhorse/internal/upstream/routes.go에서 정의됩니다.
이는 다음으로 구성됩니다:
-
HTTP 동사 (일반적으로 “POST” 또는 “PUT”)
-
경로 정규 표현식
-
업로드 유형: MIME 멀티파트 또는 “전체 요청 본문”
-
선택적으로,
Content-Type
과 같은 HTTP 헤더에서 일치할 수 있습니다
예시:
u.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader),
라우팅 규칙에 대한 테스트를 TestAcceleratedUpload라는 곳에 추가해야 합니다.
또한, 새로운 기능에 대한 업로드 요청을 수행할 때 Workhorse가 사전 승인 요청을 생성하는지 수동으로 확인해야 합니다. 이는 Rails 액세스 로그를 확인하여 확인할 수 있습니다. 라우팅 규칙에 실수가 발생하면 하드 실패가 발생하지 않기 때문에 필요합니다: 덜 효율적인 기본 경로를 사용하게 될 뿐입니다.
사전 승인 엔드포인트 추가하기
우리는 세 가지 경우를 구분합니다: Rails 컨트롤러, Grape API 엔드포인트, GraphQL 리소스.
안타까운 소식부터 시작하자면: GraphQL에 대한 직접 업로드는 현재 지원되지 않습니다. 그 이유는 Workhorse가 GraphQL 쿼리를 파싱하지 않기 때문입니다. 또한 이슈 #280819도 참조하세요. 대신 Grape를 통해 파일 업로드를 수락하는 것을 고려하세요.
Grape 사전 승인 엔드포인트의 경우, /authorize
경로를 구현하는 기존 예제를 찾아보세요. 하나의 예시는
POST :id/uploads/authorize
엔드포인트입니다. 이 특정 예시는 FileUploader를 사용하고 있으며, 이는 업로드가 해당 Uploader 클래스의 저장 위치(버킷)에 저장된다는 것을 의미합니다.
Rails 엔드포인트의 경우, WorkhorseAuthorization concern을 사용할 수 있습니다.
업로드 처리하기
일부 기능은 업로드를 처리해야 합니다. 예를 들어, 업로드된 파일에서 메타데이터를 추출하는 등의 작업입니다. 이를 구현하는 방법은 몇 가지가 있습니다. 주요 선택지는 처리하는 위치 또는 “누가 프로세서인지”입니다.
Processor | 직접 업로드 가능? | HTTP 요청 거부 가능? | 구현 |
---|---|---|---|
Sidekiq | 예 | 아니오 | 간단함 |
Workhorse | 예 | 예 | 복잡함 |
Rails | 아니오 | 예 | 쉬움 |
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/
디렉토리는 Uploader가
구성(예: /var/opt/gitlab/gitlab-rails/uploads
), 모델 이름(project
), 모델 ID(123
) 및 마운트 지점(avatar
) 등을 통해 선택합니다.
Uploader는 업로드의 개별 저장 디렉토리를 결정합니다. 모델의
mountpoint
열에 파일 이름이 포함되어 있습니다.
CarrierWave는 파일 핸들 객체에서 작동하는 getter 및 setter를 모델에 정의하므로, mountpoint
열에 직접 접근하지 않습니다.
선택적 Uploader 동작
업로드를 위한 저장 디렉터리를 결정하는 것 외에도,
CarrierWave Uploader는 콜백을 통해 여러 다른 동작을 구현할 수 있습니다.
이러한 모든 동작이 GitLab에서 사용 가능한 것은 아닙니다.
특히, 현재 CarrierWave의 version
메커니즘을 사용할 수 없습니다.
할 수 있는 작업은 다음과 같습니다:
- 파일 이름 유효성 검사
- 직접 업로드와 호환되지 않음: 파일 내용의 일회성 전처리, 예를 들면 이미지 크기 조정
- 직접 업로드와 호환되지 않음: 데이터 암호화
CarrierWave의 이미지 크기 조정 또는 암호화와 같은 전처리 동작은
업로드된 파일에 대한 로컬 액세스를 요구합니다.
이로 인해 Ruby에서 처리된 파일을 업로드해야 합니다.
이것은 Ruby에서 업로드를 하지 않는 것을 목표로 하는 직접 업로드의
취지에 반합니다.
전처리 동작이 있는 Uploader와 함께 직접 업로드를 사용하면
전처리 동작이 조용히 건너뛰어집니다.
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 파일 생명주기
Uploader는 두 개의 저장 영역과 연결됩니다: 일반 저장소와
캐시 저장소. 각 저장소는 고유한 저장 엔진을 가지고 있습니다.
마운트 포인트 설정자(project.avatar = File.open('/tmp/tanuki.png')
)에
파일을 할당하면, cache!
메서드를 통해 파일을 캐시
저장소로 복사/이동해야 합니다.
파일을 유지하려면 store!
메서드를 호출해야 합니다.
이는 ActiveRecord 콜백
을 통해 수행되거나, Uploader 인스턴스에서 store!
를 호출하여 수행됩니다.
일반적으로 cache!
와 store!
와 상호작용할 필요는 없지만,
GitLab CarrierWave 수정 사항을 디버그할 필요가 있다면,
그것들이 존재하고 항상 호출된다는 것을 아는 것이 유용합니다.
특히 CarrierWave 전처리 동작(process
등)은 before :cache
후크로
구현되어 있으며, 직접 업로드의 경우 이러한 후크는 무시되고 실행되지 않는다는 점이 중요합니다.
직접 업로드는 모든 CarrierWave
before :cache
후크를 건너뜁니다.
GitLab의 CarrierWave 수정 사항
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로 작성되었으며, goroutines는 Ruby 스레드보다 훨씬 적은 리소스를 차지합니다.
직접 업로드는 다음과 같이 작동합니다.
- Workhorse는 사용자 업로드 요청을 수락합니다.
- Workhorse는 Rails와 요청을 미리 인증하고 임시 업로드 위치를 수신합니다.
- Workhorse는 사용자의 요청에서 파일 업로드를 임시 업로드 위치에 저장합니다.
- Workhorse는 Rails에 요청을 전달합니다.
- Rails는 업로드된 파일을 임시 위치에서 최종 위치로 복사하는 원격 복사 작업을 실행합니다.
- Rails는 임시 업로드를 삭제합니다.
- Workhorse는 Rails의 타임아웃을 대비하여 임시 업로드를 두 번째로 삭제합니다.
일반적으로 cache!
는 CarrierWave::SanitizedFile
의 인스턴스를 반환하며, store!
는 Fog를 사용하여 해당 파일을 업로드합니다.
객체 저장소의 경우, GitLab에 특정한 수정 사항으로 인해, 임시 위치에서 최종 위치로의 복사는 Rails가 CarrierWave를 속이는 방식으로 구현됩니다. CarrierWave가 업로드를 cache!
하려고 할 때, 우리는 반환하는 CarrierWave::Storage::Fog::File
파일 핸들이 임시 파일을 가리킵니다. store!
단계에서 CarrierWave는 이 파일을 복사합니다.
테이블
Scalability::Frameworks 팀은 객체 저장소와 업로드를 보다 쉽게 사용하고 더욱 견고하게 만들고 있습니다. 업로더를 추가하거나 변경하는 경우, 이 테이블도 업데이트해 주시면 도움이 됩니다. 이는 업로더가 어디에서 어떻게 사용되는지를 파악하는 데 도움이 됩니다.
기능 버킷 세부사항
기능 | 업로드 기술 | 업로더 | 버킷 구조 |
---|---|---|---|
Job artifacts | direct upload |
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/??? |
Backups | 적용되지 않음 |
s3cmd , awscli , or gcs
|
/gitlab-backups/??? |
Git LFS | direct upload |
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] |
Generic file uploads | direct upload |
workhorse |
/uploads/@hashed/[0:2]/[2:4]/<hash1>/<hash2>/file |
Generic file uploads - personal snippets | direct upload |
workhorse |
/uploads/personal_snippet/<snippet_id>/<filename> |
Global appearance settings | disk buffering |
rails controller |
/uploads/appearance/... |
Topics | disk buffering |
rails controller |
/uploads/projects/topic/... |
Avatar images | direct upload |
workhorse |
/uploads/[user,group,project]/avatar/<model_id> |
Import | direct upload |
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> |
Package manager assets (except for NPM) | direct upload |
workhorse |
/packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id> |
NPM Package manager assets | carrierwave |
grape API |
/packages/<proj_id_hash>/packages/<package_id>/files/<package_file_id> |
Debian Package manager assets | direct upload |
workhorse |
/packages/<group_id or project_id_hash>/debian_*/<group_id or project_id or distribution_file_id> |
Dependency Proxy cache | send_dependency |
workhorse |
/dependency-proxy/<group_id_hash>/dependency_proxy/<group_id>/files/<blob_id or manifest_id> |
Terraform state files | carrierwave |
rails controller |
/terraform/<proj_id_hash>/<terraform_state_id> |
Pages content archives | 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 |