mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
FEATURE: Add S3 etag value to uploads table (#6795)
This commit is contained in:

committed by
Guo Xiang Tan

parent
3a04e04ccb
commit
75dbb98cca
@ -289,6 +289,7 @@ end
|
|||||||
# extension :string(10)
|
# extension :string(10)
|
||||||
# thumbnail_width :integer
|
# thumbnail_width :integer
|
||||||
# thumbnail_height :integer
|
# thumbnail_height :integer
|
||||||
|
# etag :string
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@ -297,4 +298,5 @@ end
|
|||||||
# index_uploads_on_sha1 (sha1) UNIQUE
|
# index_uploads_on_sha1 (sha1) UNIQUE
|
||||||
# index_uploads_on_url (url)
|
# index_uploads_on_url (url)
|
||||||
# index_uploads_on_user_id (user_id)
|
# index_uploads_on_user_id (user_id)
|
||||||
|
# index_uploads_on_etag (etag)
|
||||||
#
|
#
|
||||||
|
9
db/migrate/20181218071253_add_etag_to_uploads.rb
Normal file
9
db/migrate/20181218071253_add_etag_to_uploads.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class AddEtagToUploads < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :uploads, :etag, :string
|
||||||
|
add_index :uploads, [:etag]
|
||||||
|
|
||||||
|
add_column :optimized_images, :etag, :string
|
||||||
|
add_index :optimized_images, [:etag]
|
||||||
|
end
|
||||||
|
end
|
@ -19,12 +19,14 @@ module FileStore
|
|||||||
|
|
||||||
def store_upload(file, upload, content_type = nil)
|
def store_upload(file, upload, content_type = nil)
|
||||||
path = get_path_for_upload(upload)
|
path = get_path_for_upload(upload)
|
||||||
store_file(file, path, filename: upload.original_filename, content_type: content_type, cache_locally: true)
|
url, upload.etag = store_file(file, path, filename: upload.original_filename, content_type: content_type, cache_locally: true)
|
||||||
|
url
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_optimized_image(file, optimized_image, content_type = nil)
|
def store_optimized_image(file, optimized_image, content_type = nil)
|
||||||
path = get_path_for_optimized_image(optimized_image)
|
path = get_path_for_optimized_image(optimized_image)
|
||||||
store_file(file, path, content_type: content_type)
|
url, optimized_image.etag = store_file(file, path, content_type: content_type)
|
||||||
|
url
|
||||||
end
|
end
|
||||||
|
|
||||||
# options
|
# options
|
||||||
@ -42,13 +44,14 @@ module FileStore
|
|||||||
}
|
}
|
||||||
# add a "content disposition" header for "attachments"
|
# add a "content disposition" header for "attachments"
|
||||||
options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_supported_image?(filename)
|
options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_supported_image?(filename)
|
||||||
# if this fails, it will throw an exception
|
|
||||||
|
|
||||||
path.prepend(File.join(upload_path, "/")) if Rails.configuration.multisite
|
path.prepend(File.join(upload_path, "/")) if Rails.configuration.multisite
|
||||||
path = @s3_helper.upload(file, path, options)
|
|
||||||
|
|
||||||
# return the upload url
|
# if this fails, it will throw an exception
|
||||||
File.join(absolute_base_url, path)
|
path, etag = @s3_helper.upload(file, path, options)
|
||||||
|
|
||||||
|
# return the upload url and etag
|
||||||
|
return File.join(absolute_base_url, path), etag
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_file(url, path)
|
def remove_file(url, path)
|
||||||
|
@ -24,8 +24,21 @@ class S3Helper
|
|||||||
|
|
||||||
def upload(file, path, options = {})
|
def upload(file, path, options = {})
|
||||||
path = get_path_for_s3_upload(path)
|
path = get_path_for_s3_upload(path)
|
||||||
s3_bucket.object(path).upload_file(file, options)
|
obj = s3_bucket.object(path)
|
||||||
path
|
|
||||||
|
etag = begin
|
||||||
|
if File.size(file) >= Aws::S3::FileUploader::FIFTEEN_MEGABYTES
|
||||||
|
options[:multipart_threshold] = Aws::S3::FileUploader::FIFTEEN_MEGABYTES
|
||||||
|
obj.upload_file(file, options)
|
||||||
|
obj.load
|
||||||
|
obj.etag
|
||||||
|
else
|
||||||
|
options[:body] = file
|
||||||
|
obj.put(options).etag
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return path, etag
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove(s3_filename, copy_to_tombstone = false)
|
def remove(s3_filename, copy_to_tombstone = false)
|
||||||
@ -210,8 +223,12 @@ class S3Helper
|
|||||||
File.join("uploads", RailsMultisite::ConnectionManagement.current_db, "/")
|
File.join("uploads", RailsMultisite::ConnectionManagement.current_db, "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def s3_client
|
||||||
|
Aws::S3::Client.new(@s3_options)
|
||||||
|
end
|
||||||
|
|
||||||
def s3_resource
|
def s3_resource
|
||||||
Aws::S3::Resource.new(@s3_options)
|
Aws::S3::Resource.new(client: s3_client)
|
||||||
end
|
end
|
||||||
|
|
||||||
def s3_bucket
|
def s3_bucket
|
||||||
|
@ -126,7 +126,8 @@ class UploadCreator
|
|||||||
url = Discourse.store.store_upload(f, @upload)
|
url = Discourse.store.store_upload(f, @upload)
|
||||||
|
|
||||||
if url.present?
|
if url.present?
|
||||||
@upload.update!(url: url)
|
@upload.url = url
|
||||||
|
@upload.save!
|
||||||
else
|
else
|
||||||
@upload.errors.add(:url, I18n.t("upload.store_failure", upload_id: @upload.id, user_id: user_id))
|
@upload.errors.add(:url, I18n.t("upload.store_failure", upload_id: @upload.id, user_id: user_id))
|
||||||
end
|
end
|
||||||
|
@ -38,6 +38,8 @@ describe FileStore::S3Store do
|
|||||||
context 'uploading to s3' do
|
context 'uploading to s3' do
|
||||||
include_context "s3 helpers"
|
include_context "s3 helpers"
|
||||||
|
|
||||||
|
let(:etag) { "etag" }
|
||||||
|
|
||||||
describe "#store_upload" do
|
describe "#store_upload" do
|
||||||
it "returns an absolute schemaless url" do
|
it "returns an absolute schemaless url" do
|
||||||
store.expects(:get_depth_for).with(upload.id).returns(0)
|
store.expects(:get_depth_for).with(upload.id).returns(0)
|
||||||
@ -45,11 +47,13 @@ describe FileStore::S3Store do
|
|||||||
s3_object = stub
|
s3_object = stub
|
||||||
|
|
||||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.png").returns(s3_object)
|
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||||
s3_object.expects(:upload_file)
|
|
||||||
|
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: etag))
|
||||||
|
|
||||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.png"
|
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.png"
|
||||||
)
|
)
|
||||||
|
expect(upload.etag).to eq(etag)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when s3_upload_bucket includes folders path" do
|
describe "when s3_upload_bucket includes folders path" do
|
||||||
@ -63,11 +67,13 @@ describe FileStore::S3Store do
|
|||||||
s3_object = stub
|
s3_object = stub
|
||||||
|
|
||||||
s3_bucket.expects(:object).with("discourse-uploads/original/1X/#{upload.sha1}.png").returns(s3_object)
|
s3_bucket.expects(:object).with("discourse-uploads/original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||||
s3_object.expects(:upload_file)
|
|
||||||
|
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: etag))
|
||||||
|
|
||||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/discourse-uploads/original/1X/#{upload.sha1}.png"
|
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/discourse-uploads/original/1X/#{upload.sha1}.png"
|
||||||
)
|
)
|
||||||
|
expect(upload.etag).to eq(etag)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -80,11 +86,13 @@ describe FileStore::S3Store do
|
|||||||
path = "optimized/1X/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200.png"
|
path = "optimized/1X/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200.png"
|
||||||
|
|
||||||
s3_bucket.expects(:object).with(path).returns(s3_object)
|
s3_bucket.expects(:object).with(path).returns(s3_object)
|
||||||
s3_object.expects(:upload_file)
|
|
||||||
|
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: etag))
|
||||||
|
|
||||||
expect(store.store_optimized_image(optimized_image_file, optimized_image)).to eq(
|
expect(store.store_optimized_image(optimized_image_file, optimized_image)).to eq(
|
||||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{path}"
|
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{path}"
|
||||||
)
|
)
|
||||||
|
expect(optimized_image.etag).to eq(etag)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when s3_upload_bucket includes folders path" do
|
describe "when s3_upload_bucket includes folders path" do
|
||||||
@ -99,11 +107,13 @@ describe FileStore::S3Store do
|
|||||||
path = "discourse-uploads/optimized/1X/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200.png"
|
path = "discourse-uploads/optimized/1X/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200.png"
|
||||||
|
|
||||||
s3_bucket.expects(:object).with(path).returns(s3_object)
|
s3_bucket.expects(:object).with(path).returns(s3_object)
|
||||||
s3_object.expects(:upload_file)
|
|
||||||
|
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: etag))
|
||||||
|
|
||||||
expect(store.store_optimized_image(optimized_image_file, optimized_image)).to eq(
|
expect(store.store_optimized_image(optimized_image_file, optimized_image)).to eq(
|
||||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{path}"
|
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{path}"
|
||||||
)
|
)
|
||||||
|
expect(optimized_image.etag).to eq(etag)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
require 'file_store/s3_store'
|
||||||
|
|
||||||
RSpec.describe UploadCreator do
|
RSpec.describe UploadCreator do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
@ -166,5 +167,34 @@ RSpec.describe UploadCreator do
|
|||||||
expect(upload.original_filename).to eq('should_be_jpeg.jpg')
|
expect(upload.original_filename).to eq('should_be_jpeg.jpg')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'uploading to s3' do
|
||||||
|
let(:filename) { "should_be_jpeg.png" }
|
||||||
|
let(:file) { file_from_fixtures(filename) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
||||||
|
SiteSetting.s3_access_key_id = "s3-access-key-id"
|
||||||
|
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||||
|
SiteSetting.s3_region = 'us-west-1'
|
||||||
|
SiteSetting.enable_s3_uploads = true
|
||||||
|
|
||||||
|
store = FileStore::S3Store.new
|
||||||
|
s3_helper = store.instance_variable_get(:@s3_helper)
|
||||||
|
client = Aws::S3::Client.new(stub_responses: true)
|
||||||
|
s3_helper.stubs(:s3_client).returns(client)
|
||||||
|
Discourse.stubs(:store).returns(store)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should store the file and return etag' do
|
||||||
|
expect {
|
||||||
|
UploadCreator.new(file, filename).create_for(user.id)
|
||||||
|
}.to change { Upload.count }.by(1)
|
||||||
|
|
||||||
|
upload = Upload.last
|
||||||
|
|
||||||
|
expect(upload.etag).to eq('ETag')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -24,12 +24,14 @@ RSpec.describe 'Multisite s3 uploads', type: :multisite do
|
|||||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||||
"//#{SiteSetting.s3_upload_bucket}.s3.dualstack.us-east-1.amazonaws.com/uploads/default/original/1X/c530c06cf89c410c0355d7852644a73fc3ec8c04.png"
|
"//#{SiteSetting.s3_upload_bucket}.s3.dualstack.us-east-1.amazonaws.com/uploads/default/original/1X/c530c06cf89c410c0355d7852644a73fc3ec8c04.png"
|
||||||
)
|
)
|
||||||
|
expect(upload.etag).to eq("ETag")
|
||||||
end
|
end
|
||||||
|
|
||||||
conn.with_connection('second') do
|
conn.with_connection('second') do
|
||||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||||
"//#{SiteSetting.s3_upload_bucket}.s3.dualstack.us-east-1.amazonaws.com/uploads/second/original/1X/c530c06cf89c410c0355d7852644a73fc3ec8c04.png"
|
"//#{SiteSetting.s3_upload_bucket}.s3.dualstack.us-east-1.amazonaws.com/uploads/second/original/1X/c530c06cf89c410c0355d7852644a73fc3ec8c04.png"
|
||||||
)
|
)
|
||||||
|
expect(upload.etag).to eq("ETag")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user