1 Active Storage란 무엇인가?
Active Storage는 Amazon S3, Google Cloud Storage 또는 Microsoft Azure Storage와 같은 클라우드 스토리지 서비스에 파일을 업로드하고 이러한 파일을 Active Record 객체에 첨부하는 것을 용이하게 합니다. 개발 및 테스트를 위한 로컬 디스크 기반 서비스가 함께 제공되며, 백업 및 마이그레이션을 위해 하위 서비스에 파일을 미러링하는 것을 지원합니다.
Active Storage를 사용하면, 애플리케이션에서 이미지 업로드를 변환하거나 PDF 및 비디오와 같은 비이미지 업로드의 이미지 표현을 생성할 수 있으며, 임의의 파일에서 메타데이터를 추출할 수 있습니다.
1.1 Requirements
Active Storage의 다양한 기능은 Rails가 설치하지 않는 서드파티 소프트웨어에 의존하며, 별도로 설치해야 합니다:
- 이미지 분석과 변환을 위한 libvips v8.6+ 또는 ImageMagick
- 비디오 미리보기를 위한 ffmpeg v3.4+ 및 비디오/오디오 분석을 위한 ffprobe
- PDF 미리보기를 위한 poppler 또는 muPDF
이미지 분석과 변환을 위해서는 image_processing
gem도 필요합니다. Gemfile
에서 주석을 해제하거나, 필요한 경우 추가하세요:
gem "image_processing", ">= 1.2"
ImageMagick과 비교했을 때, libvips는 덜 알려져 있고 사용 가능한 범위가 좁습니다. 하지만 libvips는 최대 10배 더 빠르고 메모리를 1/10만큼 소비합니다. JPEG 파일의 경우, libjpeg-dev
를 libjpeg-turbo-dev
로 교체하면 2-7배 더 빠른 성능을 얻을 수 있습니다.
써드파티 소프트웨어를 설치하고 사용하기 전에, 라이선스 관련 영향을 반드시 이해해야 합니다. 특히 MuPDF는 AGPL 라이선스이며 일부 사용의 경우 상업용 라이선스가 필요합니다.
2 Setup
$ bin/rails active_storage:install
$ bin/rails db:migrate
이는 설정을 구성하고, Active Storage가 사용하는 세 개의 테이블을 생성합니다:
active_storage_blobs
, active_storage_attachments
, active_storage_variant_records
.
테이블 | 목적 |
---|---|
active_storage_blobs |
파일명과 콘텐츠 타입과 같은 업로드된 파일에 대한 데이터를 저장합니다. |
active_storage_attachments |
모델을 blob에 연결하는 다형성 조인 테이블입니다. 모델의 클래스 이름이 변경되면, 이 테이블의 record_type 을 모델의 새로운 클래스 이름으로 업데이트하는 마이그레이션을 실행해야 합니다. |
active_storage_variant_records |
variant tracking이 활성화된 경우, 생성된 각 variant에 대한 레코드를 저장합니다. |
모델의 기본 키로 integer 대신 UUID를 사용하는 경우, 설정 파일에 Rails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid }
를 설정해야 합니다.
config/storage.yml
에서 Active Storage 서비스를 선언하세요. 애플리케이션이 사용하는 각 서비스에 대해 이름과 필요한 설정을 제공하세요. 아래 예시는 local
, test
, amazon
이라는 세 개의 서비스를 선언합니다:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
# AWS 시크릿을 설정하려면 bin/rails credentials:edit를 사용하세요 (aws:access_key_id|secret_access_key 형태로)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
bucket: your_own_bucket-<%= Rails.env %>
region: "" # 예: 'us-east-1'
Active Storage가 어떤 service를 사용할지 Rails.application.config.active_storage.service
를 설정하여 지정하세요. 각 environment마다 서로 다른 service를 사용할 가능성이 높기 때문에, environment별로 설정하는 것을 권장합니다. 이전 예시의 disk service를 development environment에서 사용하려면, config/environments/development.rb
에 다음과 같이 추가하면 됩니다:
# 파일을 로컬에 저장합니다.
config.active_storage.service = :local
프로덕션에서 S3 서비스를 사용하기 위해서는 config/environments/production.rb
에 다음을 추가합니다:
# Amazon S3에 파일 저장
config.active_storage.service = :amazon
테스트시 test service를 사용하려면 config/environments/test.rb
에 다음을 추가하세요:
# 업로드된 파일을 로컬 파일시스템의 임시 디렉토리에 저장합니다.
config.active_storage.service = :test
환경별 configuration 파일이 우선됩니다:
예를 들어 production 환경에서는 config/storage/production.yml
파일이 (존재하는 경우) config/storage.yml
파일보다 우선적으로 적용됩니다.
production 데이터를 실수로 삭제하는 위험을 더욱 줄이기 위해 bucket 이름에 Rails.env
를 사용하는 것이 권장됩니다.
amazon:
service: S3
# ...
bucket: your_own_bucket-<%= Rails.env %>
google:
service: GCS
# ...
bucket: your_own_bucket-<%= Rails.env %>
azure:
service: AzureStorage
# ...
container: your_container_name-<%= Rails.env %>
내장 service adapter(예: Disk
와 S3
)와 필요한 설정에 대한 자세한 정보는 계속해서 읽어주세요.
2.1 Disk Service
config/storage.yml
에서 Disk service를 선언하세요:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
로컬 디스크에 파일을 저장합니다. root 경로는 애플리케이션의 storage/ 디렉토리로 설정됩니다.
2.2 S3 Service (Amazon S3 및 S3 호환 API)
Amazon S3에 연결하려면 config/storage.yml
에 S3 service를 다음과 같이 선언하세요:
# AWS secrets를 설정하려면 bin/rails credentials:edit를 사용하세요 (aws:access_key_id|secret_access_key 형식으로)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # 예시: 'us-east-1'
bucket: your_own_bucket-<%= Rails.env %>
클라이언트와 업로드 옵션을 선택적으로 제공:
# AWS secrets를 설정하려면 bin/rails credentials:edit를 사용하세요 (aws:access_key_id|secret_access_key 형식으로)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # 예) 'us-east-1'
bucket: your_own_bucket-<%= Rails.env %>
http_open_timeout: 0
http_read_timeout: 0
retry_limit: 0
upload:
server_side_encryption: "" # 'aws:kms' 또는 'AES256'
cache_control: "private, max-age=<%= 1.day.to_i %>"
애플리케이션에 적절한 client HTTP timeout과 retry 제한을 설정하세요. 특정 실패 시나리오에서는 기본 AWS client 설정으로 인해 연결이 최대 몇 분 동안 유지되고 요청 대기열이 발생할 수 있습니다.
Gemfile
에 aws-sdk-s3
gem을 추가하세요:
gem "aws-sdk-s3", require: false
원하는 경우에만 이 gem을 require 할 수 있도록 하려면 require: false
를 사용하세요. 예를 들어 프로덕션 환경에서만 실제로 AWS S3를 사용하는 경우에 유용합니다.
Active Storage의 핵심 기능은 다음 권한이 필요합니다: s3:ListBucket
, s3:PutObject
, s3:GetObject
, 그리고 s3:DeleteObject
. Public access는 추가로 s3:PutObjectAcl
이 필요합니다. ACL 설정과 같은 추가 업로드 옵션을 구성한 경우 추가 권한이 필요할 수 있습니다.
환경 변수, 표준 SDK 설정 파일, 프로필, IAM 인스턴스 프로필 또는 작업 역할을 사용하려는 경우, 위의 예시에서 access_key_id
, secret_access_key
, region
키를 생략할 수 있습니다. S3 Service는 AWS SDK documentation에 설명된 모든 인증 옵션을 지원합니다.
DigitalOcean Spaces와 같은 S3 호환 object storage API에 연결하려면 endpoint
를 제공하세요:
digitalocean:
service: S3
endpoint: https://nyc3.digitaloceanspaces.com
access_key_id: <%= Rails.application.credentials.dig(:digitalocean, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:digitalocean, :secret_access_key) %>
# ...그리고 다른 옵션들
AWS S3 Client의 많은 다른 옵션들이 있습니다. AWS S3 Client 문서에서 확인하실 수 있습니다.
2.3 Microsoft Azure Storage Service
config/storage.yml
에서 Azure Storage 서비스를 선언하세요:
azure:
service: AzureStorage
storage_account_name: your_account_name
storage_access_key: your_access_key
container: your_container_name
azure_test:
service: AzureStorage
storage_account_name: your_account_name
storage_access_key: your_access_key
container: your_container_name_test
# bin/rails credentials:edit를 사용해서 Azure Storage secret을 설정하세요 (azure_storage:storage_access_key로)
azure:
service: AzureStorage
storage_account_name: your_account_name
storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
container: your_container_name-<%= Rails.env %>
Gemfile
에 azure-storage-blob
gem을 추가하세요:
gem "azure-storage-blob", "~> 2.0", require: false
2.4 Google Cloud Storage Service
config/storage.yml
에서 Google Cloud Storage 서비스를 선언합니다:
google:
service: GCS
project: your-project-id
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
bucket: your-bucket-name
google:
service: GCS
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
project: ""
bucket: 자신의_버킷명-<%= Rails.env %>
keyfile 경로 대신 credentials의 Hash를 선택적으로 제공할 수 있습니다:
# GCS secrets(gcs:private_key_id|private_key)를 설정하려면 bin/rails credentials:edit를 사용하세요
google:
service: GCS
credentials:
type: "service_account"
project_id: ""
private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
client_email: ""
client_id: ""
auth_uri: "https://accounts.google.com/o/oauth2/auth"
token_uri: "https://accounts.google.com/o/oauth2/token"
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url: ""
project: ""
bucket: your_own_bucket-<%= Rails.env %>
업로드된 asset에 설정할 Cache-Control 메타데이터를 선택적으로 제공할 수 있습니다:
google:
service: GCS
...
cache_control: "public, max-age=3600"
URL 서명 시 credentials
대신 선택적으로 IAM을 사용할 수 있습니다. GKE 애플리케이션을 Workload Identity로 인증하는 경우 유용합니다. 자세한 내용은 이 Google Cloud 블로그 포스트를 참조하세요.
google:
service: GCS
...
iam: true
URL 서명 시 특정 GSA를 선택적으로 사용할 수 있습니다. IAM을 사용할 때는 metadata server에 접속하여 GSA 이메일을 가져오지만, 이 metadata server가 항상 존재하는 것은 아니며(예: 로컬 테스트) 기본이 아닌 GSA를 사용하고 싶을 수 있습니다.
google:
service: GCS
...
iam: true
gsa_email: "foobar@baz.iam.gserviceaccount.com"
Gemfile
에 google-cloud-storage
gem을 추가하세요:
gem "google-cloud-storage", "~> 1.11", require: false
2.5 Mirror Service
여러 서비스를 mirror service를 정의하여 동기화 상태로 유지할 수 있습니다. mirror service는 두 개 이상의 하위 서비스 간에 업로드와 삭제를 복제합니다.
mirror service는 프로덕션 환경에서 서비스 간 마이그레이션을 하는 동안 임시로 사용하기 위한 것입니다. 새로운 서비스로 미러링을 시작하고, 기존 서비스의 파일들을 새로운 서비스로 복사한 다음, 새로운 서비스로 완전히 전환할 수 있습니다.
미러링은 atomic하지 않습니다. 기본 서비스에서는 업로드가 성공했지만 하위 서비스 중 하나에서 실패할 수 있습니다. 새로운 서비스로 완전히 전환하기 전에 모든 파일이 복사되었는지 확인하세요.
위에서 설명한 대로 미러링하고 싶은 각 서비스를 정의하세요. mirror service를 정의할 때 이름으로 참조하세요:
# AWS 시크릿(aws:access_key_id|secret_access_key)을 설정하려면 bin/rails credentials:edit를 사용하세요
s3_west_coast:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # 예: 'us-west-1'
bucket: your_own_bucket-<%= Rails.env %>
s3_east_coast:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # 예: 'us-east-1'
bucket: your_own_bucket-<%= Rails.env %>
production:
service: Mirror
primary: s3_east_coast
mirrors:
- s3_west_coast
모든 secondary service가 업로드를 받긴 하지만, 다운로드는 항상 primary service에서 처리됩니다.
Mirror service는 direct upload와 호환됩니다. 새로운 파일들은 primary service에 직접 업로드됩니다. 직접 업로드된 파일이 record에 첨부되면, 이를 secondary service로 복사하는 background job이 대기열에 추가됩니다.
2.6 Public access
기본적으로 Active Storage는 서비스에 대해 private access를 가정합니다. 이는 blob에 대해 signed, single-use URL을 생성한다는 의미입니다. blob을 공개적으로 액세스할 수 있도록 하려면 앱의 config/storage.yml
에서 public: true
를 지정하십시오:
gcs: &gcs
service: GCS
project: ""
private_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/private_key.json") %>
bucket: your_own_bucket-<%= Rails.env %>
public_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/public_key.json") %>
bucket: your_own_bucket-<%= Rails.env %>
public: true
위 코드는 GCS(Google Cloud Storage) 설정을 위한 YAML 구성 파일입니다. private_gcs와 public_gcs 두 환경에 대한 설정을 정의하며, 각각 개인 키와 공개 키를 사용하고 버킷 이름에는 Rails 환경이 접미사로 붙습니다. public_gcs의 경우 public 속성이 true로 설정되어 있습니다.
bucket이 public access를 위해 적절하게 구성되어 있는지 확인하세요. Amazon S3, Google Cloud Storage, Microsoft Azure 스토리지 서비스에서 public read 권한을 활성화하는 방법은 문서를 참고하세요. Amazon S3의 경우 추가로 s3:PutObjectAcl
권한이 필요합니다.
기존 애플리케이션을 public: true
를 사용하도록 변환할 때는, 전환하기 전에 bucket의 모든 개별 파일이 publicly-readable하도록 업데이트되어 있는지 확인하세요.
3 파일을 레코드에 첨부하기
3.1 has_one_attached
has_one_attached
매크로는 레코드와 파일 간의 일대일 매핑을 설정합니다. 각 레코드는 하나의 파일을 첨부할 수 있습니다.
예를 들어, 애플리케이션에 User
모델이 있다고 가정해봅시다. 각 사용자가 avatar를 가질 수 있도록 하려면 User
모델을 다음과 같이 정의하세요:
class User < ApplicationRecord
has_one_attached :avatar
end
Rails 6.0 이상을 사용하는 경우, 다음과 같이 model generator 명령을 실행할 수 있습니다:
$ bin/rails generate model User avatar:attachment
사용자를 avatar와 함께 생성할 수 있습니다:
<%= form.file_field :avatar %>
class SignupController < ApplicationController
def create
user = User.create!(user_params)
session[:user_id] = user.id
redirect_to root_path
end
private
def user_params
params.expect(user: [:email_address, :password, :avatar])
end
end
기존 사용자에게 avatar를 첨부하기 위해 avatar.attach
를 호출하세요:
user.avatar.attach(params[:avatar])
사용자의 avatar를 첨부합니다.
특정 사용자가 avatar를 가지고 있는지 확인하려면 avatar.attached?
를 호출하세요:
user.avatar.attached?
경우에 따라 특정 attachment에 대한 기본 service를 오버라이드하고 싶을 수 있습니다.
service
옵션에 service 이름을 지정하여 attachment별로 특정 service를 설정할 수 있습니다:
class User < ApplicationRecord
has_one_attached :avatar, service: :google
end
service: 옵션을 통해 model 레벨에서 특정 파일이 어떤 service를 사용할지 선언할 수 있습니다.
첨부된 파일에 대한 특정 variant를 설정하려면, 반환된 attachable 객체에서 variant
메소드를 호출하면 됩니다:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
썸네일 variant를 얻으려면 avatar.variant(:thumb)
를 호출하세요:
<%= image_tag user.avatar.variant(:thumb) %>
preview에 대해 특정 variant를 사용할 수도 있습니다:
class User < ApplicationRecord
has_one_attached :video do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
<%= image_tag user.video.preview(:thumb) %>
여러분의 variant들이 접근될 것을 미리 알고 있다면, Rails가 미리 생성하도록 지정할 수 있습니다:
class User < ApplicationRecord
has_one_attached :video do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100], preprocessed: true
end
end
Rails는 attachment가 레코드에 첨부된 후 variant를 생성하기 위한 job을 enqueue합니다.
Active Storage는 polymorphic associations에 의존하고, polymorphic associations는 데이터베이스에 클래스 이름을 저장하는 것에 의존하기 때문에, 해당 데이터는 Ruby 코드에서 사용되는 클래스 이름과 동기화된 상태를 유지해야 합니다. has_one_attached
를 사용하는 클래스의 이름을 변경할 때는, 해당 행의 active_storage_attachments.record_type
polymorphic type 컬럼에 있는 클래스 이름도 함께 업데이트해야 합니다.
3.2 has_many_attached
has_many_attached
매크로는 레코드와 파일 간의 일대다 관계를 설정합니다. 각 레코드는 여러 개의 파일을 첨부할 수 있습니다.
예를 들어, 애플리케이션에 Message
모델이 있다고 가정해봅시다. 각 메시지가 여러 개의 이미지를 가질 수 있도록 하려면 Message
모델을 다음과 같이 정의하세요:
class Message < ApplicationRecord
has_many_attached :images
end
또는 Rails 6.0 이상을 사용하는 경우, 다음과 같이 model 생성기 명령을 실행할 수 있습니다:
$ bin/rails generate model Message images:attachments
이미지가 포함된 메시지를 생성할 수 있습니다:
class MessagesController < ApplicationController
def create
message = Message.create!(message_params)
redirect_to message
end
private
def message_params
params.expect(message: [ :title, :content, images: [] ])
end
end
기존 message에 새 image를 추가하려면 images.attach
를 호출하세요:
@message.images.attach(params[:images])
images.attached?
를 호출하여 특정 메시지에 이미지가 있는지 확인합니다:
@message.images.attached?
image attachment가 있는지 확인합니다.
service
옵션을 사용해서 has_one_attached
와 동일한 방법으로 기본 service를 오버라이드할 수 있습니다:
class Message < ApplicationRecord
has_many_attached :images, service: :s3
end
사진들을 S3에 저장하는 데에만 사용되는 Message model을 만들고 싶다면, service 옵션을 지정하여 :s3를 사용할 수 있습니다.
특정 variant를 구성하는 것은 has_one_attached
와 동일한 방식으로, yielded attachable 객체에 대해 variant
메서드를 호출하여 수행됩니다:
class Message < ApplicationRecord
has_many_attached :images do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
Active Storage는 polymorphic associations에 의존하며, polymorphic associations는 데이터베이스에 클래스 이름을 저장하는 것에 의존하므로, 해당 데이터는 Ruby 코드에서 사용되는 클래스 이름과 동기화된 상태를 유지해야 합니다. has_many_attached
를 사용하는 클래스의 이름을 변경할 때는, 해당 행의 active_storage_attachments.record_type
polymorphic type 컬럼의 클래스 이름도 함께 업데이트해야 합니다.
3.3 File/IO Objects 첨부하기
가끔 HTTP request를 통해 도착하지 않은 파일을 첨부해야 할 때가 있습니다. 예를 들어, 디스크에서 생성했거나 사용자가 제출한 URL에서 다운로드한 파일을 첨부하고 싶을 수 있습니다. 또한 model test에서 fixture 파일을 첨부하고 싶을 수도 있습니다. 이를 위해서는 최소한 open IO object와 filename을 포함하는 Hash를 제공해야 합니다:
@message.images.attach(io: File.open("/path/to/file"), filename: "file.pdf")
가능한 경우 content type도 함께 제공하세요. Active Storage는 파일의 데이터로부터 content type을 파악하려고 시도합니다. 이것이 불가능한 경우에는 사용자가 제공한 content type을 대체값으로 사용합니다.
@message.images.attach(io: File.open("/path/to/file"), filename: "file.pdf", content_type: "application/pdf")
파일 시스템의 파일을 직접 첨부할 때는 전체 파일 경로를 제공해야 합니다. 여기서는 파일 I/O를 사용하여 디스크에서 파일을 읽습니다.
데이터의 content type 추론을 건너뛰고 싶다면 content_type
과 함께 identify: false
를 전달하면 됩니다.
@message.images.attach(
io: File.open("/path/to/file"),
filename: "file.pdf",
content_type: "application/pdf",
identify: false
)
content type을 제공하지 않고 Active Storage가 파일의 content type을 자동으로 결정할 수 없는 경우, 기본값으로 application/octet-stream이 사용됩니다.
S3 Bucket에서 폴더/하위 폴더를 지정하는 데 사용할 수 있는 key
라는 추가 파라미터가 있습니다. AWS S3는 그렇지 않으면 파일 이름에 임의의 key를 사용합니다. S3 Bucket 파일을 더 잘 구성하려는 경우 이 방식이 유용합니다.
@message.images.attach(
io: File.open("/path/to/file"),
filename: "file.pdf",
content_type: "application/pdf",
key: "#{Rails.env}/blog_content/intuitive_filename.pdf",
identify: false
)
이와 같이 개발 환경에서 테스트할 때 파일은 [S3_BUCKET]/development/blog_content/
폴더에 저장됩니다. key 파라미터를 사용하는 경우, 업로드가 제대로 이루어지기 위해서는 key가 고유하도록 해야 합니다. 파일명에 고유한 랜덤 key를 추가하는 것이 권장되며, 예를 들면 다음과 같습니다:
def s3_file_key
"#{Rails.env}/blog_content/intuitive_filename-#{SecureRandom.uuid}.pdf"
end
@message.images.attach(
io: File.open("/path/to/file"),
filename: "file.pdf",
content_type: "application/pdf",
key: s3_file_key,
identify: false # 업로드된 파일에 대한 content type과 메타데이터를 자동으로 파악하는 것을 건너뛰기
)
3.4 Attachment 교체 vs 추가
Rails에서 기본적으로 has_many_attached
association에 파일을 첨부하면 기존의 첨부 파일들은 교체됩니다.
기존의 첨부 파일을 유지하려면, 각 첨부 파일의 signed_id
를 hidden form field로 사용할 수 있습니다:
<% @message.images.each do |image| %>
<%= form.hidden_field :images, multiple: true, value: image.signed_id %>
<% end %>
<%= form.file_field :images, multiple: true %>
이는 기존 attachment를 선택적으로 제거할 수 있다는 장점이 있습니다. 예를 들어 JavaScript를 사용하여 개별 hidden field를 제거하는 것이 가능합니다.
3.5 Form Validation
Attachment는 관련 레코드가 성공적으로 save
되기 전까지는 storage service로 전송되지 않습니다. 이는 form 제출이 validation에 실패하면 새로운 attachment(들)가 유실되고 다시 업로드해야 함을 의미합니다. direct uploads는 form이 제출되기 전에 저장되므로, validation이 실패할 때도 업로드한 파일을 유지하는 데 사용할 수 있습니다.
<%= form.hidden_field :avatar, value: @user.avatar.signed_id if @user.avatar.attached? %>
<%= form.file_field :avatar, direct_upload: true %>
4 파일 제거하기
모델에서 attachment를 제거하려면 attachment에서 [purge
][Attached::One#purge]를 호출하세요. 애플리케이션이 Active Job을 사용하도록 설정되어 있다면 [purge_later
][Attached::One#purge_later]를 호출하여 백그라운드에서 제거할 수 있습니다. Purging은 storage service에서 blob과 파일을 삭제합니다.
# 동기적으로 avatar와 실제 리소스 파일을 삭제합니다.
user.avatar.purge
# Active Job을 통해 연관된 모델과 실제 리소스 파일을 비동기적으로 삭제합니다.
user.avatar.purge_later
Serving Files
Active Storage는 파일을 제공하는 두 가지 방법을 지원합니다: redirecting과 proxying입니다.
모든 Active Storage 컨트롤러는 기본적으로 공개적으로 접근 가능합니다. 생성된 URL은 추측하기 어렵지만 의도적으로 영구적입니다. 파일에 더 높은 수준의 보호가 필요한 경우 Authenticated Controllers를 구현하는 것을 고려하세요.
4.1 Redirect Mode
blob에 대한 영구적인 URL을 생성하기 위해서는 url_for
view helper에 blob을 전달할 수 있습니다. 이는 blob의 signed_id
가 포함된 URL을 생성하고, 이는 blob의 RedirectController
로 라우팅됩니다.
url_for(user.avatar)
# => https://www.example.com/rails/active_storage/blobs/redirect/:signed_id/my-avatar.png
RedirectController
는 실제 서비스 엔드포인트로 리다이렉트합니다. 이러한 간접 참조는 서비스 URL을 실제 URL과 분리하여, 예를 들어 고가용성을 위해 다른 서비스에서 첨부 파일을 미러링하는 것을 가능하게 합니다. 리다이렉션의 HTTP 만료 시간은 5분입니다.
다운로드 링크를 생성하려면 rails_blob_{path|url}
헬퍼를 사용하세요. 이 헬퍼를 사용하면 disposition을 설정할 수 있습니다.
rails_blob_path(user.avatar, disposition: "attachment")
attachment로서 user의 아바타에 대한 영구 URL을 생성합니다.
XSS 공격을 방지하기 위해 Active Storage는 일부 파일 유형에 대해 Content-Disposition 헤더를 "attachment"로 강제 설정합니다. 이 동작을 변경하려면 Rails 애플리케이션 설정하기에서 사용 가능한 설정 옵션을 참조하세요.
컨트롤러/뷰 컨텍스트 외부(Background jobs, Cronjobs 등)에서 링크를 생성해야 하는 경우, 다음과 같이 rails_blob_path
에 접근할 수 있습니다:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
4.2 Proxy Mode
선택적으로 파일들을 proxy할 수 있습니다. 이는 애플리케이션 서버가 요청에 대한 응답으로 storage service로부터 파일 데이터를 다운로드한다는 것을 의미합니다. 이는 CDN에서 파일을 제공할 때 유용할 수 있습니다.
다음과 같이 Active Storage가 기본적으로 proxy를 사용하도록 설정할 수 있습니다:
# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy
또는 특정 attachment들을 명시적으로 proxy하고 싶다면 rails_storage_proxy_path
와 rails_storage_proxy_url
형식의 URL helper를 사용할 수 있습니다.
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
4.2.1 CDN을 Active Storage 앞에 배치하기
추가적으로, Active Storage 첨부파일에 CDN을 사용하려면 proxy mode로 URL을 생성해야만 첨부파일이 앱에 의해 서비스되고 CDN이 추가 설정 없이 첨부파일을 캐시할 수 있습니다. 기본 Active Storage proxy 컨트롤러가 CDN에게 응답을 캐시하도록 지시하는 HTTP 헤더를 설정하기 때문에 이는 별도의 설정 없이 작동합니다.
또한 생성된 URL이 앱 호스트 대신 CDN 호스트를 사용하도록 해야 합니다. 이를 달성하는 방법은 여러 가지가 있지만, 일반적으로 config/routes.rb
파일을 조정하여 첨부파일과 그 변형에 대한 적절한 URL을 생성할 수 있도록 하는 것이 포함됩니다. 예를 들어, 다음과 같이 추가할 수 있습니다:
# config/routes.rb
direct :cdn_image do |model, options|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
if model.respond_to?(:signed_id)
route_for(
:rails_service_blob_proxy,
model.signed_id(expires_in: expires_in),
model.filename,
options.merge(host: ENV["CDN_HOST"])
)
else
signed_blob_id = model.blob.signed_id(expires_in: expires_in)
variation_key = model.variation.key
filename = model.blob.filename
route_for(
:rails_blob_representation_proxy,
signed_blob_id,
variation_key,
filename,
options.merge(host: ENV["CDN_HOST"])
)
end
end
그리고 다음과 같이 routes를 생성합니다:
<%= cdn_image_url(user.avatar.variant(resize_to_limit: [128, 128])) %>
4.3 인증된 Controllers
모든 Active Storage controllers는 기본적으로 공개적으로 접근 가능합니다. 생성된 URL들은 일반적인 signed_id
를 사용하여, 추측하기는 어렵지만 영구적입니다. blob URL을 알고 있는 누구나 접근할 수 있으며, ApplicationController
의 before_action
이 로그인을 요구하더라도 접근이 가능합니다. 파일들에 더 높은 수준의 보안이 필요한 경우, ActiveStorage::Blobs::RedirectController
, ActiveStorage::Blobs::ProxyController
, ActiveStorage::Representations::RedirectController
, ActiveStorage::Representations::ProxyController
를 기반으로 자신만의 인증된 controllers를 구현할 수 있습니다.
계정이 자신의 로고에만 접근할 수 있도록 하려면 다음과 같이 할 수 있습니다:
# config/routes.rb
resource :account do
resource :logo
end
# app/controllers/logos_controller.rb
class LogosController < ApplicationController
# ApplicationController를 통해:
# include Authenticate, SetCurrentAccount
def show
redirect_to Current.account.logo.url
end
end
<%= image_tag account_logo_path %>
그리고 다음과 같이 Active Storage 기본 라우트를 비활성화해야 합니다:
config.active_storage.draw_routes = false
Active Storage가 라우트를 설정하지 못하도록 합니다. 대신 자신의 라우트를 수동으로 설정할 수 있습니다.
공개적으로 접근 가능한 URL로 파일이 액세스되는 것을 방지하기 위해서입니다.
5 파일 다운로드하기
때로는 업로드된 blob을 후처리해야 할 필요가 있습니다—예를 들어, 다른 형식으로 변환하는 경우입니다. attachment의 download
메서드를 사용하여 blob의 바이너리 데이터를 메모리로 읽어들일 수 있습니다:
binary = user.avatar.download
사용자의 avatar를 binary 형태로 다운로드합니다.
외부 프로그램(예: 바이러스 스캐너나 미디어 트랜스코더)이 조작할 수 있도록 blob을 디스크의 파일로 다운로드하고 싶을 수 있습니다. 디스크의 tempfile로 blob을 다운로드하려면 attachment의 open
메소드를 사용하세요.
message.video.open do |file|
system "/path/to/virus/scanner", file.path
# ...
end
파일이 after_create
callback에서는 아직 사용할 수 없고 after_create_commit
에서만 사용 가능하다는 것을 알아두는 것이 중요합니다.
6 파일 분석하기
Active Storage는 Active Job에서 작업을 대기열에 추가하여 업로드된 파일을 분석합니다. 분석된 파일은 metadata hash에 analyzed: true
를 포함한 추가 정보를 저장합니다. blob이 분석되었는지는 analyzed?
를 호출하여 확인할 수 있습니다.
이미지 분석은 width
와 height
속성을 제공합니다. 비디오 분석은 이러한 속성들과 함께 duration
, angle
, display_aspect_ratio
, 그리고 해당 채널의 존재 여부를 나타내는 video
와 audio
boolean을 제공합니다. 오디오 분석은 duration
과 bit_rate
속성을 제공합니다.
7 이미지, 비디오, PDF 표시하기
Active Storage는 다양한 파일을 표현하는 것을 지원합니다. 이미지 variant나 비디오 또는 PDF의 미리보기를 표시하기 위해 attachment에서 representation
을 호출할 수 있습니다. representation
을 호출하기 전에, representable?
을 호출하여 attachment가 표현 가능한지 확인하세요. 일부 파일 형식은 기본적으로 Active Storage에서 미리보기를 할 수 없습니다(예: Word 문서). representable?
이 false를 반환하면 대신 파일을 링크하는 것을 고려해볼 수 있습니다.
<ul>
<% @message.files.each do |file| %>
<li>
<% if file.representable? %>
<%= image_tag file.representation(resize_to_limit: [100, 100]) %>
<% else %>
<%= link_to rails_blob_path(file, disposition: "attachment") do %>
<%= image_tag "placeholder.png", alt: "파일 다운로드" %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
내부적으로 representation
은 이미지에 대해서는 variant
를 호출하고, 미리보기 가능한 파일에 대해서는 preview
를 호출합니다. 이러한 메서드들을 직접 호출할 수도 있습니다.
7.1 Lazy vs Immediate Loading
기본적으로 Active Storage는 표현을 지연(lazy) 처리합니다. 다음의 코드는:
image_tag file.representation(resize_to_limit: [100, 100])
<img>
태그를 생성하며 여기서 src
는 ActiveStorage::Representations::RedirectController
를 가리킵니다. 브라우저는 이 컨트롤러에 요청을 보내고, 컨트롤러는 다음을 수행합니다:
- 파일을 처리하고 필요한 경우 처리된 파일을 업로드합니다.
- 파일에 대한
302
리다이렉트를 반환합니다:- 원격 서비스(예: S3)로 리다이렉트하거나
- proxy mode가 활성화된 경우
ActiveStorage::Blobs::ProxyController
로 리다이렉트하여 파일 내용을 반환합니다.
파일을 지연 로딩하면 초기 페이지 로딩 속도를 늦추지 않고도 single use URLs와 같은 기능을 사용할 수 있습니다.
이는 대부분의 경우에 잘 작동합니다.
이미지의 URL을 즉시 생성하고 싶다면 .processed.url
을 호출할 수 있습니다:
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
이미지의 너비와 높이를 각각 최대 100 픽셀로 제한하되 종횡비를 유지하면서 표시합니다. 이미지가 더 작으면 원본 크기가 유지됩니다.
Active Storage variant tracker는 요청된 표현이 이전에 처리된 경우 데이터베이스에 기록을 저장함으로써 성능을 향상시킵니다. 따라서 위 코드는 원격 서비스(예: S3)에 API 호출을 한 번만 수행하며, variant가 저장되면 그것을 사용합니다. Variant tracker는 자동으로 실행되지만 config.active_storage.track_variants
를 통해 비활성화할 수 있습니다.
페이지에 많은 이미지를 렌더링하는 경우, 위의 예시는 모든 variant 레코드를 로드하는 N+1 쿼리를 발생시킬 수 있습니다. 이러한 N+1 쿼리를 피하려면 ActiveStorage::Attachment
의 named scope를 사용하세요.
message.images.with_all_variant_records.each do |file|
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
end
각각의 미리 처리된 representation을 순회하면서 image tag를 출력합니다. 이미지는 100x100 픽셀 제한으로 리사이징됩니다.
7.2 이미지 변환하기
이미지를 변환하면 원하는 크기로 이미지를 표시할 수 있습니다.
이미지의 변형을 만들려면 attachment에서 [variant
][]를 호출하세요.
variant processor가 지원하는 모든 변환을 method에 전달할 수 있습니다.
브라우저가 variant URL에 접근하면, Active Storage는 지정된 형식으로 원본 blob을 지연 변환하고
새로운 service 위치로 redirect합니다.
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
variant가 요청되면 Active Storage는 이미지 형식에 따라 자동으로 변환을 적용합니다:
[
config.active_storage.variable_content_types
][]에 의해 가변적이고 [config.active_storage.web_image_content_types
][]에 의해 웹 이미지로 간주되지 않는 content type들은 PNG로 변환됩니다.quality
가 지정되지 않은 경우, variant processor의 기본 품질이 해당 형식에 사용됩니다.
Active Storage는 variant processor로 [Vips][] 또는 MiniMagick를 사용할 수 있습니다. 기본값은 config.load_defaults
대상 버전에 따라 다르며, [config.active_storage.variant_processor
][]를 설정하여 processor를 변경할 수 있습니다.
사용 가능한 매개변수는 [image_processing
][] gem에 의해 정의되며 사용하는 variant processor에 따라 다르지만, 두 processor 모두 다음 매개변수를 지원합니다:
매개변수 | 예시 | 설명 |
---|---|---|
resize_to_limit |
resize_to_limit: [100, 100] |
원본 종횡비를 유지하면서 지정된 크기 내에 맞도록 이미지를 축소합니다. 지정된 크기보다 클 경우에만 이미지를 크기 조정합니다. |
resize_to_fit |
resize_to_fit: [100, 100] |
원본 종횡비를 유지하면서 지정된 크기 내에 맞도록 이미지 크기를 조정합니다. 지정된 크기보다 클 경우 축소하고 작을 경우 확대합니다. |
resize_to_fill |
resize_to_fill: [100, 100] |
원본 종횡비를 유지하면서 지정된 크기를 채우도록 이미지 크기를 조정합니다. 필요한 경우 더 큰 차원에서 이미지를 자릅니다. |
resize_and_pad |
resize_and_pad: [100, 100] |
원본 종횡비를 유지하면서 지정된 크기 내에 맞도록 이미지 크기를 조정합니다. 필요한 경우 원본 이미지에 알파 채널이 있으면 투명색으로, 그렇지 않으면 검은색으로 나머지 영역을 채웁니다. |
crop |
crop: [20, 50, 300, 300] |
이미지에서 영역을 추출합니다. 처음 두 인수는 추출할 영역의 왼쪽과 위쪽 가장자리이며, 마지막 두 인수는 추출할 영역의 너비와 높이입니다. |
rotate |
rotate: 90 |
지정된 각도만큼 이미지를 회전합니다. |
[image_processing
][]은 Vips와 MiniMagick processor 모두에 대한 모든 매개변수를 자체 문서에서 제공합니다.
위에 나열된 매개변수를 포함한 일부 매개변수는 해시 내에서 key: value
쌍으로 전달할 수 있는 processor별 추가 옵션을 허용합니다:
<!-- Vips는 많은 변형에서 `crop` 설정을 지원합니다 -->
<%= image_tag user.avatar.variant(resize_to_fill: [100, 100, { crop: :centre }]) %>
기존 애플리케이션을 MiniMagick과 Vips 사이에서 마이그레이션하는 경우, processor별 옵션을 업데이트해야 합니다:
<!-- MiniMagick -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>
<!-- Vips -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>
config.active_storage.variable_content_types
는 variant를 만들 수 있는 content type을 지정합니다.
기본값:
config.active_storage.variable_content_types = %w(
image/png
image/gif
image/jpg
image/jpeg
image/pjpeg
image/tiff
image/bmp
image/vnd.adobe.photoshop
image/vnd.microsoft.icon
image/webp
)
config.active_storage.variant_processor
는 variant를 생성할 때 사용할 이미지 프로세서를 지정합니다. 클래식한 ImageMagick 기반의 :mini_magick
프로세서나 더 현대적인 libvips 기반의 :vips
중에서 선택할 수 있습니다.
기본값:
config.active_storage.variant_processor = :mini_magick
:vips
프로세서를 사용하려면, 먼저 [ruby-vips] gem이 포함된 [image_processing
] gem이 필요합니다:
bundle add image_processing
config.active_storage.web_image_content_types
는 웹에서 일반적으로 사용되는 이미지 content type의 배열을 지정합니다. [variant
]가 호출되어 명시적인 content type이 없는 경우, 변환된 파일은 원본 blob의 content type을 유지하거나, 원본이 웹 이미지가 아닌 경우 image/png
로 변환됩니다.
기본값:
config.active_storage.web_image_content_types = %w(
image/png
image/jpeg
image/jpg
image/gif
)
7.3 파일 미리보기
이미지가 아닌 일부 파일들도 미리보기가 가능합니다: 즉, 이미지 형태로 표시될 수 있습니다. 예를 들어, 비디오 파일은 첫 번째 프레임을 추출하여 미리보기를 할 수 있습니다. Active Storage는 기본적으로 비디오와 PDF 문서의 미리보기를 지원합니다. 지연 생성되는 미리보기의 링크를 만들려면 attachment의 preview
메서드를 사용하세요:
<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>
다른 포맷을 지원하기 위해서는 자신만의 previewer를 추가하세요. 자세한 내용은 ActiveStorage::Preview
문서를 참조하세요.
8 Direct Uploads
Active Storage는 포함된 JavaScript 라이브러리를 통해 클라이언트에서 클라우드로 직접 업로드하는 것을 지원합니다.
8.1 사용법
애플리케이션의 JavaScript 번들에
activestorage.js
를 포함시킵니다.Asset pipeline 사용시:
//= require activestorage
npm 패키지 사용시:
import * as ActiveStorage from "@rails/activestorage" ActiveStorage.start()
파일 필드에
direct_upload: true
를 추가합니다:<%= form.file_field :attachments, multiple: true, direct_upload: true %>
또는
FormBuilder
를 사용하지 않는 경우, data 속성을 직접 추가합니다:<input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
서드파티 저장소 서비스에서 직접 업로드 요청을 허용하도록 CORS를 구성합니다.
끝입니다! 폼 제출 시 업로드가 시작됩니다.
8.2 Cross-Origin Resource Sharing (CORS) 설정
서드파티 서비스로 직접 업로드가 동작하게 하려면, 해당 서비스가 여러분의 앱으로부터의 cross-origin 요청을 허용하도록 설정해야 합니다. 각 서비스의 CORS 문서를 참조하세요:
다음 사항들을 허용하도록 주의하세요:
- 여러분의 앱에 접근하는 모든 origin
PUT
request method- 다음 헤더들:
Content-Type
Content-MD5
Content-Disposition
(Azure Storage 제외)x-ms-blob-content-disposition
(Azure Storage만 해당)x-ms-blob-type
(Azure Storage만 해당)Cache-Control
(GCS의 경우,cache_control
이 설정된 경우에만)
Disk 서비스는 여러분의 앱과 origin을 공유하므로 CORS 설정이 필요하지 않습니다.
8.2.1 예시: S3 CORS 설정
[
{
"AllowedHeaders": [
"Content-Type",
"Content-MD5",
"Content-Disposition"
],
"AllowedMethods": [
"PUT"
],
"AllowedOrigins": [
"https://www.example.com"
],
"MaxAgeSeconds": 3600
}
]
8.2.2 Google Cloud Storage CORS Configuration 예시
<?xml version="1.0" encoding="UTF-8"?>
<CorsConfig>
<Cors>
<Origins>
<Origin>*</Origin>
</Origins>
<Methods>
<Method>GET</Method>
<Method>POST</Method>
<Method>PUT</Method>
<Method>DELETE</Method>
</Methods>
<ResponseHeaders>
<ResponseHeader>x-goog-meta-foo1</ResponseHeader>
<ResponseHeader>x-goog-meta-foo2</ResponseHeader>
</ResponseHeaders>
<MaxAgeSec>3600</MaxAgeSec>
</Cors>
</CorsConfig>
[
{
"origin": ["https://www.example.com"],
"method": ["PUT"],
"responseHeader": ["Content-Type", "Content-MD5", "Content-Disposition"],
"maxAgeSeconds": 3600
}
]
8.2.3 Azure Storage CORS 설정 예시
다음은 간단한 Azure Storage CORS 설정의 예시입니다:
<Cors>
<CorsRule>
<AllowedOrigins>http://www.example.com</AllowedOrigins>
<AllowedMethods>PUT,GET</AllowedMethods>
<AllowedHeaders>*</AllowedHeaders>
<ExposedHeaders>*</ExposedHeaders>
<MaxAgeInSeconds>3000</MaxAgeInSeconds>
</CorsRule>
</Cors>
더 자세한 내용은 Azure Storage CORS 가이드를 참고하세요.
<Cors>
<CorsRule>
<AllowedOrigins>https://www.example.com</AllowedOrigins>
<AllowedMethods>PUT</AllowedMethods>
<AllowedHeaders>Content-Type, Content-MD5, x-ms-blob-content-disposition, x-ms-blob-type</AllowedHeaders>
<MaxAgeInSeconds>3600</MaxAgeInSeconds>
</CorsRule>
</Cors>
8.3 Direct Upload JavaScript 이벤트
이벤트 이름 | 이벤트 대상 | 이벤트 데이터 (event.detail ) |
설명 |
---|---|---|---|
direct-uploads:start |
<form> |
없음 | direct upload 필드에 파일이 포함된 form이 제출되었습니다. |
direct-upload:initialize |
<input> |
{id, file} |
form 제출 후 모든 파일에 대해 디스패치됩니다. |
direct-upload:start |
<input> |
{id, file} |
direct upload가 시작됩니다. |
direct-upload:before-blob-request |
<input> |
{id, file, xhr} |
direct upload 메타데이터를 위해 애플리케이션에 요청하기 전입니다. |
direct-upload:before-storage-request |
<input> |
{id, file, xhr} |
파일을 저장하기 위한 요청을 하기 전입니다. |
direct-upload:progress |
<input> |
{id, file, progress} |
파일 저장 요청이 진행되는 동안입니다. |
direct-upload:error |
<input> |
{id, file, error} |
오류가 발생했습니다. 이 이벤트가 취소되지 않으면 alert 가 표시됩니다. |
direct-upload:end |
<input> |
{id, file} |
direct upload가 종료되었습니다. |
direct-uploads:end |
<form> |
없음 | 모든 direct upload가 종료되었습니다. |
8.4 예시
이러한 이벤트들을 사용하여 업로드 진행 상황을 표시할 수 있습니다.
form에서 업로드된 파일을 표시하려면:
// direct_uploads.js
addEventListener("direct-upload:initialize", event => {
const { target, detail } = event
const { id, file } = detail
target.insertAdjacentHTML("beforebegin", `
<div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
<div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename"></span>
</div>
`)
target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent = file.name
})
addEventListener("direct-upload:start", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.remove("direct-upload--pending")
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail
const progressElement = document.getElementById(`direct-upload-progress-${id}`)
progressElement.style.width = `${progress}%`
})
addEventListener("direct-upload:error", event => {
event.preventDefault()
const { id, error } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--error")
element.setAttribute("title", error)
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--complete")
})
스타일 추가:
/* direct_uploads.css */
.direct-upload { display: inline-block; position: relative; padding: 2px 4px; margin: 0 3px 3px 0; border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 3px; font-size: 11px; line-height: 13px; }
.direct-upload--pending { opacity: 0.6; }
.direct-upload__progress { position: absolute; top: 0; left: 0; bottom: 0; opacity: 0.2; background: #0076ff; transition: width 120ms ease-out, opacity 60ms 60ms ease-in; transform: translate3d(0, 0, 0); }
.direct-upload--complete .direct-upload__progress { opacity: 0.4; }
.direct-upload--error { border-color: red; }
input[type=file][data-direct-upload-url][disabled] { display: none; }
8.5 사용자 정의 드래그 앤 드롭 솔루션
이 목적을 위해서 DirectUpload
클래스를 사용할 수 있습니다. 선택한 라이브러리에서 파일을 받으면 DirectUpload를 인스턴스화하고 create 메소드를 호출하세요. Create는 업로드가 완료되었을 때 호출할 콜백을 받습니다.
import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
// 파일 드롭에 바인딩 - 부모 요소에 ondrop을 사용하거나
// Dropzone과 같은 라이브러리 사용
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// 일반적인 파일 선택에 바인딩
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// input에서 선택된 파일들을 지울 수 있습니다
input.value = null
})
const uploadFile = (file) => {
// form에 file_field direct_upload: true가 필요하며,
// 이는 data-direct-upload-url을 제공합니다
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// 에러 처리
} else {
// blob.signed_id 값을 가진 적절한 이름의 hidden input을
// form에 추가하여 일반적인 업로드 흐름에서
// blob id가 전송되도록 합니다
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
8.6 파일 업로드 진행 상황 추적하기
DirectUpload
constructor를 사용할 때, 세 번째 파라미터를 포함할 수 있습니다.
이를 통해 DirectUpload
객체가 업로드 프로세스 중에 directUploadWillStoreFileWithXHR
메소드를 호출할 수 있습니다.
그런 다음 필요에 따라 XHR에 자신만의 progress handler를 연결할 수 있습니다.
import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url) {
this.upload = new DirectUpload(file, url, this)
}
uploadFile(file) {
this.upload.create((error, blob) => {
if (error) {
// 에러를 처리합니다
} else {
// blob.signed_id 값을 가진 적절한 이름의
// hidden input을 form에 추가합니다
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// event.loaded와 event.total을 사용하여 진행 표시줄을 업데이트합니다
}
}
8.7 라이브러리나 프레임워크와 통합하기
선택한 라이브러리에서 파일을 받으면 DirectUpload
인스턴스를 생성하고 "create" 메소드를 사용하여 업로드 프로세스를 시작해야 합니다. 필요한 경우 추가 헤더를 포함시킬 수 있습니다. "create" 메소드는 또한 업로드가 완료되었을 때 트리거될 콜백 함수를 제공해야 합니다.
import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url, token) {
const headers = { 'Authentication': `Bearer ${token}` }
// INFO: headers 전송은 선택적인 매개변수입니다. 헤더를 보내지 않기로 선택하면,
// 인증은 쿠키나 세션 데이터를 사용해 수행됩니다.
this.upload = new DirectUpload(file, url, this, headers)
}
uploadFile(file) {
this.upload.create((error, blob) => {
if (error) {
// 에러 처리
} else {
// 다음 요청에서 blob.signed_id를 파일 참조로 사용
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// event.loaded와 event.total을 사용하여 진행 막대 업데이트
}
}
Rails 어플리케이션에서 커스텀 인증을 구현하려면 다음과 같은 새로운 controller를 생성해야 합니다:
class DirectUploadsController < ActiveStorage::DirectUploadsController
skip_forgery_protection
before_action :authenticate!
def authenticate!
@token = request.headers["Authorization"]&.split&.last
유효한 토큰이 아닐 경우 unauthorized를 반환합니다
head :unauthorized unless valid_token?(@token)
end
end
Direct Uploads를 사용하면 파일이 업로드는 되지만 레코드에 첨부되지 않는 경우가 있을 수 있습니다. 미첨부 업로드 제거하기를 고려해보세요.
9 Testing
통합 테스트나 컨트롤러 테스트에서 파일 업로드를 테스트하려면 file_fixture_upload
를 사용하세요.
Rails는 파일을 다른 파라미터와 동일하게 처리합니다.
class SignupController < ActionDispatch::IntegrationTest
test "회원가입이 가능한지" do
post signup_path, params: {
name: "David",
avatar: file_fixture_upload("david.png", "image/png")
}
user = User.order(:created_at).last
assert user.avatar.attached?
end
end
9.1 테스트 중 생성된 파일 삭제하기
9.1.1 System Tests
System test는 transaction을 롤백하여 테스트 데이터를 정리합니다. 객체에 대해 destroy
가 호출되지 않기 때문에 첨부된 파일들은 절대 정리되지 않습니다. 파일들을 정리하고 싶다면 after_teardown
콜백에서 수행할 수 있습니다. 여기서 수행하면 테스트 중에 생성된 모든 연결이 완료되었음을 보장하며, Active Storage가 파일을 찾을 수 없다는 오류를 받지 않을 수 있습니다.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
# ...
end
parallel tests와 DiskService
를 사용하는 경우, 각 프로세스가 Active Storage를 위해 자체 폴더를 사용하도록 설정해야 합니다. 이렇게 하면 teardown
콜백이 관련 프로세스의 테스트에서만 파일을 삭제합니다.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
# ...
end
시스템 테스트에서 attachment가 있는 모델의 삭제를 검증하고 Active Job을 사용하는 경우, 테스트 환경에서 inline queue adapter를 사용하도록 설정하세요. 이렇게 하면 purge job이 미래의 알 수 없는 시점이 아닌 즉시 실행됩니다.
# 즉시 처리되도록 inline job 처리를 사용
config.active_job.queue_adapter = :inline
9.1.2 Integration Tests
System Tests와 마찬가지로, Integration Tests 중에 업로드된 파일들은 자동으로 정리되지 않습니다. 파일들을 정리하고 싶다면 teardown
콜백에서 수행할 수 있습니다.
class ActionDispatch::IntegrationTest
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
end
테스트가 완료된 후에는 Active Storage가 업로드된 파일을 정리하도록 합니다. 필요할 경우 test/test_helper.rb
에 위의 코드를 추가하세요.
parallel tests와 Disk service를 사용한다면, 각 프로세스가 Active Storage를 위해 자체 폴더를 사용하도록 설정해야 합니다. 이렇게 하면 teardown
콜백은 해당 프로세스의 테스트에서 생성된 파일만 삭제합니다.
class ActionDispatch::IntegrationTest
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
end
9.2 Fixtures에 Attachment 추가하기
기존 fixtures에 attachment를 추가할 수 있습니다. 먼저, 별도의 storage service를 생성해야 합니다:
# config/storage.yml
test_fixtures:
service: Disk
root: <%= Rails.root.join("tmp/storage_fixtures") %>
Active Storage에게 fixture 파일을 어디로 "업로드" 할지 알려줍니다. 따라서 임시 디렉토리여야 합니다. 일반적인 test
서비스와는 다른 디렉토리로 지정함으로써, fixture 파일과 테스트 중에 업로드된 파일을 분리할 수 있습니다.
다음으로, Active Storage 클래스를 위한 fixture 파일들을 생성하세요:
# active_storage/attachments.yml
david_avatar:
name: avatar
record: david (User)
blob: david_avatar_blob
# active_storage/blobs.yml
david_avatar_blob: <%= ActiveStorage::FixtureSet.blob filename: "david.png", service_name: "test_fixtures" %>
그런 다음 fixtures 디렉토리(기본 경로는 test/fixtures/files
)에 해당 파일명의 파일을 넣으세요.
자세한 내용은 ActiveStorage::FixtureSet
문서를 참조하세요.
모든 설정이 완료되면 테스트에서 첨부 파일에 접근할 수 있습니다:
class UserTest < ActiveSupport::TestCase
def test_avatar
avatar = users(:david).avatar
assert avatar.attached?
assert_not_nil avatar.download
assert_equal 1000, avatar.byte_size
end
end
9.2.1 Fixture 정리하기
테스트에서 업로드된 파일들은 각 테스트 종료 시점에 정리되지만, fixture 파일들은 모든 테스트가 완료되는 시점에 한 번만 정리하면 됩니다.
parallel 테스트를 사용하는 경우, parallelize_teardown
을 호출하세요:
class ActiveSupport::TestCase
# ...
parallelize_teardown do |i|
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
# ...
end
병렬 테스트를 실행하지 않는 경우, Minitest.after_run
또는 테스트 프레임워크에 맞는 동등한 기능을 사용하세요(예: RSpec의 경우 after(:suite)
):
# test_helper.rb
Minitest.after_run do
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
test가 끝난 후에 Active Storage fixture 파일을 정리합니다.
9.3 서비스 구성하기
test 환경에서 사용할 서비스를 구성하기 위해 config/storage/test.yml
을 추가할 수 있습니다.
이는 service
옵션이 사용될 때 유용합니다.
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
config/storage/test.yml
이 없으면 테스트를 실행할 때에도 config/storage.yml
에 설정된 s3
service가 사용됩니다.
기본 설정이 사용되며 파일들은 config/storage.yml
에 설정된 service provider에 업로드됩니다.
이 경우, config/storage/test.yml
을 추가하고 요청 전송을 방지하기 위해 s3
service에 Disk service를 사용할 수 있습니다.
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
s3:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
10 Implementing Support for Other Cloud Services
만약 위에서 언급된 것 외의 cloud service를 지원해야 한다면, Service를 직접 구현해야 합니다. 각 service는 파일을 cloud에 upload하고 download하는 데 필요한 메소드들을 구현하여 ActiveStorage::Service
를 확장합니다.
11 Purging Unattached Uploads
파일이 upload되었지만 record에 attach되지 않는 경우가 있습니다. 이는 Direct Uploads를 사용할 때 발생할 수 있습니다. unattached scope를 사용하여 attach되지 않은 record들을 조회할 수 있습니다. 아래는 custom rake task를 사용하는 예시입니다.
namespace :active_storage do
desc "연결되지 않은 Active Storage blob들을 제거합니다. 정기적으로 실행하세요."
task purge_unattached: :environment do
ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge_later)
end
end
ActiveStorage::Blob.unattached
에 의해 생성되는 쿼리는 대규모 데이터베이스를 가진 애플리케이션에서 느리고 잠재적으로 성능에 영향을 줄 수 있습니다.