С S3 на самом деле нет простых готовых решений для загрузки файлов, потому что Amazon - довольно сложный инструмент.
Раньше у меня была аналогичная проблема, и я потратил две недели, пытаясь понять, как работает S3, а теперь использую рабочее решение для загрузки файлов на S3. Я могу сказать вам решение, которое работает для меня, я никогда не пробовал то, что было предложено Heroku. Я предпочитаю плагин Plupload, поскольку это единственный компонент, который мне действительно удалось заставить работать, помимо простой прямой загрузки S3 через XHR, и предлагает использование процентных индикаторов и изменение размера изображения в браузере, что я считаю совершенно обязательным. для производственных приложений, где у некоторых пользователей есть изображения размером 20 МБ, которые они хотят загрузить в качестве аватара.
Некоторые основы S3:
Шаг 1
Корзина Amazon нуждается в правильной конфигурации в своем CORS-файле, чтобы в первую очередь разрешить внешние загрузки. Тотториал Heroku уже рассказал вам, как разместить конфигурацию в нужном месте. http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html
Шаг 2
Требуются данные политики, иначе ваш клиент не сможет получить доступ к соответствующему файлу корзины. Я считаю, что создание политик лучше выполнять с помощью вызовов Ajax, чтобы, например, администратор мог загружать файлы в папки разных пользователей. В моем примере cancan используется для управления безопасностью данного пользователя, а figaro используется для управления переменными ENV.
def aws_policy_image
user = User.find_by_id(params[:user_id])
authorize! :upload_image, current_user
options = {}
bucket = Rails.configuration.bucket
access_key_id = ENV["AWS_ACCESS_KEY_ID"]
secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
options[:key] ||= "users/" + params[:user_id] # folder on AWS to store file in
options[:acl] ||= 'private'
options[:expiration_date] ||= 10.hours.from_now.utc.iso8601
options[:max_filesize] ||= 10.megabytes
options[:content_type] ||= 'image/' # Videos would be binary/octet-stream
options[:filter_title] ||= 'Images'
options[:filter_extentions] ||= 'jpg,jpeg,gif,png,bmp'
policy = Base64.encode64(
"{'expiration': '#{options[:expiration_date]}',
'conditions': [
{'x-amz-server-side-encryption': 'AES256'},
{'bucket': '#{bucket}'},
{'acl': '#{options[:acl]}'},
{'success_action_status': '201'},
['content-length-range', 0, #{options[:max_filesize]}],
['starts-with', '$key', '#{options[:key]}'],
['starts-with', '$Content-Type', ''],
['starts-with', '$name', ''],
['starts-with', '$Filename', '']
]
}").gsub(/\n|\r/, '')
signature = Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest::Digest.new('sha1'),
secret_access_key, policy)).gsub("\n", "")
render :json => {:access_key_id => access_key_id, :policy => policy, :signature => signature, :bucket => bucket}
end
Я дошел до того, что поместил этот метод в контроллер приложения, хотя вы могли бы найти для него лучшее место. Путь к этой функции, конечно же, должен быть включен в маршрут.
Шаг 3
Frontend, получите plupload: http://www.plupload.com/ создайте ссылку, которая будет действовать как кнопка загрузки :
<a id="upload_button" href="#">Upload</a>
Создайте сценарий, который настраивает инициализацию plupload.
function Plupload(config_x, access_key_id, policy, signature, bucket) {
var $this = this;
$this.config = $.extend({
key: 'error',
acl: 'private',
content_type: '',
filter_title: 'Images',
filter_extentions: 'jpg,jpeg,gif,png,bmp',
select_button: "upload_button",
multi_selection: true,
callback: function (params) {
},
add_files_callback: function (up, files) {
},
complete_callback: function (params) {
}
}, config_x);
$this.params = {
runtimes: 'html5',
browse_button: $this.config.select_button,
max_file_size: $this.config.max_file_size,
url: 'https://' + bucket + '.s3.amazonaws.com/',
flash_swf_url: '/assets/plupload/js/Moxie.swf',
silverlight_xap_url: '/assets/plupload/js/Moxie.xap',
init: {
FilesRemoved: function (up, files) {
/*if (up.files.length < 1) {
$('#' + config.select_button).fadeIn('slow');
}*/
}
},
multi_selection: $this.config.multi_selection,
multipart: true,
// resize: {width: 1000, height: 1000}, // currently causes "blob" problem
multipart_params: {
'acl': $this.config.acl,
'Content-Type': $this.config.content_type,
'success_action_status': '201',
'AWSAccessKeyId': access_key_id,
'x-amz-server-side-encryption': "AES256",
'policy': policy,
'signature': signature
},
// Resize images on clientside if we can
resize: {
preserve_headers: false, // (!)
width: 1200,
height: 1200,
quality: 70
},
filters: [
{
title: $this.config.filter_title,
extensions: $this.config.filter_extentions
}
],
file_data_name: 'file'
};
$this.uploader = new plupload.Uploader($this.params);
$this.uploader.init();
$this.uploader.bind('UploadProgress', function (up, file) {
$('#' + file.id + ' .percent').text(file.percent + '%');
});
// before upload
$this.uploader.bind('BeforeUpload', function (up, file) {
// optional: regen the filename, otherwise the user will upload image.jpg that will overwrite each other
var extension = file.name.split('.').pop();
var file_name = extension + "_" + (+new Date);
up.settings.multipart_params.key = $this.config.key + '/' + file_name + '.' + extension;
up.settings.multipart_params.Filename = $this.config.key + '/' + file_name + '.' + extension;
file.name = file_name + '.' + extension;
});
// shows error object in the browser console (for now)
$this.uploader.bind('Error', function (up, error) {
console.log('Expand the error object below to see the error. Use WireShark to debug.');
alert_x(".validation-error", error.message);
});
// files added
$this.uploader.bind('FilesAdded', function (up, files) {
$this.config.add_files_callback(up, files, $this.uploader);
// p(uploader);
// uploader.start();
});
// when file gets uploaded
$this.uploader.bind('FileUploaded', function (up, file) {
$this.config.callback(file);
up.refresh();
});
// when all files are uploaded
$this.uploader.bind('UploadComplete', function (up, file) {
$this.config.complete_callback(file);
up.refresh();
});
}
Plupload.prototype.init = function () {
//
}
Шаг 4
Реализация универсальной функции загрузчика файлов:
ImageUploader = {
init: function (user_id, config, callback) {
$.ajax({
type: "get",
url: "/aws_policy_image",
data: {user_id: user_id},
error: function (request, status, error) {
alert(request.responseText);
},
success: function (msg) {
// set aws credentials
callback(config, msg);
}
});
},
},
// local functions
photo_uploader: function (user_id) {
var container = "#photos .unverified_images" // for example;
var can_render = false;
this.init(user_id,
{
select_button: "upload_photos",
callback: function (file) {
file.aws_id = file.id;
file.id = "0";
file.album_title = "userpics"; // I use this param to manage photo directory
file.user_id = user_id;
//console.log(file);
[** your ajax code here that saves the image object in the database via file variable you get here **]
});
},
add_files_callback: function (up, files, uploader) {
$.each(files, function (index, value) {
// do something like adding a progress bar html
});
uploader.start();
},
complete_callback: function (files) {
can_render = true;
}
}, function (config, msg) {
config.key = "users/" + user_id;
// Most important part:
window.photo_uploader = new Plupload(config, msg.access_key_id, msg.policy, msg.signature, msg.bucket);
});
}
can_render
переменная полезна для того, чтобы вы могли создать приложение только после того, как повторно отрендерили страницу, когда загрузчик на самом деле закончил.
А чтобы кнопка работала откуда-то еще, позвоните:
ImageUploader.photo_uploader(user_id);
И кнопка будет действовать как кнопка загрузчика Plupload. Важно то, что Политика составлена таким образом, чтобы никто не мог загрузить фотографию в чужой каталог. Было бы здорово иметь версию, которая делает то же самое не через обратные вызовы ajax, а с веб-хуками, это то, чем я хочу заниматься в будущем.
Опять же, это не идеальное решение, но, судя по моему опыту, оно работает достаточно хорошо для загрузки изображений и видео на Amazon.
Примечание. Если кто-то спросит, почему у меня такая сложная объектно-ориентированная структура объектов загрузчика, причина в том, что в моем приложении есть все виды загрузчиков, которые ведут себя по-разному, и им нужен инициализатор с обычным поведением. . Как я это сделал, я могу написать инициализатор, скажем, для видео, с минимальным количеством кода, который будет делать то же самое, что и существующий загрузчик изображений.
person
Vitaly Stanchits
schedule
30.09.2015