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 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: []