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 +4 -4
- data/README.md +12 -6
- data/Rakefile +2 -2
- data/app/models/active_storage_db/file.rb +1 -1
- data/lib/active_storage/service/db_service.rb +53 -46
- data/lib/active_storage/service/db_service_rails60.rb +25 -0
- data/lib/active_storage/service/db_service_rails61.rb +37 -0
- data/lib/active_storage/service/db_service_rails70.rb +50 -0
- data/lib/active_storage_db/version.rb +1 -1
- data/lib/tasks/active_storage_db_tasks.rake +52 -19
- metadata +5 -3
- data/app/models/active_storage_db/application_record.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23bbb0b9bf0759169020c1da8e4a8d5028938ffdb1b13dc4a356783da06eb868
|
4
|
+
data.tar.gz: 1e1e1146a363bba15efe440cd82fbc4926763596746995f8696c24ca9cb9789c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc1229568804d8a6c1a0912f8ddcf59895d12374a57b460a6f29c8bce1aac7216c936b6b8c255ea41563f39b22c748e34645bac787da24886d9c02bcd367adb3
|
7
|
+
data.tar.gz: b85e8f1b41dfc2e01cfb566efd561a44606959cd1cb6b3a9cf718e4618b396949aad3fa87f8037ba61eda0789425bf9b4cf1031805d370c4702d2c973a5dfe5b
|
data/README.md
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/active_storage_db)
|
4
4
|
[](https://github.com/blocknotes/active_storage_db/actions/workflows/linters.yml)
|
5
|
-
[](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_postgres_70.yml)
|
6
|
+
[](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
|
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
|
33
|
+
Some utility tasks are available:
|
34
34
|
|
35
|
-
|
36
|
-
|
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
|
-
|
20
|
-
APP_RAKEFILE = File.expand_path("spec
|
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,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
|
-
|
5
|
-
|
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
|
-
|
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)
|
34
|
-
|
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
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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,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(:
|
7
|
-
|
8
|
-
|
9
|
-
|
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 :
|
15
|
-
|
16
|
-
|
17
|
-
abort('Required arguments: source
|
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.
|
37
|
+
blob = ::ActiveStorage::Blob.find_by(id: blob_id)
|
24
38
|
abort('Source file not found') unless blob
|
25
39
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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.
|
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-
|
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
|