active_storage_db 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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