mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
* Revert "Pass service_name param to DirectUploadsController" This reverts commit193289dbbe
. * Revert "Multi-service direct uploads in Action Text attachment uploads" This reverts commit0b69ad4de6
.
This commit is contained in:
parent
20846a20dc
commit
aaa64687e8
18 changed files with 152 additions and 437 deletions
|
@ -506,16 +506,14 @@ var activestorage = {exports: {}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class BlobRecord {
|
class BlobRecord {
|
||||||
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
constructor(file, checksum, url) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.attributes = {
|
this.attributes = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
content_type: file.type || "application/octet-stream",
|
content_type: file.type || "application/octet-stream",
|
||||||
byte_size: file.size,
|
byte_size: file.size,
|
||||||
checksum: checksum,
|
checksum: checksum
|
||||||
};
|
};
|
||||||
this.directUploadToken = directUploadToken;
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
this.xhr = new XMLHttpRequest;
|
this.xhr = new XMLHttpRequest;
|
||||||
this.xhr.open("POST", url, true);
|
this.xhr.open("POST", url, true);
|
||||||
this.xhr.responseType = "json";
|
this.xhr.responseType = "json";
|
||||||
|
@ -543,9 +541,7 @@ var activestorage = {exports: {}};
|
||||||
create(callback) {
|
create(callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.xhr.send(JSON.stringify({
|
this.xhr.send(JSON.stringify({
|
||||||
blob: this.attributes,
|
blob: this.attributes
|
||||||
direct_upload_token: this.directUploadToken,
|
|
||||||
attachment_name: this.attachmentName
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
requestDidLoad(event) {
|
requestDidLoad(event) {
|
||||||
|
@ -603,12 +599,10 @@ var activestorage = {exports: {}};
|
||||||
}
|
}
|
||||||
let id = 0;
|
let id = 0;
|
||||||
class DirectUpload {
|
class DirectUpload {
|
||||||
constructor(file, url, directUploadToken, attachmentName, delegate) {
|
constructor(file, url, delegate) {
|
||||||
this.id = ++id;
|
this.id = ++id;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.directUploadToken = directUploadToken;
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
}
|
}
|
||||||
create(callback) {
|
create(callback) {
|
||||||
|
@ -617,7 +611,7 @@ var activestorage = {exports: {}};
|
||||||
callback(error);
|
callback(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = new BlobRecord(this.file, checksum, this.url, this.directUploadToken, this.attachmentName);
|
const blob = new BlobRecord(this.file, checksum, this.url);
|
||||||
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
||||||
blob.create((error => {
|
blob.create((error => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -646,7 +640,7 @@ var activestorage = {exports: {}};
|
||||||
constructor(input, file) {
|
constructor(input, file) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
|
this.directUpload = new DirectUpload(this.file, this.url, this);
|
||||||
this.dispatch("initialize");
|
this.dispatch("initialize");
|
||||||
}
|
}
|
||||||
start(callback) {
|
start(callback) {
|
||||||
|
@ -677,12 +671,6 @@ var activestorage = {exports: {}};
|
||||||
get url() {
|
get url() {
|
||||||
return this.input.getAttribute("data-direct-upload-url");
|
return this.input.getAttribute("data-direct-upload-url");
|
||||||
}
|
}
|
||||||
get directUploadToken() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-token");
|
|
||||||
}
|
|
||||||
get attachmentName() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-attachment-name");
|
|
||||||
}
|
|
||||||
dispatch(name, detail = {}) {
|
dispatch(name, detail = {}) {
|
||||||
detail.file = this.file;
|
detail.file = this.file;
|
||||||
detail.id = this.directUpload.id;
|
detail.id = this.directUpload.id;
|
||||||
|
@ -842,7 +830,7 @@ class AttachmentUpload {
|
||||||
constructor(attachment, element) {
|
constructor(attachment, element) {
|
||||||
this.attachment = attachment;
|
this.attachment = attachment;
|
||||||
this.element = element;
|
this.element = element;
|
||||||
this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this.directUploadToken, this.directUploadAttachmentName, this);
|
this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
@ -877,14 +865,6 @@ class AttachmentUpload {
|
||||||
return this.element.dataset.directUploadUrl
|
return this.element.dataset.directUploadUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
get directUploadToken() {
|
|
||||||
return this.element.dataset.directUploadToken
|
|
||||||
}
|
|
||||||
|
|
||||||
get directUploadAttachmentName() {
|
|
||||||
return this.element.dataset.directUploadAttachmentName
|
|
||||||
}
|
|
||||||
|
|
||||||
get blobUrlTemplate() {
|
get blobUrlTemplate() {
|
||||||
return this.element.dataset.blobUrlTemplate
|
return this.element.dataset.blobUrlTemplate
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,14 +32,6 @@ module ActionText
|
||||||
options[:data][:direct_upload_url] ||= main_app.rails_direct_uploads_url
|
options[:data][:direct_upload_url] ||= main_app.rails_direct_uploads_url
|
||||||
options[:data][:blob_url_template] ||= main_app.rails_service_blob_url(":signed_id", ":filename")
|
options[:data][:blob_url_template] ||= main_app.rails_service_blob_url(":signed_id", ":filename")
|
||||||
|
|
||||||
class_with_attachment = "ActionText::RichText#embeds"
|
|
||||||
options[:data][:direct_upload_attachment_name] ||= class_with_attachment
|
|
||||||
options[:data][:direct_upload_token] = ActiveStorage::DirectUploadToken.generate_direct_upload_token(
|
|
||||||
class_with_attachment,
|
|
||||||
ActiveStorage::Blob.service.name,
|
|
||||||
session
|
|
||||||
)
|
|
||||||
|
|
||||||
editor_tag = content_tag("trix-editor", "", options)
|
editor_tag = content_tag("trix-editor", "", options)
|
||||||
input_tag = hidden_field_tag(name, value.try(:to_trix_html) || value, id: options[:input], form: form)
|
input_tag = hidden_field_tag(name, value.try(:to_trix_html) || value, id: options[:input], form: form)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ export class AttachmentUpload {
|
||||||
constructor(attachment, element) {
|
constructor(attachment, element) {
|
||||||
this.attachment = attachment
|
this.attachment = attachment
|
||||||
this.element = element
|
this.element = element
|
||||||
this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this.directUploadToken, this.attachmentName, this)
|
this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
@ -42,11 +42,4 @@ export class AttachmentUpload {
|
||||||
get blobUrlTemplate() {
|
get blobUrlTemplate() {
|
||||||
return this.element.dataset.blobUrlTemplate
|
return this.element.dataset.blobUrlTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
get directUploadToken() {
|
|
||||||
return this.element.getAttribute("data-direct-upload-token");
|
|
||||||
}
|
|
||||||
get attachmentName() {
|
|
||||||
return this.element.getAttribute("data-direct-upload-attachment-name");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
require "minitest/mock"
|
|
||||||
|
|
||||||
class ActionText::FormHelperTest < ActionView::TestCase
|
class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
tests ActionText::TagHelper
|
tests ActionText::TagHelper
|
||||||
|
@ -29,7 +28,6 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
test "rich text area tag" do
|
test "rich text area tag" do
|
||||||
message = Message.new
|
message = Message.new
|
||||||
|
|
||||||
with_stub_token do
|
|
||||||
form_with model: message, scope: :message do |form|
|
form_with model: message, scope: :message do |form|
|
||||||
rich_text_area_tag :content, message.content, { input: "trix_input_1" }
|
rich_text_area_tag :content, message.content, { input: "trix_input_1" }
|
||||||
end
|
end
|
||||||
|
@ -37,15 +35,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="content" id="trix_input_1" autocomplete="off" />' \
|
'<input type="hidden" name="content" id="trix_input_1" autocomplete="off" />' \
|
||||||
'<trix-editor input="trix_input_1" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor input="trix_input_1" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area" do
|
test "form with rich text area" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :content
|
form.rich_text_area :content
|
||||||
end
|
end
|
||||||
|
@ -53,15 +49,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area having class" do
|
test "form with rich text area having class" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :content, class: "custom-class"
|
form.rich_text_area :content, class: "custom-class"
|
||||||
end
|
end
|
||||||
|
@ -69,15 +63,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_content" input="message_content_trix_input_message" class="custom-class" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_content" input="message_content_trix_input_message" class="custom-class" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area for non-attribute" do
|
test "form with rich text area for non-attribute" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :not_an_attribute
|
form.rich_text_area :not_an_attribute
|
||||||
end
|
end
|
||||||
|
@ -85,15 +77,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[not_an_attribute]" id="message_not_an_attribute_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[not_an_attribute]" id="message_not_an_attribute_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_not_an_attribute" input="message_not_an_attribute_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_not_an_attribute" input="message_not_an_attribute_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "modelless form with rich text area" do
|
test "modelless form with rich text area" do
|
||||||
with_stub_token do
|
|
||||||
form_with url: "/messages", scope: :message do |form|
|
form_with url: "/messages", scope: :message do |form|
|
||||||
form.rich_text_area :content, { input: "trix_input_2" }
|
form.rich_text_area :content, { input: "trix_input_2" }
|
||||||
end
|
end
|
||||||
|
@ -101,15 +91,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[content]" id="trix_input_2" autocomplete="off" />' \
|
'<input type="hidden" name="message[content]" id="trix_input_2" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_content" input="trix_input_2" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_content" input="trix_input_2" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area having placeholder without locale" do
|
test "form with rich text area having placeholder without locale" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :content, placeholder: true
|
form.rich_text_area :content, placeholder: true
|
||||||
end
|
end
|
||||||
|
@ -117,17 +105,15 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor placeholder="Content" id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor placeholder="Content" id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area having placeholder with locale" do
|
test "form with rich text area having placeholder with locale" do
|
||||||
I18n.with_locale :placeholder do
|
I18n.with_locale :placeholder do
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
with_stub_token do
|
|
||||||
form.rich_text_area :title, placeholder: true
|
form.rich_text_area :title, placeholder: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -135,15 +121,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[title]" id="message_title_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[title]" id="message_title_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor placeholder="Story title" id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor placeholder="Story title" id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area with value" do
|
test "form with rich text area with value" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :title, value: "<h1>hello world</h1>"
|
form.rich_text_area :title, value: "<h1>hello world</h1>"
|
||||||
end
|
end
|
||||||
|
@ -151,15 +135,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[title]" id="message_title_trix_input_message" value="<h1>hello world</h1>" autocomplete="off" />' \
|
'<input type="hidden" name="message[title]" id="message_title_trix_input_message" value="<h1>hello world</h1>" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area with form attribute" do
|
test "form with rich text area with form attribute" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :title, form: "other_form"
|
form.rich_text_area :title, form: "other_form"
|
||||||
end
|
end
|
||||||
|
@ -167,15 +149,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[title]" id="message_title_trix_input_message" form="other_form" autocomplete="off" />' \
|
'<input type="hidden" name="message[title]" id="message_title_trix_input_message" form="other_form" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area with data[direct_upload_url]" do
|
test "form with rich text area with data[direct_upload_url]" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :content, data: { direct_upload_url: "http://test.host/direct_uploads" }
|
form.rich_text_area :content, data: { direct_upload_url: "http://test.host/direct_uploads" }
|
||||||
end
|
end
|
||||||
|
@ -183,15 +163,13 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/redirect/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "form with rich text area with data[blob_url_template]" do
|
test "form with rich text area with data[blob_url_template]" do
|
||||||
with_stub_token do
|
|
||||||
form_with model: Message.new, scope: :message do |form|
|
form_with model: Message.new, scope: :message do |form|
|
||||||
form.rich_text_area :content, data: { blob_url_template: "http://test.host/blobs/:signed_id/:filename" }
|
form.rich_text_area :content, data: { blob_url_template: "http://test.host/blobs/:signed_id/:filename" }
|
||||||
end
|
end
|
||||||
|
@ -199,14 +177,9 @@ class ActionText::FormHelperTest < ActionView::TestCase
|
||||||
assert_dom_equal \
|
assert_dom_equal \
|
||||||
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
'<form action="/messages" accept-charset="UTF-8" method="post">' \
|
||||||
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
'<input type="hidden" name="message[content]" id="message_content_trix_input_message" autocomplete="off" />' \
|
||||||
'<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/blobs/:signed_id/:filename" data-direct-upload-attachment-name="ActionText::RichText#embeds" data-direct-upload-token="token">' \
|
'<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/blobs/:signed_id/:filename">' \
|
||||||
"</trix-editor>" \
|
"</trix-editor>" \
|
||||||
"</form>",
|
"</form>",
|
||||||
output_buffer
|
output_buffer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_stub_token(&block)
|
|
||||||
ActiveStorage::DirectUploadToken.stub(:generate_direct_upload_token, "token", &block)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -1241,7 +1241,7 @@ module ActionView
|
||||||
def file_field(object_name, method, options = {})
|
def file_field(object_name, method, options = {})
|
||||||
options = { include_hidden: multiple_file_field_include_hidden }.merge!(options)
|
options = { include_hidden: multiple_file_field_include_hidden }.merge!(options)
|
||||||
|
|
||||||
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(method, options)).render
|
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
|
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
|
||||||
|
|
|
@ -342,7 +342,7 @@ module ActionView
|
||||||
# file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
|
# file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
|
||||||
# # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
|
# # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
|
||||||
def file_field_tag(name, options = {})
|
def file_field_tag(name, options = {})
|
||||||
text_field_tag(name, nil, convert_direct_upload_option_to_url(name, options.merge(type: :file)))
|
text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates a password field, a masked text field that will hide the users input behind a mask character.
|
# Creates a password field, a masked text field that will hide the users input behind a mask character.
|
||||||
|
@ -984,23 +984,9 @@ module ActionView
|
||||||
tag_options.delete("data-disable-with")
|
tag_options.delete("data-disable-with")
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert_direct_upload_option_to_url(name, options)
|
def convert_direct_upload_option_to_url(options)
|
||||||
if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
|
if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
|
||||||
options["data-direct-upload-url"] = rails_direct_uploads_url
|
options["data-direct-upload-url"] = rails_direct_uploads_url
|
||||||
|
|
||||||
if options[:object] && options[:object].class.respond_to?(:reflect_on_attachment)
|
|
||||||
attachment_reflection = options[:object].class.reflect_on_attachment(name)
|
|
||||||
|
|
||||||
class_with_attachment = "#{options[:object].class.name.underscore}##{name}"
|
|
||||||
options["data-direct-upload-attachment-name"] = class_with_attachment
|
|
||||||
|
|
||||||
service_name = attachment_reflection.options[:service_name] || ActiveStorage::Blob.service.name
|
|
||||||
options["data-direct-upload-token"] = ActiveStorage::DirectUploadToken.generate_direct_upload_token(
|
|
||||||
class_with_attachment,
|
|
||||||
service_name,
|
|
||||||
session
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
options
|
options
|
||||||
end
|
end
|
||||||
|
|
|
@ -508,7 +508,7 @@ function toArray(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class BlobRecord {
|
class BlobRecord {
|
||||||
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
constructor(file, checksum, url) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.attributes = {
|
this.attributes = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
|
@ -516,8 +516,6 @@ class BlobRecord {
|
||||||
byte_size: file.size,
|
byte_size: file.size,
|
||||||
checksum: checksum
|
checksum: checksum
|
||||||
};
|
};
|
||||||
this.directUploadToken = directUploadToken;
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
this.xhr = new XMLHttpRequest;
|
this.xhr = new XMLHttpRequest;
|
||||||
this.xhr.open("POST", url, true);
|
this.xhr.open("POST", url, true);
|
||||||
this.xhr.responseType = "json";
|
this.xhr.responseType = "json";
|
||||||
|
@ -545,9 +543,7 @@ class BlobRecord {
|
||||||
create(callback) {
|
create(callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.xhr.send(JSON.stringify({
|
this.xhr.send(JSON.stringify({
|
||||||
blob: this.attributes,
|
blob: this.attributes
|
||||||
direct_upload_token: this.directUploadToken,
|
|
||||||
attachment_name: this.attachmentName
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
requestDidLoad(event) {
|
requestDidLoad(event) {
|
||||||
|
@ -608,12 +604,10 @@ class BlobUpload {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
class DirectUpload {
|
class DirectUpload {
|
||||||
constructor(file, url, serviceName, attachmentName, delegate) {
|
constructor(file, url, delegate) {
|
||||||
this.id = ++id;
|
this.id = ++id;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.serviceName = serviceName;
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
}
|
}
|
||||||
create(callback) {
|
create(callback) {
|
||||||
|
@ -622,7 +616,7 @@ class DirectUpload {
|
||||||
callback(error);
|
callback(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName);
|
const blob = new BlobRecord(this.file, checksum, this.url);
|
||||||
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
||||||
blob.create((error => {
|
blob.create((error => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -653,7 +647,7 @@ class DirectUploadController {
|
||||||
constructor(input, file) {
|
constructor(input, file) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
|
this.directUpload = new DirectUpload(this.file, this.url, this);
|
||||||
this.dispatch("initialize");
|
this.dispatch("initialize");
|
||||||
}
|
}
|
||||||
start(callback) {
|
start(callback) {
|
||||||
|
@ -684,12 +678,6 @@ class DirectUploadController {
|
||||||
get url() {
|
get url() {
|
||||||
return this.input.getAttribute("data-direct-upload-url");
|
return this.input.getAttribute("data-direct-upload-url");
|
||||||
}
|
}
|
||||||
get directUploadToken() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-token");
|
|
||||||
}
|
|
||||||
get attachmentName() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-attachment-name");
|
|
||||||
}
|
|
||||||
dispatch(name, detail = {}) {
|
dispatch(name, detail = {}) {
|
||||||
detail.file = this.file;
|
detail.file = this.file;
|
||||||
detail.id = this.directUpload.id;
|
detail.id = this.directUpload.id;
|
||||||
|
|
|
@ -503,7 +503,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class BlobRecord {
|
class BlobRecord {
|
||||||
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
constructor(file, checksum, url) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.attributes = {
|
this.attributes = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
|
@ -511,8 +511,6 @@
|
||||||
byte_size: file.size,
|
byte_size: file.size,
|
||||||
checksum: checksum
|
checksum: checksum
|
||||||
};
|
};
|
||||||
this.directUploadToken = directUploadToken;
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
this.xhr = new XMLHttpRequest;
|
this.xhr = new XMLHttpRequest;
|
||||||
this.xhr.open("POST", url, true);
|
this.xhr.open("POST", url, true);
|
||||||
this.xhr.responseType = "json";
|
this.xhr.responseType = "json";
|
||||||
|
@ -540,9 +538,7 @@
|
||||||
create(callback) {
|
create(callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.xhr.send(JSON.stringify({
|
this.xhr.send(JSON.stringify({
|
||||||
blob: this.attributes,
|
blob: this.attributes
|
||||||
direct_upload_token: this.directUploadToken,
|
|
||||||
attachment_name: this.attachmentName
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
requestDidLoad(event) {
|
requestDidLoad(event) {
|
||||||
|
@ -600,12 +596,10 @@
|
||||||
}
|
}
|
||||||
let id = 0;
|
let id = 0;
|
||||||
class DirectUpload {
|
class DirectUpload {
|
||||||
constructor(file, url, serviceName, attachmentName, delegate) {
|
constructor(file, url, delegate) {
|
||||||
this.id = ++id;
|
this.id = ++id;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.serviceName = serviceName;
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
}
|
}
|
||||||
create(callback) {
|
create(callback) {
|
||||||
|
@ -614,7 +608,7 @@
|
||||||
callback(error);
|
callback(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName);
|
const blob = new BlobRecord(this.file, checksum, this.url);
|
||||||
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
||||||
blob.create((error => {
|
blob.create((error => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -643,7 +637,7 @@
|
||||||
constructor(input, file) {
|
constructor(input, file) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
|
this.directUpload = new DirectUpload(this.file, this.url, this);
|
||||||
this.dispatch("initialize");
|
this.dispatch("initialize");
|
||||||
}
|
}
|
||||||
start(callback) {
|
start(callback) {
|
||||||
|
@ -674,12 +668,6 @@
|
||||||
get url() {
|
get url() {
|
||||||
return this.input.getAttribute("data-direct-upload-url");
|
return this.input.getAttribute("data-direct-upload-url");
|
||||||
}
|
}
|
||||||
get directUploadToken() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-token");
|
|
||||||
}
|
|
||||||
get attachmentName() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-attachment-name");
|
|
||||||
}
|
|
||||||
dispatch(name, detail = {}) {
|
dispatch(name, detail = {}) {
|
||||||
detail.file = this.file;
|
detail.file = this.file;
|
||||||
detail.id = this.directUpload.id;
|
detail.id = this.directUpload.id;
|
||||||
|
|
|
@ -4,10 +4,8 @@
|
||||||
# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
|
# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
|
||||||
# the blob that was created up front.
|
# the blob that was created up front.
|
||||||
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
||||||
include ActiveStorage::DirectUploadToken
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args.merge(service_name: verified_service_name))
|
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
|
||||||
render json: direct_upload_json(blob)
|
render json: direct_upload_json(blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -16,10 +14,6 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
||||||
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
|
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def verified_service_name
|
|
||||||
ActiveStorage::DirectUploadToken.verify_direct_upload_token(params[:direct_upload_token], params[:attachment_name], session)
|
|
||||||
end
|
|
||||||
|
|
||||||
def direct_upload_json(blob)
|
def direct_upload_json(blob)
|
||||||
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
|
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
|
||||||
url: blob.service_url_for_direct_upload,
|
url: blob.service_url_for_direct_upload,
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import { getMetaValue } from "./helpers"
|
import { getMetaValue } from "./helpers"
|
||||||
|
|
||||||
export class BlobRecord {
|
export class BlobRecord {
|
||||||
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
constructor(file, checksum, url) {
|
||||||
this.file = file
|
this.file = file
|
||||||
|
|
||||||
this.attributes = {
|
this.attributes = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
content_type: file.type || "application/octet-stream",
|
content_type: file.type || "application/octet-stream",
|
||||||
byte_size: file.size,
|
byte_size: file.size,
|
||||||
checksum: checksum,
|
checksum: checksum
|
||||||
}
|
}
|
||||||
|
|
||||||
this.directUploadToken = directUploadToken
|
|
||||||
this.attachmentName = attachmentName
|
|
||||||
|
|
||||||
this.xhr = new XMLHttpRequest
|
this.xhr = new XMLHttpRequest
|
||||||
this.xhr.open("POST", url, true)
|
this.xhr.open("POST", url, true)
|
||||||
this.xhr.responseType = "json"
|
this.xhr.responseType = "json"
|
||||||
|
@ -46,11 +43,7 @@ export class BlobRecord {
|
||||||
|
|
||||||
create(callback) {
|
create(callback) {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
this.xhr.send(JSON.stringify({
|
this.xhr.send(JSON.stringify({ blob: this.attributes }))
|
||||||
blob: this.attributes,
|
|
||||||
direct_upload_token: this.directUploadToken,
|
|
||||||
attachment_name: this.attachmentName
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestDidLoad(event) {
|
requestDidLoad(event) {
|
||||||
|
|
|
@ -5,12 +5,10 @@ import { BlobUpload } from "./blob_upload"
|
||||||
let id = 0
|
let id = 0
|
||||||
|
|
||||||
export class DirectUpload {
|
export class DirectUpload {
|
||||||
constructor(file, url, serviceName, attachmentName, delegate) {
|
constructor(file, url, delegate) {
|
||||||
this.id = ++id
|
this.id = ++id
|
||||||
this.file = file
|
this.file = file
|
||||||
this.url = url
|
this.url = url
|
||||||
this.serviceName = serviceName
|
|
||||||
this.attachmentName = attachmentName
|
|
||||||
this.delegate = delegate
|
this.delegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +19,7 @@ export class DirectUpload {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName)
|
const blob = new BlobRecord(this.file, checksum, this.url)
|
||||||
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
||||||
|
|
||||||
blob.create(error => {
|
blob.create(error => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ export class DirectUploadController {
|
||||||
constructor(input, file) {
|
constructor(input, file) {
|
||||||
this.input = input
|
this.input = input
|
||||||
this.file = file
|
this.file = file
|
||||||
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this)
|
this.directUpload = new DirectUpload(this.file, this.url, this)
|
||||||
this.dispatch("initialize")
|
this.dispatch("initialize")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,14 +41,6 @@ export class DirectUploadController {
|
||||||
return this.input.getAttribute("data-direct-upload-url")
|
return this.input.getAttribute("data-direct-upload-url")
|
||||||
}
|
}
|
||||||
|
|
||||||
get directUploadToken() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-token")
|
|
||||||
}
|
|
||||||
|
|
||||||
get attachmentName() {
|
|
||||||
return this.input.getAttribute("data-direct-upload-attachment-name")
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(name, detail = {}) {
|
dispatch(name, detail = {}) {
|
||||||
detail.file = this.file
|
detail.file = this.file
|
||||||
detail.id = this.directUpload.id
|
detail.id = this.directUpload.id
|
||||||
|
|
|
@ -41,7 +41,6 @@ module ActiveStorage
|
||||||
autoload :Service
|
autoload :Service
|
||||||
autoload :Previewer
|
autoload :Previewer
|
||||||
autoload :Analyzer
|
autoload :Analyzer
|
||||||
autoload :DirectUploadToken
|
|
||||||
|
|
||||||
mattr_accessor :logger
|
mattr_accessor :logger
|
||||||
mattr_accessor :verifier
|
mattr_accessor :verifier
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module ActiveStorage
|
|
||||||
module DirectUploadToken
|
|
||||||
extend self
|
|
||||||
|
|
||||||
SEPARATOR = "."
|
|
||||||
DIRECT_UPLOAD_TOKEN_LENGTH = 32
|
|
||||||
|
|
||||||
def generate_direct_upload_token(attachment_name, service_name, session)
|
|
||||||
token = direct_upload_token(session, attachment_name)
|
|
||||||
encode_direct_upload_token([service_name, token].join(SEPARATOR))
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_direct_upload_token(token, attachment_name, session)
|
|
||||||
raise ActiveStorage::InvalidDirectUploadTokenError if token.nil?
|
|
||||||
|
|
||||||
service_name, *token_components = decode_token(token).split(SEPARATOR)
|
|
||||||
decoded_token = token_components.join(SEPARATOR)
|
|
||||||
|
|
||||||
return service_name if valid_direct_upload_token?(decoded_token, attachment_name, session)
|
|
||||||
|
|
||||||
raise ActiveStorage::InvalidDirectUploadTokenError
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def direct_upload_token(session, attachment_name) # :doc:
|
|
||||||
direct_upload_token_hmac(session, "direct_upload##{attachment_name}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_direct_upload_token?(token, attachment_name, session) # :doc:
|
|
||||||
correct_token = direct_upload_token(session, attachment_name)
|
|
||||||
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
|
|
||||||
rescue ArgumentError
|
|
||||||
raise ActiveStorage::InvalidDirectUploadTokenError
|
|
||||||
end
|
|
||||||
|
|
||||||
def direct_upload_token_hmac(session, identifier) # :doc:
|
|
||||||
OpenSSL::HMAC.digest(
|
|
||||||
OpenSSL::Digest::SHA256.new,
|
|
||||||
real_direct_upload_token(session),
|
|
||||||
identifier
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def real_direct_upload_token(session) # :doc:
|
|
||||||
session[:_direct_upload_token] ||= SecureRandom.urlsafe_base64(DIRECT_UPLOAD_TOKEN_LENGTH, padding: false)
|
|
||||||
encode_direct_upload_token(session[:_direct_upload_token])
|
|
||||||
end
|
|
||||||
|
|
||||||
def decode_token(encoded_token) # :nodoc:
|
|
||||||
Base64.urlsafe_decode64(encoded_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def encode_direct_upload_token(raw_token) # :nodoc:
|
|
||||||
Base64.urlsafe_encode64(raw_token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -26,7 +26,4 @@ module ActiveStorage
|
||||||
|
|
||||||
# Raised when a Previewer is unable to generate a preview image.
|
# Raised when a Previewer is unable to generate a preview image.
|
||||||
class PreviewError < Error; end
|
class PreviewError < Error; end
|
||||||
|
|
||||||
# Raised when direct upload fails because of the invalid token
|
|
||||||
class InvalidDirectUploadTokenError < Error; end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
require "database/setup"
|
require "database/setup"
|
||||||
require "minitest/mock"
|
|
||||||
|
|
||||||
if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
|
if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
|
||||||
class ActiveStorage::S3DirectUploadsControllerTest < ActionDispatch::IntegrationTest
|
class ActiveStorage::S3DirectUploadsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
@ -28,10 +27,8 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveStorage::DirectUploadToken.stub(:verify_direct_upload_token, "s3") do
|
|
||||||
post rails_direct_uploads_url, params: { blob: {
|
post rails_direct_uploads_url, params: { blob: {
|
||||||
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
||||||
end
|
|
||||||
|
|
||||||
response.parsed_body.tap do |details|
|
response.parsed_body.tap do |details|
|
||||||
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
||||||
|
@ -76,10 +73,8 @@ if SERVICE_CONFIGURATIONS[:gcs]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveStorage::DirectUploadToken.stub(:verify_direct_upload_token, "gcs") do
|
|
||||||
post rails_direct_uploads_url, params: { blob: {
|
post rails_direct_uploads_url, params: { blob: {
|
||||||
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
||||||
end
|
|
||||||
|
|
||||||
@response.parsed_body.tap do |details|
|
@response.parsed_body.tap do |details|
|
||||||
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
||||||
|
@ -120,10 +115,8 @@ if SERVICE_CONFIGURATIONS[:azure]
|
||||||
"library_ID": "12345"
|
"library_ID": "12345"
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveStorage::DirectUploadToken.stub(:verify_direct_upload_token, "azure") do
|
|
||||||
post rails_direct_uploads_url, params: { blob: {
|
post rails_direct_uploads_url, params: { blob: {
|
||||||
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
||||||
end
|
|
||||||
|
|
||||||
@response.parsed_body.tap do |details|
|
@response.parsed_body.tap do |details|
|
||||||
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
||||||
|
@ -152,10 +145,8 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
|
||||||
"library_ID": "12345"
|
"library_ID": "12345"
|
||||||
}
|
}
|
||||||
|
|
||||||
with_valid_service_name do
|
|
||||||
post rails_direct_uploads_url, params: { blob: {
|
post rails_direct_uploads_url, params: { blob: {
|
||||||
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
||||||
end
|
|
||||||
|
|
||||||
@response.parsed_body.tap do |details|
|
@response.parsed_body.tap do |details|
|
||||||
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
||||||
|
@ -179,12 +170,10 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
|
||||||
"library_ID": "12345"
|
"library_ID": "12345"
|
||||||
}
|
}
|
||||||
|
|
||||||
with_valid_service_name do
|
|
||||||
set_include_root_in_json(true) do
|
set_include_root_in_json(true) do
|
||||||
post rails_direct_uploads_url, params: { blob: {
|
post rails_direct_uploads_url, params: { blob: {
|
||||||
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
@response.parsed_body.tap do |details|
|
@response.parsed_body.tap do |details|
|
||||||
assert_nil details["blob"]
|
assert_nil details["blob"]
|
||||||
|
@ -192,33 +181,6 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handling direct upload with custom service name" do
|
|
||||||
checksum = OpenSSL::Digest::MD5.base64digest("Hello")
|
|
||||||
metadata = {
|
|
||||||
"foo": "bar",
|
|
||||||
"my_key_1": "my_value_1",
|
|
||||||
"my_key_2": "my_value_2",
|
|
||||||
"platform": "my_platform",
|
|
||||||
"library_ID": "12345"
|
|
||||||
}
|
|
||||||
|
|
||||||
with_valid_service_name do
|
|
||||||
post rails_direct_uploads_url, params: { blob: {
|
|
||||||
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
|
|
||||||
end
|
|
||||||
|
|
||||||
@response.parsed_body.tap do |details|
|
|
||||||
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
|
|
||||||
assert_equal "hello.txt", details["filename"]
|
|
||||||
assert_equal 6, details["byte_size"]
|
|
||||||
assert_equal checksum, details["checksum"]
|
|
||||||
assert_equal metadata, details["metadata"].deep_transform_keys(&:to_sym)
|
|
||||||
assert_equal "text/plain", details["content_type"]
|
|
||||||
assert_match(/rails\/active_storage\/disk/, details["direct_upload"]["url"])
|
|
||||||
assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def set_include_root_in_json(value)
|
def set_include_root_in_json(value)
|
||||||
original = ActiveRecord::Base.include_root_in_json
|
original = ActiveRecord::Base.include_root_in_json
|
||||||
|
@ -227,8 +189,4 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
|
||||||
ensure
|
ensure
|
||||||
ActiveRecord::Base.include_root_in_json = original
|
ActiveRecord::Base.include_root_in_json = original
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_valid_service_name(&block)
|
|
||||||
ActiveStorage::DirectUploadToken.stub(:verify_direct_upload_token, "local", &block)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "test_helper"
|
|
||||||
|
|
||||||
class DirectUploadTokenTest < ActionController::TestCase
|
|
||||||
setup do
|
|
||||||
@session = {}
|
|
||||||
@service_name = "local"
|
|
||||||
@avatar_attachment_name = "user#avatar"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_validates_correct_token
|
|
||||||
token = ActiveStorage::DirectUploadToken.generate_direct_upload_token(@avatar_attachment_name, @service_name, @session)
|
|
||||||
verified_service_name = ActiveStorage::DirectUploadToken.verify_direct_upload_token(token, @avatar_attachment_name, @session)
|
|
||||||
|
|
||||||
assert_equal verified_service_name, @service_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_not_validates_nil_token
|
|
||||||
token = nil
|
|
||||||
|
|
||||||
assert_raises(ActiveStorage::InvalidDirectUploadTokenError) do
|
|
||||||
ActiveStorage::DirectUploadToken.verify_direct_upload_token(token, @avatar_attachment_name, @session)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_not_validates_token_when_session_is_empty
|
|
||||||
token = ActiveStorage::DirectUploadToken.generate_direct_upload_token(@avatar_attachment_name, @service_name, {})
|
|
||||||
|
|
||||||
assert_raises(ActiveStorage::InvalidDirectUploadTokenError) do
|
|
||||||
ActiveStorage::DirectUploadToken.verify_direct_upload_token(token, @avatar_attachment_name, @session)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_not_validates_token_from_different_attachment
|
|
||||||
background_attachment_name = "user#background"
|
|
||||||
token = ActiveStorage::DirectUploadToken.generate_direct_upload_token(background_attachment_name, @service_name, @session)
|
|
||||||
|
|
||||||
assert_raises(ActiveStorage::InvalidDirectUploadTokenError) do
|
|
||||||
ActiveStorage::DirectUploadToken.verify_direct_upload_token(token, @avatar_attachment_name, @session)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_not_validates_token_from_different_session
|
|
||||||
token = ActiveStorage::DirectUploadToken.generate_direct_upload_token(@avatar_attachment_name, @service_name, @session)
|
|
||||||
|
|
||||||
another_session = {}
|
|
||||||
ActiveStorage::DirectUploadToken.generate_direct_upload_token(@avatar_attachment_name, @service_name, another_session)
|
|
||||||
|
|
||||||
assert_raises(ActiveStorage::InvalidDirectUploadTokenError) do
|
|
||||||
ActiveStorage::DirectUploadToken.verify_direct_upload_token(token, @avatar_attachment_name, another_session)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1146,12 +1146,9 @@ input.addEventListener('change', (event) => {
|
||||||
|
|
||||||
const uploadFile = (file) => {
|
const uploadFile = (file) => {
|
||||||
// your form needs the file_field direct_upload: true, which
|
// your form needs the file_field direct_upload: true, which
|
||||||
// provides data-direct-upload-url, data-direct-upload-token
|
// provides data-direct-upload-url
|
||||||
// and data-direct-upload-attachment-name
|
|
||||||
const url = input.dataset.directUploadUrl
|
const url = input.dataset.directUploadUrl
|
||||||
const token = input.dataset.directUploadToken
|
const upload = new DirectUpload(file, url)
|
||||||
const attachmentName = input.dataset.directUploadAttachmentName
|
|
||||||
const upload = new DirectUpload(file, url, token, attachmentName)
|
|
||||||
|
|
||||||
upload.create((error, blob) => {
|
upload.create((error, blob) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -1170,7 +1167,7 @@ const uploadFile = (file) => {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you need to track the progress of the file upload, you can pass a fifth
|
If you need to track the progress of the file upload, you can pass a third
|
||||||
parameter to the `DirectUpload` constructor. During the upload, DirectUpload
|
parameter to the `DirectUpload` constructor. During the upload, DirectUpload
|
||||||
will call the object's `directUploadWillStoreFileWithXHR` method. You can then
|
will call the object's `directUploadWillStoreFileWithXHR` method. You can then
|
||||||
bind your own progress handler on the XHR.
|
bind your own progress handler on the XHR.
|
||||||
|
@ -1179,8 +1176,8 @@ bind your own progress handler on the XHR.
|
||||||
import { DirectUpload } from "@rails/activestorage"
|
import { DirectUpload } from "@rails/activestorage"
|
||||||
|
|
||||||
class Uploader {
|
class Uploader {
|
||||||
constructor(file, url, token, attachmentName) {
|
constructor(file, url) {
|
||||||
this.upload = new DirectUpload(file, url, token, attachmentName, this)
|
this.upload = new DirectUpload(this.file, this.url, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
upload(file) {
|
upload(file) {
|
||||||
|
|
Loading…
Reference in a new issue