activestorage-delayed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActivestorageDelayed
4
+ class DelayedUploaderJob < ActiveJob::Base
5
+ queue_as :default
6
+
7
+ def perform(delayed_upload_id)
8
+ ActivestorageDelayed::DelayedUploader.new(delayed_upload_id).call
9
+ end
10
+ end
11
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActivestorageDelayed
4
+ VERSION = '0.1.0'
5
+ 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: []