active_storage_db 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 733afb3767f4174510cc6e59a72a5c6e137d17479fc6ec733a977ff81f80fbaf
4
- data.tar.gz: dd7ffb266b99e6fbb2a0537b3d198a3e20200bc01dbdb43fe2407cd01161793d
3
+ metadata.gz: 23bbb0b9bf0759169020c1da8e4a8d5028938ffdb1b13dc4a356783da06eb868
4
+ data.tar.gz: 1e1e1146a363bba15efe440cd82fbc4926763596746995f8696c24ca9cb9789c
5
5
  SHA512:
6
- metadata.gz: 4ef2822294d193dd86b6bf865602ec14f13e5868a418e2ad3685d9eaade829b302659634c4effebf495f63c63a96252da50a170ad0b2e3cb831243602e0cfdcd
7
- data.tar.gz: 8b4e73f58bfcc0f3a840812757069be9ef9859585a3a9158721ede94885d014a2b507917fdc8e5a5da7e3f17c0ef90df7146958205aa88a07243471f3b44d118
6
+ metadata.gz: cc1229568804d8a6c1a0912f8ddcf59895d12374a57b460a6f29c8bce1aac7216c936b6b8c255ea41563f39b22c748e34645bac787da24886d9c02bcd367adb3
7
+ data.tar.gz: b85e8f1b41dfc2e01cfb566efd561a44606959cd1cb6b3a9cf718e4618b396949aad3fa87f8037ba61eda0789425bf9b4cf1031805d370c4702d2c973a5dfe5b
data/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [![gem version](https://badge.fury.io/rb/active_storage_db.svg)](https://badge.fury.io/rb/active_storage_db)
4
4
  [![linters](https://github.com/blocknotes/active_storage_db/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/active_storage_db/actions/workflows/linters.yml)
5
- [![specs Postgres](https://github.com/blocknotes/active_storage_db/actions/workflows/postgres.yml/badge.svg)](https://github.com/blocknotes/active_storage_db/actions/workflows/postgres.yml)
6
- [![specs MySQL](https://github.com/blocknotes/active_storage_db/actions/workflows/mysql.yml/badge.svg)](https://github.com/blocknotes/active_storage_db/actions/workflows/mysql.yml)
5
+ [![specs Postgres](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_postgres_70.yml/badge.svg)](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_postgres_70.yml)
6
+ [![Specs MySQL](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_mysql_70.yml/badge.svg)](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_mysql_70.yml)
7
7
 
8
8
  An Active Storage service upload/download plugin that stores files in a PostgreSQL or MySQL database.
9
9
 
10
10
  Main features:
11
- - supports Rails 6.0, 6.1 and 7.0;
11
+ - supports Rails _6.0_, _6.1_ and _7.0_;
12
12
  - all service methods implemented;
13
13
  - attachment data stored in a binary field (or blob).
14
14
 
@@ -30,10 +30,16 @@ db:
30
30
 
31
31
  ## Misc
32
32
 
33
- Some rake tasks are available:
33
+ Some utility tasks are available:
34
34
 
35
- - `asdb:list`: list the stored attachments
36
- - `asdb:get`: download an attachment (ex. `bin/rails "asdb:get[ruby-logo.png,/tmp]"`)
35
+ ```sh
36
+ # list attachments ordered by blob id desc (with limit 100):
37
+ bin/rails 'asdb:list'
38
+ # search attachments by filename (or part of it)
39
+ bin/rails 'asdb:search[some_filename]'
40
+ # download attachment by blob id (retrieved with list or search tasks) - the second argument is the destination:
41
+ bin/rails 'asdb:download[123,/tmp]'
42
+ ```
37
43
 
38
44
  ## Do you like it? Star it!
39
45
 
data/Rakefile CHANGED
@@ -16,8 +16,8 @@ RDoc::Task.new(:rdoc) do |rdoc|
16
16
  rdoc.rdoc_files.include('lib/**/*.rb')
17
17
  end
18
18
 
19
- dummy_app = ENV['RAILS_7'] ? 'dummy7' : 'dummy'
20
- APP_RAKEFILE = File.expand_path("spec/#{dummy_app}/Rakefile", __dir__)
19
+ app_ver = ENV.fetch('RAILS', '').tr('.', '')
20
+ APP_RAKEFILE = File.expand_path("spec/dummy#{app_ver}/Rakefile", __dir__)
21
21
  load 'rails/tasks/engine.rake'
22
22
 
23
23
  load 'rails/tasks/statistics.rake'
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageDB
4
- class File < ApplicationRecord
4
+ class File < ActiveRecord::Base
5
5
  validates :ref,
6
6
  presence: true,
7
7
  allow_blank: false,
@@ -1,9 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_storage/service/db_service_rails60'
4
+ require 'active_storage/service/db_service_rails61'
5
+ require 'active_storage/service/db_service_rails70'
6
+
3
7
  module ActiveStorage
4
- class Service::DBService < Service # rubocop:disable Style/ClassAndModuleChildren
5
- def initialize(**_config)
8
+ # Wraps a DB table as an Active Storage service. See ActiveStorage::Service
9
+ # for the generic API documentation that applies to all services.
10
+ class Service::DBService < Service
11
+ if Rails::VERSION::MAJOR >= 7
12
+ include ActiveStorage::DBServiceRails70
13
+ elsif Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
14
+ include ActiveStorage::DBServiceRails61
15
+ else
16
+ include ActiveStorage::DBServiceRails60
17
+ end
18
+
19
+ def initialize(public: false, **)
6
20
  @chunk_size = ENV.fetch('ASDB_CHUNK_SIZE') { 1.megabytes }
21
+ @public = public
7
22
  end
8
23
 
9
24
  def upload(key, io, checksum: nil, **)
@@ -22,7 +37,9 @@ module ActiveStorage
22
37
  else
23
38
  instrument :download, key: key do
24
39
  record = ::ActiveStorageDB::File.find_by(ref: key)
25
- record&.data || raise(ActiveStorage::FileNotFoundError)
40
+ raise(ActiveStorage::FileNotFoundError) unless record
41
+
42
+ record.data
26
43
  end
27
44
  end
28
45
  end
@@ -30,14 +47,17 @@ module ActiveStorage
30
47
  def download_chunk(key, range)
31
48
  instrument :download_chunk, key: key, range: range do
32
49
  chunk_select = "SUBSTRING(data FROM #{range.begin + 1} FOR #{range.size}) AS chunk"
33
- ::ActiveStorageDB::File.select(chunk_select).find_by(ref: key)&.chunk ||
34
- raise(ActiveStorage::FileNotFoundError)
50
+ record = ::ActiveStorageDB::File.select(chunk_select).find_by(ref: key)
51
+ raise(ActiveStorage::FileNotFoundError) unless record
52
+
53
+ record.chunk
35
54
  end
36
55
  end
37
56
 
38
57
  def delete(key)
39
58
  instrument :delete, key: key do
40
59
  ::ActiveStorageDB::File.find_by(ref: key)&.destroy
60
+ # Ignore files already deleted
41
61
  end
42
62
  end
43
63
 
@@ -55,34 +75,6 @@ module ActiveStorage
55
75
  end
56
76
  end
57
77
 
58
- def url(key, expires_in:, filename:, disposition:, content_type:)
59
- instrument :url, key: key do |payload|
60
- content_disposition = content_disposition_with(type: disposition, filename: filename)
61
- verified_key_with_expiration = ActiveStorage.verifier.generate(
62
- {
63
- key: key,
64
- disposition: content_disposition,
65
- content_type: content_type
66
- },
67
- expires_in: expires_in,
68
- purpose: :blob_key
69
- )
70
- current_uri = URI.parse(current_host)
71
- generated_url = url_helpers.service_url(
72
- verified_key_with_expiration,
73
- protocol: current_uri.scheme,
74
- host: current_uri.host,
75
- port: current_uri.port,
76
- disposition: content_disposition,
77
- content_type: content_type,
78
- filename: filename
79
- )
80
- payload[:url] = generated_url
81
-
82
- generated_url
83
- end
84
- end
85
-
86
78
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
87
79
  instrument :url, key: key do |payload|
88
80
  verified_token_with_expiration = ActiveStorage.verifier.generate(
@@ -90,15 +82,16 @@ module ActiveStorage
90
82
  key: key,
91
83
  content_type: content_type,
92
84
  content_length: content_length,
93
- checksum: checksum
85
+ checksum: checksum,
86
+ service_name: respond_to?(:name) ? name : 'db'
94
87
  },
95
88
  expires_in: expires_in,
96
89
  purpose: :blob_token
97
90
  )
98
- generated_url = url_helpers.update_service_url(verified_token_with_expiration, host: current_host)
99
- payload[:url] = generated_url
100
91
 
101
- generated_url
92
+ url_helpers.update_service_url(verified_token_with_expiration, url_options).tap do |generated_url|
93
+ payload[:url] = generated_url
94
+ end
102
95
  end
103
96
  end
104
97
 
@@ -108,15 +101,29 @@ module ActiveStorage
108
101
 
109
102
  private
110
103
 
111
- def current_host
112
- if ActiveStorage::Current.respond_to? :url_options
113
- opts = ActiveStorage::Current.url_options || {}
114
- url = "#{opts[:protocol]}#{opts[:host]}"
115
- url += ":#{opts[:port]}" if opts[:port]
116
- url || ''
117
- else
118
- ActiveStorage::Current.host
119
- end
104
+ def generate_url(key, expires_in:, filename:, content_type:, disposition:)
105
+ content_disposition = content_disposition_with(type: disposition, filename: filename)
106
+ verified_key_with_expiration = ActiveStorage.verifier.generate(
107
+ {
108
+ key: key,
109
+ disposition: content_disposition,
110
+ content_type: content_type,
111
+ service_name: respond_to?(:name) ? name : 'db'
112
+ },
113
+ expires_in: expires_in,
114
+ purpose: :blob_key
115
+ )
116
+
117
+ current_uri = URI.parse(current_host)
118
+ url_helpers.service_url(
119
+ verified_key_with_expiration,
120
+ protocol: current_uri.scheme,
121
+ host: current_uri.host,
122
+ port: current_uri.port,
123
+ disposition: content_disposition,
124
+ content_type: content_type,
125
+ filename: filename
126
+ )
120
127
  end
121
128
 
122
129
  def ensure_integrity_of(key, checksum)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module DBServiceRails60
5
+ def url(key, expires_in:, filename:, disposition:, content_type:)
6
+ instrument :url, key: key do |payload|
7
+ generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition).tap do |generated_url|
8
+ payload[:url] = generated_url
9
+ end
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def current_host
16
+ url_options[:host]
17
+ end
18
+
19
+ def url_options
20
+ {
21
+ host: ActiveStorage::Current.host
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module DBServiceRails61
5
+ private
6
+
7
+ def current_host
8
+ url_options[:host]
9
+ end
10
+
11
+ def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
12
+ generate_url(
13
+ key,
14
+ expires_in: expires_in,
15
+ filename: filename,
16
+ content_type: content_type,
17
+ disposition: disposition
18
+ )
19
+ end
20
+
21
+ def public_url(key, filename:, content_type: nil, disposition: :attachment, **)
22
+ generate_url(
23
+ key,
24
+ expires_in: nil,
25
+ filename: filename,
26
+ content_type: content_type,
27
+ disposition: disposition
28
+ )
29
+ end
30
+
31
+ def url_options
32
+ {
33
+ host: ActiveStorage::Current.host
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module DBServiceRails70
5
+ def compose(source_keys, destination_key, **)
6
+ buffer = nil
7
+ source_keys.each do |source_key|
8
+ data = ::ActiveStorageDB::File.find_by!(ref: source_key).data
9
+ if buffer
10
+ buffer << data
11
+ else
12
+ buffer = data
13
+ end
14
+ end
15
+ ::ActiveStorageDB::File.create!(ref: destination_key, data: buffer) if buffer
16
+ end
17
+
18
+ private
19
+
20
+ def current_host
21
+ opts = url_options || {}
22
+ url = "#{opts[:protocol]}#{opts[:host]}"
23
+ url + ":#{opts[:port]}" if opts[:port]
24
+ end
25
+
26
+ def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
27
+ generate_url(
28
+ key,
29
+ expires_in: expires_in,
30
+ filename: filename,
31
+ content_type: content_type,
32
+ disposition: disposition
33
+ )
34
+ end
35
+
36
+ def public_url(key, filename:, content_type: nil, disposition: :attachment, **)
37
+ generate_url(
38
+ key,
39
+ expires_in: nil,
40
+ filename: filename,
41
+ content_type: content_type,
42
+ disposition: disposition
43
+ )
44
+ end
45
+
46
+ def url_options
47
+ ActiveStorage::Current.url_options
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageDB
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -1,31 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module ActiveStorage
4
+ module Tasks
5
+ module_function
6
+
7
+ def print_blob_header(digits: 0)
8
+ puts ['Size'.rjust(8), 'Date'.rjust(18), 'Id'.rjust(digits + 2), ' Filename'].join
9
+ end
10
+
11
+ def print_blob(blob, digits: 0)
12
+ size = (blob.byte_size / 1024).to_s.rjust(7)
13
+ date = blob.created_at.strftime('%Y-%m-%d %H:%M')
14
+ puts "#{size}K #{date} #{blob.id.to_s.rjust(digits)} #{blob.filename}"
15
+ end
16
+ end
17
+ end
18
+
3
19
  namespace :asdb do
4
- desc 'ActiveStorageDB: list attachments'
20
+ desc 'ActiveStorageDB: list attachments ordered by blob id desc'
5
21
  task list: [:environment] do |_t, _args|
6
- ::ActiveStorage::Blob.order(:filename).pluck(:byte_size, :created_at, :filename).each do |size, dt, filename|
7
- size_k = (size / 1024).to_s.rjust(7)
8
- date = dt.strftime('%Y-%m-%d %H:%M')
9
- puts "#{size_k}K #{date} #{filename}"
22
+ query = ::ActiveStorage::Blob.order(id: :desc).limit(100)
23
+ digits = Math.log(query.maximum(:id), 10).to_i + 1
24
+
25
+ ::ActiveStorage::Tasks.print_blob_header(digits: digits)
26
+ query.each do |blob|
27
+ ::ActiveStorage::Tasks.print_blob(blob, digits: digits)
10
28
  end
11
29
  end
12
30
 
13
- desc 'ActiveStorageDB: download attachment'
14
- task :get, [:src, :dst] => [:environment] do |_t, args|
15
- src = args[:src]&.strip
16
- dst = args[:dst]&.strip
17
- abort('Required arguments: source file, destination file') if src.blank? || dst.blank?
18
-
19
- dst = "#{dst}/#{src}" if Dir.exist?(dst)
20
- dir = File.dirname(dst)
21
- abort("Can't write on: #{dir}") unless File.writable?(dir)
31
+ desc 'ActiveStorageDB: download attachment by blob id'
32
+ task :download, [:blob_id, :destination] => [:environment] do |_t, args|
33
+ blob_id = args[:blob_id]&.strip
34
+ destination = args[:destination]&.strip || Dir.pwd
35
+ abort('Required arguments: source blob id, destination path') if blob_id.blank? || destination.blank?
22
36
 
23
- blob = ::ActiveStorage::Blob.order(created_at: :desc).find_by(filename: src)
37
+ blob = ::ActiveStorage::Blob.find_by(id: blob_id)
24
38
  abort('Source file not found') unless blob
25
39
 
26
- ret = File.binwrite(dst, blob.download)
27
- puts "#{ret} bytes written"
28
- rescue StandardError => e
29
- puts e
40
+ destination = "#{destination}/#{blob.filename}" if Dir.exist?(destination)
41
+ dir = File.dirname(destination)
42
+ abort("Can't write on path: #{dir}") unless File.writable?(dir)
43
+
44
+ ret = File.binwrite(destination, blob.download)
45
+ puts "#{ret} bytes written - #{destination}"
46
+ end
47
+
48
+ desc 'ActiveStorageDB: search attachment by filename (or part of it)'
49
+ task :search, [:filename] => [:environment] do |_t, args|
50
+ filename = args[:filename]&.strip
51
+ abort('Required arguments: filename') if filename.blank?
52
+
53
+ blobs = ::ActiveStorage::Blob.where('filename LIKE ?', "%#{filename}%").order(id: :desc)
54
+ if blobs.any?
55
+ digits = Math.log(blobs.first.id, 10).to_i + 1
56
+ ::ActiveStorage::Tasks.print_blob_header(digits: digits)
57
+ blobs.each do |blob|
58
+ ::ActiveStorage::Tasks.print_blob(blob, digits: digits)
59
+ end
60
+ else
61
+ puts 'No results'
62
+ end
30
63
  end
31
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_db
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mattia Roccoberton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-26 00:00:00.000000000 Z
11
+ date: 2022-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activestorage
@@ -77,12 +77,14 @@ files:
77
77
  - README.md
78
78
  - Rakefile
79
79
  - app/controllers/active_storage_db/files_controller.rb
80
- - app/models/active_storage_db/application_record.rb
81
80
  - app/models/active_storage_db/file.rb
82
81
  - config/initializers/inflections.rb
83
82
  - config/routes.rb
84
83
  - db/migrate/20200702202022_create_active_storage_db_files.rb
85
84
  - lib/active_storage/service/db_service.rb
85
+ - lib/active_storage/service/db_service_rails60.rb
86
+ - lib/active_storage/service/db_service_rails61.rb
87
+ - lib/active_storage/service/db_service_rails70.rb
86
88
  - lib/active_storage_db.rb
87
89
  - lib/active_storage_db/engine.rb
88
90
  - lib/active_storage_db/version.rb
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveStorageDB
4
- class ApplicationRecord < ActiveRecord::Base
5
- self.abstract_class = true
6
- end
7
- end