Skip to content

Commit

Permalink
Support direct uploads to multiple services
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitryTsepelev committed Nov 14, 2021
1 parent a92db0d commit 193289d
Show file tree
Hide file tree
Showing 18 changed files with 433 additions and 151 deletions.
34 changes: 27 additions & 7 deletions actiontext/app/assets/javascripts/actiontext.js
Expand Up @@ -506,14 +506,16 @@ var activestorage = {exports: {}};
}
}
class BlobRecord {
constructor(file, checksum, url) {
constructor(file, checksum, url, directUploadToken, attachmentName) {
this.file = file;
this.attributes = {
filename: file.name,
content_type: file.type || "application/octet-stream",
byte_size: file.size,
checksum: checksum
checksum: checksum,
};
this.directUploadToken = directUploadToken;
this.attachmentName = attachmentName;
this.xhr = new XMLHttpRequest;
this.xhr.open("POST", url, true);
this.xhr.responseType = "json";
Expand Down Expand Up @@ -541,7 +543,9 @@ var activestorage = {exports: {}};
create(callback) {
this.callback = callback;
this.xhr.send(JSON.stringify({
blob: this.attributes
blob: this.attributes,
direct_upload_token: this.directUploadToken,
attachment_name: this.attachmentName
}));
}
requestDidLoad(event) {
Expand Down Expand Up @@ -599,10 +603,12 @@ var activestorage = {exports: {}};
}
let id = 0;
class DirectUpload {
constructor(file, url, delegate) {
constructor(file, url, directUploadToken, attachmentName, delegate) {
this.id = ++id;
this.file = file;
this.url = url;
this.directUploadToken = directUploadToken;
this.attachmentName = attachmentName;
this.delegate = delegate;
}
create(callback) {
Expand All @@ -611,7 +617,7 @@ var activestorage = {exports: {}};
callback(error);
return;
}
const blob = new BlobRecord(this.file, checksum, this.url);
const blob = new BlobRecord(this.file, checksum, this.url, this.directUploadToken, this.attachmentName);
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
blob.create((error => {
if (error) {
Expand Down Expand Up @@ -640,7 +646,7 @@ var activestorage = {exports: {}};
constructor(input, file) {
this.input = input;
this.file = file;
this.directUpload = new DirectUpload(this.file, this.url, this);
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
this.dispatch("initialize");
}
start(callback) {
Expand Down Expand Up @@ -671,6 +677,12 @@ var activestorage = {exports: {}};
get 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 = {}) {
detail.file = this.file;
detail.id = this.directUpload.id;
Expand Down Expand Up @@ -830,7 +842,7 @@ class AttachmentUpload {
constructor(attachment, element) {
this.attachment = attachment;
this.element = element;
this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this);
this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this.directUploadToken, this.directUploadAttachmentName, this);
}

start() {
Expand Down Expand Up @@ -865,6 +877,14 @@ class AttachmentUpload {
return this.element.dataset.directUploadUrl
}

get directUploadToken() {
return this.element.dataset.directUploadToken
}

get directUploadAttachmentName() {
return this.element.dataset.directUploadAttachmentName
}

get blobUrlTemplate() {
return this.element.dataset.blobUrlTemplate
}
Expand Down
8 changes: 8 additions & 0 deletions actiontext/app/helpers/action_text/tag_helper.rb
Expand Up @@ -32,6 +32,14 @@ def rich_text_area_tag(name, value = nil, options = {})
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")

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)
input_tag = hidden_field_tag(name, value.try(:to_trix_html) || value, id: options[:input], form: form)

Expand Down
243 changes: 135 additions & 108 deletions actiontext/test/template/form_helper_test.rb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion actionview/lib/action_view/helpers/form_helper.rb
Expand Up @@ -1220,7 +1220,7 @@ def hidden_field(object_name, method, options = {})
# file_field(:attachment, :file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
def file_field(object_name, method, options = {})
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(method, options.dup)).render
end

# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
Expand Down
18 changes: 16 additions & 2 deletions actionview/lib/action_view/helpers/form_tag_helper.rb
Expand Up @@ -316,7 +316,7 @@ def hidden_field_tag(name, value = nil, options = {})
# 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" />
def file_field_tag(name, options = {})
text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
text_field_tag(name, nil, convert_direct_upload_option_to_url(name, options.merge(type: :file)))
end

# Creates a password field, a masked text field that will hide the users input behind a mask character.
Expand Down Expand Up @@ -954,9 +954,23 @@ def set_default_disable_with(value, tag_options)
tag_options.delete("data-disable-with")
end

def convert_direct_upload_option_to_url(options)
def convert_direct_upload_option_to_url(name, options)
if options.delete(:direct_upload) && respond_to?(: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
options
end
Expand Down
4 changes: 4 additions & 0 deletions activestorage/CHANGELOG.md
@@ -1,3 +1,7 @@
* Support direct uploads to multiple services.

*Dmitry Tsepelev*

* Invalid default content types are deprecated

Blobs created with content_type `image/jpg`, `image/pjpeg`, `image/bmp`, `text/javascript` will now produce
Expand Down
22 changes: 17 additions & 5 deletions activestorage/app/assets/javascripts/activestorage.esm.js
Expand Up @@ -508,14 +508,16 @@ function toArray(value) {
}

class BlobRecord {
constructor(file, checksum, url) {
constructor(file, checksum, url, directUploadToken, attachmentName) {
this.file = file;
this.attributes = {
filename: file.name,
content_type: file.type || "application/octet-stream",
byte_size: file.size,
checksum: checksum
};
this.directUploadToken = directUploadToken;
this.attachmentName = attachmentName;
this.xhr = new XMLHttpRequest;
this.xhr.open("POST", url, true);
this.xhr.responseType = "json";
Expand Down Expand Up @@ -543,7 +545,9 @@ class BlobRecord {
create(callback) {
this.callback = callback;
this.xhr.send(JSON.stringify({
blob: this.attributes
blob: this.attributes,
direct_upload_token: this.directUploadToken,
attachment_name: this.attachmentName
}));
}
requestDidLoad(event) {
Expand Down Expand Up @@ -604,10 +608,12 @@ class BlobUpload {
let id = 0;

class DirectUpload {
constructor(file, url, delegate) {
constructor(file, url, serviceName, attachmentName, delegate) {
this.id = ++id;
this.file = file;
this.url = url;
this.serviceName = serviceName;
this.attachmentName = attachmentName;
this.delegate = delegate;
}
create(callback) {
Expand All @@ -616,7 +622,7 @@ class DirectUpload {
callback(error);
return;
}
const blob = new BlobRecord(this.file, checksum, this.url);
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName);
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
blob.create((error => {
if (error) {
Expand Down Expand Up @@ -647,7 +653,7 @@ class DirectUploadController {
constructor(input, file) {
this.input = input;
this.file = file;
this.directUpload = new DirectUpload(this.file, this.url, this);
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
this.dispatch("initialize");
}
start(callback) {
Expand Down Expand Up @@ -678,6 +684,12 @@ class DirectUploadController {
get 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 = {}) {
detail.file = this.file;
detail.id = this.directUpload.id;
Expand Down
22 changes: 17 additions & 5 deletions activestorage/app/assets/javascripts/activestorage.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -4,8 +4,10 @@
# 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.
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
include ActiveStorage::DirectUploadToken

def create
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args.merge(service_name: verified_service_name))
render json: direct_upload_json(blob)
end

Expand All @@ -14,6 +16,10 @@ def blob_args
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
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)
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
url: blob.service_url_for_direct_upload,
Expand Down
13 changes: 10 additions & 3 deletions activestorage/app/javascript/activestorage/blob_record.js
@@ -1,16 +1,19 @@
import { getMetaValue } from "./helpers"

export class BlobRecord {
constructor(file, checksum, url) {
constructor(file, checksum, url, directUploadToken, attachmentName) {
this.file = file

this.attributes = {
filename: file.name,
content_type: file.type || "application/octet-stream",
byte_size: file.size,
checksum: checksum
checksum: checksum,
}

this.directUploadToken = directUploadToken
this.attachmentName = attachmentName

this.xhr = new XMLHttpRequest
this.xhr.open("POST", url, true)
this.xhr.responseType = "json"
Expand Down Expand Up @@ -43,7 +46,11 @@ export class BlobRecord {

create(callback) {
this.callback = callback
this.xhr.send(JSON.stringify({ blob: this.attributes }))
this.xhr.send(JSON.stringify({
blob: this.attributes,
direct_upload_token: this.directUploadToken,
attachment_name: this.attachmentName
}))
}

requestDidLoad(event) {
Expand Down
6 changes: 4 additions & 2 deletions activestorage/app/javascript/activestorage/direct_upload.js
Expand Up @@ -5,10 +5,12 @@ import { BlobUpload } from "./blob_upload"
let id = 0

export class DirectUpload {
constructor(file, url, delegate) {
constructor(file, url, serviceName, attachmentName, delegate) {
this.id = ++id
this.file = file
this.url = url
this.serviceName = serviceName
this.attachmentName = attachmentName
this.delegate = delegate
}

Expand All @@ -19,7 +21,7 @@ export class DirectUpload {
return
}

const blob = new BlobRecord(this.file, checksum, this.url)
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName)
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)

blob.create(error => {
Expand Down

0 comments on commit 193289d

Please sign in to comment.