activestorage-delayed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +140 -0
- data/Rakefile +5 -0
- data/lib/activestorage-delayed/delayed_concern.rb +37 -0
- data/lib/activestorage-delayed/delayed_uploader.rb +93 -0
- data/lib/activestorage-delayed/delayed_uploader_job.rb +11 -0
- data/lib/activestorage-delayed/models/delayed_upload.rb +32 -0
- data/lib/activestorage-delayed/railtie.rb +12 -0
- data/lib/activestorage-delayed/version.rb +5 -0
- data/lib/activestorage_delayed.rb +13 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d45bf7babcb289d33cb76078e9e944be721fd42ebd7b28f4bd1ca60fee81c148
|
4
|
+
data.tar.gz: 933186d50d21a26346941e7253f5fdc2b15c194b049411054a2f10d825d52bf2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 766911783743811c91ac4b7aeb7161783d17128a3ad626321d8220b53862e2182ec5b278fb7f1c3827a2dc3ec9acf30eddc58a05dc1d32280edfd03404b8b82a
|
7
|
+
data.tar.gz: aa629632c162e3f9b4910242ab89422ee95aca3c6ddc67b190b215b966028eadb1876e4ea9d7463e88fbf94a433683532b6c96d7d8f4ec98cbb8bfb7ae5ffbd6
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# Activestorage Delayed
|
2
|
+
|
3
|
+
ActiveStorage for Rails 6 and 7 does not support to upload files in background which in most cases delays the submit process and then making the visitor get bored or receive a timeout error.
|
4
|
+
This is a Ruby on Rails gem to upload activestorage files in background by saving them as base64 encoded in the database and be processed later.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
- Add this line to your application's Gemfile:
|
8
|
+
```ruby
|
9
|
+
gem 'activestorage-delayed'
|
10
|
+
```
|
11
|
+
- And then execute: `bundle install`
|
12
|
+
- Generate the migration: `rails g migration add_activestorage_delayed`
|
13
|
+
- Add the following content to the migration file:
|
14
|
+
```ruby
|
15
|
+
create_table :activestorage_delayed_uploads do |t|
|
16
|
+
t.references :uploadable, polymorphic: true, null: false
|
17
|
+
t.string :attr_name, null: false
|
18
|
+
t.string :deleted_ids, default: ''
|
19
|
+
t.boolean :clean_before, default: false
|
20
|
+
t.text :files
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
```
|
24
|
+
- Run the migration: `rails db:migrate`
|
25
|
+
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
- Include `ActivestorageDelayed::DelayedConcern`
|
29
|
+
- Add `delayed_attach` to the files you want to upload in background
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class User < ApplicationRecord
|
33
|
+
include ActivestorageDelayed::DelayedConcern
|
34
|
+
|
35
|
+
has_one_attached :photo, require: true, use_filename: true
|
36
|
+
delayed_attach :photo
|
37
|
+
|
38
|
+
has_many_attached :certificates
|
39
|
+
delayed_attach :certificates
|
40
|
+
end
|
41
|
+
|
42
|
+
```
|
43
|
+
### `delayed_attach` accepts an optional hash with the following options:
|
44
|
+
- `require`: If set to `true`, the `photo` or the `photo_tmp` will be required before saving.
|
45
|
+
- `use_filename`: If set to `true`, the image filename will be used as the name of uploaded file instead of the hash-key used by `activestorage`
|
46
|
+
|
47
|
+
### Examples to upload files in background
|
48
|
+
- Upload a single file
|
49
|
+
```ruby
|
50
|
+
User.create(photo_tmp: File.open('my_file.png')) # uploads the file in background
|
51
|
+
User.create(photo: File.open('my_file.png')) # uploads the file directly
|
52
|
+
```
|
53
|
+
**HTML**:
|
54
|
+
```haml
|
55
|
+
f.file_field :photo_tmp
|
56
|
+
```
|
57
|
+
|
58
|
+
- Upload multiple files
|
59
|
+
```ruby
|
60
|
+
User.create(certificates_tmp: [File.open('my_file.png'), File.open('my_file.png')])
|
61
|
+
```
|
62
|
+
**HTML**:
|
63
|
+
```haml
|
64
|
+
= f.file_field :certificates_tmp, multiple: true
|
65
|
+
```
|
66
|
+
|
67
|
+
- Deletes first 2 certificates and uploads a new one
|
68
|
+
```ruby
|
69
|
+
file_ids = User.first.certificates.limit(2).pluck(:id)
|
70
|
+
User.first.update(certificates_tmp: { deleted_ids: file_ids, files: [File.open('my_file.png')] })
|
71
|
+
```
|
72
|
+
**HTML**
|
73
|
+
```haml
|
74
|
+
= file_field_tag 'user[certificates_tmp][files][]', multiple: true
|
75
|
+
- user.certificates.each do |file|
|
76
|
+
%li
|
77
|
+
= image_tag(file)
|
78
|
+
= check_box_tag 'user[certificates_tmp][deleted_ids][]', value: file.id
|
79
|
+
```
|
80
|
+
|
81
|
+
- Removes all certificates before uploading a new one
|
82
|
+
```ruby
|
83
|
+
User.first.update(certificates_tmp: { clean_before: true, files: [File.open('my_file.png')] })
|
84
|
+
```
|
85
|
+
|
86
|
+
- Upload files with custom names (requires `use_filename: true`)
|
87
|
+
```ruby
|
88
|
+
class User < ApplicationRecord
|
89
|
+
def photo_filename(filename)
|
90
|
+
"#{id}-#{full_name.parameterize}#{File.extname(filename)}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
When `<attr_name>_filename` is defined, then it is called to fetch the uploaded file name.
|
95
|
+
Note: Check [this](https://gist.github.com/owen2345/33730a452d73b6b292326bb602b0ee6b) if you want to rename an already uploaded file (remote file)
|
96
|
+
|
97
|
+
- Capture event when file upload has failed
|
98
|
+
```ruby
|
99
|
+
class User < ApplicationRecord
|
100
|
+
def ast_delayed_on_error(attr_name, error)
|
101
|
+
puts "Failed #{attr_name} with #{error}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
- Capture event when file has been uploaded
|
107
|
+
```ruby
|
108
|
+
class User < ApplicationRecord
|
109
|
+
after_save_commit do
|
110
|
+
puts 'Photo has been uploaded' if photo.blob.present?
|
111
|
+
puts 'No pending enqueued photo uploads' unless photo_delayed_uploads.any?
|
112
|
+
end
|
113
|
+
|
114
|
+
before_save do
|
115
|
+
puts "current assigned tmp photo: #{photo_tmp.inspect}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
`<attr_name>_delayed_uploads` is a `has_many` association that contains the list of scheduled uploads for the corresponding attribute.
|
120
|
+
|
121
|
+
|
122
|
+
## Preprocessing original files before uploading (Rails 7+)
|
123
|
+
```ruby
|
124
|
+
class User < ApplicationRecord
|
125
|
+
has_one_attached :photo do |attachable|
|
126
|
+
attachable.variant :default, strip: true, quality: 70, resize_to_fill: [200, 200]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
`:default` variant will be used to pre-preprocess the original file before uploading (if defined). By this way you have your desired image size and quality as the original image.
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
## Contributing
|
135
|
+
Bug reports and pull requests are welcome on https://github.com/owen2345/activestorage-delayed. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
136
|
+
|
137
|
+
To ensure your contribution changes, run the tests with: `docker-compose run test`
|
138
|
+
|
139
|
+
## License
|
140
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActivestorageDelayed
|
4
|
+
module DelayedConcern
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
@ast_delayed_settings = {}
|
8
|
+
def self.delayed_attach(attr_name, required: false, use_filename: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
9
|
+
@ast_delayed_settings[attr_name] = { use_filename: use_filename }
|
10
|
+
tmp_attr_name = :"#{attr_name}_tmp"
|
11
|
+
has_many_attr = :"#{attr_name}_delayed_uploads"
|
12
|
+
attr_accessor tmp_attr_name
|
13
|
+
|
14
|
+
has_many has_many_attr, as: :uploadable, dependent: :destroy, class_name: 'ActivestorageDelayed::DelayedUpload'
|
15
|
+
if required
|
16
|
+
validates tmp_attr_name, presence: true, unless: ->(o) { o.send(attr_name).blob }
|
17
|
+
validates attr_name, presence: true, unless: ->(o) { o.send(tmp_attr_name) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param delayed_data [Hash<files: Array<File1, File2>, deleted_ids: Array<1, 2>, clean_before: Boolean>]
|
21
|
+
# @param delayed_data [Array<File1, File2>]
|
22
|
+
define_method "#{tmp_attr_name}=" do |delayed_data|
|
23
|
+
instance_variable_set(:"@#{tmp_attr_name}", delayed_data.dup)
|
24
|
+
delayed_data = { files: delayed_data } unless delayed_data.is_a?(Hash)
|
25
|
+
delayed_data[:tmp_files] = delayed_data.delete(:files)
|
26
|
+
delayed_data[:deleted_ids] = (delayed_data.delete(:deleted_ids) || []).join(',')
|
27
|
+
delayed_data[:attr_name] = attr_name
|
28
|
+
send(has_many_attr) << send(has_many_attr).new(delayed_data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param _attr_name (String)
|
34
|
+
# @param _error (Exception)
|
35
|
+
def ast_delayed_on_error(_attr_name, _error); end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActivestorageDelayed
|
4
|
+
class DelayedUploader
|
5
|
+
attr_reader :delayed_upload
|
6
|
+
|
7
|
+
def initialize(delayed_upload_id)
|
8
|
+
@delayed_upload = delayed_upload_id if delayed_upload_id.is_a?(ActiveRecord::Base)
|
9
|
+
@delayed_upload ||= DelayedUpload.find_by(id: delayed_upload_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
return unless delayed_upload
|
14
|
+
|
15
|
+
remove_files
|
16
|
+
upload_photos
|
17
|
+
save_changes
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# TODO: check the ability to delete io with save or upload method
|
23
|
+
# file_data['io'].close
|
24
|
+
def upload_photos # rubocop:disable Metrics/AbcSize
|
25
|
+
tmp_files_data.each do |file_data|
|
26
|
+
model.send(attr_name).attach(file_data.transform_keys(&:to_sym))
|
27
|
+
end
|
28
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
29
|
+
Rails.logger.error("********* #{self.class.name} -> Failed uploading files: #{e.message}. #{e.backtrace[0..20]}")
|
30
|
+
model.ast_delayed_on_error(attr_name, e)
|
31
|
+
end
|
32
|
+
|
33
|
+
def save_changes
|
34
|
+
model.save!
|
35
|
+
delayed_upload.destroy!
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Array<Hash<io: StringIO, filename: String, content_type: String>]
|
39
|
+
def tmp_files_data
|
40
|
+
@tmp_files_data ||= begin
|
41
|
+
files = JSON.parse(delayed_upload.files || '[]')
|
42
|
+
files.each do |file_data|
|
43
|
+
file_data['io'] = base64_to_file(file_data)
|
44
|
+
if attr_settings[:use_filename]
|
45
|
+
file_data['key'] = filename_for(file_data['filename'])
|
46
|
+
file_data['filename'] = file_data['key']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def base64_to_file(file_data)
|
53
|
+
tempfile = Tempfile.new(file_data['filename'])
|
54
|
+
tempfile.binmode
|
55
|
+
tempfile.write Base64.decode64(file_data['io'])
|
56
|
+
tempfile.rewind
|
57
|
+
tempfile
|
58
|
+
end
|
59
|
+
|
60
|
+
def model
|
61
|
+
@model ||= delayed_upload.uploadable
|
62
|
+
end
|
63
|
+
|
64
|
+
def filename_for(filename)
|
65
|
+
method_name = "#{attr_name}_filename".to_sym
|
66
|
+
return model.send(method_name, filename) if model.respond_to?(method_name)
|
67
|
+
|
68
|
+
name = File.basename(filename, '.*').parameterize
|
69
|
+
name = "#{SecureRandom.uuid}-#{name}" if support_multiple?
|
70
|
+
"#{model.id}-#{name}#{File.extname(filename)}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def remove_files
|
74
|
+
items = delayed_upload.uploadable.send(attr_name)
|
75
|
+
return unless support_multiple?
|
76
|
+
|
77
|
+
items.where(id: delayed_upload.deleted_ids.split(',')).destroy_all if delayed_upload.deleted_ids.present?
|
78
|
+
items.destroy_all if delayed_upload.clean_before
|
79
|
+
end
|
80
|
+
|
81
|
+
def support_multiple?
|
82
|
+
model.send(attr_name).class.name.include?('Many')
|
83
|
+
end
|
84
|
+
|
85
|
+
def attr_name
|
86
|
+
delayed_upload.attr_name.to_sym
|
87
|
+
end
|
88
|
+
|
89
|
+
def attr_settings
|
90
|
+
model.class.instance_variable_get(:@ast_delayed_settings)[attr_name]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# t.string :attr_name, null: false
|
4
|
+
# t.string :deleted_ids, default: ''
|
5
|
+
# t.boolean :clean_before, default: false
|
6
|
+
# t.text :files
|
7
|
+
|
8
|
+
module ActivestorageDelayed
|
9
|
+
class DelayedUpload < ActiveRecord::Base
|
10
|
+
self.table_name = 'activestorage_delayed_uploads'
|
11
|
+
attr_accessor :tmp_files
|
12
|
+
|
13
|
+
belongs_to :uploadable, polymorphic: true
|
14
|
+
|
15
|
+
before_save :parse_tmp_files
|
16
|
+
after_create_commit do
|
17
|
+
ActivestorageDelayed::DelayedUploaderJob.perform_later(id)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse_tmp_files
|
23
|
+
self.files = (tmp_files.is_a?(Array) ? tmp_files : [tmp_files]).select(&:present?).map do |file|
|
24
|
+
{
|
25
|
+
'io' => Base64.encode64(file.read),
|
26
|
+
'filename' => file.try(:original_filename) || File.basename(file.path),
|
27
|
+
'content_type' => file.try(:content_type) || Marcel::MimeType.for(file)
|
28
|
+
}
|
29
|
+
end.to_json
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
module ActivestorageDelayed
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
railtie_name :activestorage_delayed
|
7
|
+
|
8
|
+
config.after_initialize do |_app|
|
9
|
+
require_relative '../../initializers/upload_default_variation'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_storage'
|
4
|
+
require 'active_job'
|
5
|
+
require 'activestorage-delayed/version'
|
6
|
+
require 'activestorage-delayed/railtie'
|
7
|
+
require 'activestorage-delayed/delayed_concern'
|
8
|
+
require 'activestorage-delayed/delayed_uploader'
|
9
|
+
require 'activestorage-delayed/delayed_uploader_job'
|
10
|
+
require 'activestorage-delayed/models/delayed_upload'
|
11
|
+
|
12
|
+
module ActivestorageDelayed
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activestorage-delayed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Owen Peredo Diaz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activestorage
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Ruby on Rails gem to upload activestorage files in background
|
42
|
+
email:
|
43
|
+
- owenperedo@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- lib/activestorage-delayed/delayed_concern.rb
|
51
|
+
- lib/activestorage-delayed/delayed_uploader.rb
|
52
|
+
- lib/activestorage-delayed/delayed_uploader_job.rb
|
53
|
+
- lib/activestorage-delayed/models/delayed_upload.rb
|
54
|
+
- lib/activestorage-delayed/railtie.rb
|
55
|
+
- lib/activestorage-delayed/version.rb
|
56
|
+
- lib/activestorage_delayed.rb
|
57
|
+
homepage: https://github.com/owen2345/activestorage-delayed
|
58
|
+
licenses: []
|
59
|
+
metadata:
|
60
|
+
homepage_uri: https://github.com/owen2345/activestorage-delayed
|
61
|
+
source_code_uri: https://github.com/owen2345/activestorage-delayed
|
62
|
+
changelog_uri: https://github.com/owen2345/activestorage-delayed
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubygems_version: 3.1.2
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Ruby on Rails gem to upload activestorage files in background
|
82
|
+
test_files: []
|