active_storage_db 1.5.0 → 1.6.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 +4 -4
- data/app/controllers/active_storage_db/files_controller.rb +6 -9
- data/app/models/active_storage_db/file.rb +1 -1
- data/db/migrate/20200702202022_create_active_storage_db_files.rb +1 -2
- data/lib/active_storage/service/db_service.rb +46 -23
- data/lib/active_storage/service/db_service_rails70.rb +5 -3
- data/lib/active_storage_db/engine.rb +8 -0
- data/lib/active_storage_db/version.rb +1 -1
- data/lib/tasks/active_storage_db_tasks.rake +35 -16
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef4de196ae1110b81dfa6adfdd641e82bd009d3d1ed7fd0fbdd497d66cee089f
|
|
4
|
+
data.tar.gz: f02ae2f4d33d4f054814b0c1fa178af09ec2f6084beb02448af43cf16b45db40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 331e0ccf899fba5d8ed88eacfd95612b074fb403696092b0744a81641dcea1d83f7c723dd430281e3a91224a01657f132ce5cdf9eb06fc04b4e9b6ac98ba6c5d
|
|
7
|
+
data.tar.gz: 4e657d03432899f4c18bac98201c639c0473150fedd0f83bbfe1ae674000e3765aca3111d823aaf46178a9e4195c44c9ca28297256b1e573bcbe3747cda16f72
|
data/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
[](https://codeclimate.com/github/blocknotes/active_storage_db/maintainability)
|
|
6
6
|
|
|
7
7
|
[](https://github.com/blocknotes/active_storage_db/actions/workflows/linters.yml)
|
|
8
|
-
[](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_postgres_rails81.yml)
|
|
9
|
+
[](https://github.com/blocknotes/active_storage_db/actions/workflows/specs_mysql_rails81.yml)
|
|
10
10
|
|
|
11
11
|
An Active Storage service upload/download plugin that stores files in a PostgreSQL or MySQL database.
|
|
12
12
|
Experimental support also for MSSQL and SQLite.
|
|
@@ -21,8 +21,8 @@ Useful also with platforms like Heroku (due to their ephemeral file system).
|
|
|
21
21
|
- Add to your Gemfile `gem 'active_storage_db'` (and execute: `bundle`)
|
|
22
22
|
- Install the gem migrations: `bin/rails active_storage_db:install:migrations` (and execute: `bin/rails db:migrate`)
|
|
23
23
|
- Add to your `config/routes.rb`: `mount ActiveStorageDB::Engine => '/active_storage_db'`
|
|
24
|
-
- Change Active Storage service in
|
|
25
|
-
- Add to
|
|
24
|
+
- Change Active Storage service in _config/environments/development.rb_ to: `config.active_storage.service = :db`
|
|
25
|
+
- Add to _config/storage.yml_:
|
|
26
26
|
|
|
27
27
|
```yml
|
|
28
28
|
db:
|
|
@@ -16,19 +16,20 @@ module ActiveStorageDB
|
|
|
16
16
|
|
|
17
17
|
def update
|
|
18
18
|
if (token = decode_verified_token)
|
|
19
|
-
file_uploaded = upload_file(token
|
|
20
|
-
head(file_uploaded ? :no_content :
|
|
19
|
+
file_uploaded = upload_file(token)
|
|
20
|
+
head(file_uploaded ? :no_content : ActiveStorageDB::UNPROCESSABLE_STATUS)
|
|
21
21
|
else
|
|
22
22
|
head(:not_found)
|
|
23
23
|
end
|
|
24
24
|
rescue ActiveStorage::IntegrityError
|
|
25
|
-
head(
|
|
25
|
+
head(ActiveStorageDB::UNPROCESSABLE_STATUS)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
30
|
def acceptable_content?(token)
|
|
31
|
-
token[:content_type] == request.
|
|
31
|
+
token[:content_type] == request.media_type &&
|
|
32
|
+
token[:content_length] == request.content_length
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def db_service
|
|
@@ -53,15 +54,11 @@ module ActiveStorageDB
|
|
|
53
54
|
send_data(db_service.download(key), options)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
def upload_file(token
|
|
57
|
+
def upload_file(token) # rubocop:disable Naming/PredicateMethod
|
|
57
58
|
return false unless acceptable_content?(token)
|
|
58
59
|
|
|
59
60
|
db_service.upload(token[:key], request.body, checksum: token[:checksum])
|
|
60
61
|
true
|
|
61
62
|
end
|
|
62
|
-
|
|
63
|
-
def unprocessable
|
|
64
|
-
Gem::Version.new(Rails.version) >= Gem::Version.new("7.1") ? :unprocessable_content : :unprocessable_entity
|
|
65
|
-
end
|
|
66
63
|
end
|
|
67
64
|
end
|
|
@@ -20,7 +20,6 @@ class CreateActiveStorageDBFiles < ActiveRecord::Migration[6.0]
|
|
|
20
20
|
|
|
21
21
|
def primary_key_type
|
|
22
22
|
config = Rails.configuration.generators
|
|
23
|
-
|
|
24
|
-
primary_key_type || :primary_key
|
|
23
|
+
config.options.dig(config.orm, :primary_key_type) || :primary_key
|
|
25
24
|
end
|
|
26
25
|
end
|
|
@@ -18,16 +18,26 @@ module ActiveStorage
|
|
|
18
18
|
end
|
|
19
19
|
# :nocov:
|
|
20
20
|
|
|
21
|
+
MINIMUM_CHUNK_SIZE = 1
|
|
22
|
+
|
|
21
23
|
def initialize(public: false, **)
|
|
22
|
-
@chunk_size = ENV.fetch("ASDB_CHUNK_SIZE") { 1.megabytes }
|
|
24
|
+
@chunk_size = [ENV.fetch("ASDB_CHUNK_SIZE") { 1.megabytes }.to_i, MINIMUM_CHUNK_SIZE].max
|
|
25
|
+
@max_size = ENV.fetch("ASDB_MAX_FILE_SIZE", nil)&.to_i
|
|
23
26
|
@public = public
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def upload(key, io, checksum: nil, **)
|
|
27
30
|
instrument :upload, key: key, checksum: checksum do
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
data = io.read
|
|
32
|
+
if @max_size && data.bytesize > @max_size
|
|
33
|
+
raise ArgumentError, "File size exceeds the maximum allowed size of #{@max_size} bytes"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if checksum
|
|
37
|
+
digest = Digest::MD5.base64digest(data)
|
|
38
|
+
raise ActiveStorage::IntegrityError unless digest == checksum
|
|
39
|
+
end
|
|
40
|
+
::ActiveStorageDB::File.create!(ref: key, data: data)
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
43
|
|
|
@@ -45,11 +55,13 @@ module ActiveStorage
|
|
|
45
55
|
|
|
46
56
|
def download_chunk(key, range)
|
|
47
57
|
instrument :download_chunk, key: key, range: range do
|
|
58
|
+
# NOTE: from/size are derived from Range#begin and Range#size (always integers),
|
|
59
|
+
# so string interpolation into SQL is safe here.
|
|
48
60
|
from = range.begin + 1
|
|
49
61
|
size = range.size
|
|
50
62
|
args = adapter_sqlserver? || adapter_sqlite? ? "data, #{from}, #{size}" : "data FROM #{from} FOR #{size}"
|
|
51
63
|
record = object_for(key, fields: "SUBSTRING(#{args}) AS chunk")
|
|
52
|
-
raise
|
|
64
|
+
raise ActiveStorage::FileNotFoundError unless record
|
|
53
65
|
|
|
54
66
|
record.chunk
|
|
55
67
|
end
|
|
@@ -68,7 +80,7 @@ module ActiveStorage
|
|
|
68
80
|
def delete_prefixed(prefix)
|
|
69
81
|
instrument :delete_prefixed, prefix: prefix do
|
|
70
82
|
comment = "DBService#delete_prefixed"
|
|
71
|
-
sanitized_prefix = "#{
|
|
83
|
+
sanitized_prefix = "#{ActiveRecord::Base.sanitize_sql_like(prefix)}%"
|
|
72
84
|
::ActiveStorageDB::File.annotate(comment).where("ref LIKE ?", sanitized_prefix).destroy_all
|
|
73
85
|
end
|
|
74
86
|
end
|
|
@@ -76,7 +88,7 @@ module ActiveStorage
|
|
|
76
88
|
def exist?(key)
|
|
77
89
|
instrument :exist, key: key do |payload|
|
|
78
90
|
comment = "DBService#exist?"
|
|
79
|
-
result = ::ActiveStorageDB::File.annotate(comment).
|
|
91
|
+
result = ::ActiveStorageDB::File.annotate(comment).exists?(ref: key)
|
|
80
92
|
payload[:exist] = result
|
|
81
93
|
result
|
|
82
94
|
end
|
|
@@ -90,7 +102,7 @@ module ActiveStorage
|
|
|
90
102
|
content_type: content_type,
|
|
91
103
|
content_length: content_length,
|
|
92
104
|
checksum: checksum,
|
|
93
|
-
service_name:
|
|
105
|
+
service_name: service_name_for_token
|
|
94
106
|
},
|
|
95
107
|
expires_in: expires_in,
|
|
96
108
|
purpose: :blob_token
|
|
@@ -108,12 +120,20 @@ module ActiveStorage
|
|
|
108
120
|
|
|
109
121
|
private
|
|
110
122
|
|
|
123
|
+
def service_name_for_token
|
|
124
|
+
name.presence || "db"
|
|
125
|
+
end
|
|
126
|
+
|
|
111
127
|
def adapter_sqlite?
|
|
112
|
-
@adapter_sqlite
|
|
128
|
+
return @adapter_sqlite if defined?(@adapter_sqlite)
|
|
129
|
+
|
|
130
|
+
@adapter_sqlite = active_storage_db_adapter_name == "SQLite"
|
|
113
131
|
end
|
|
114
132
|
|
|
115
133
|
def adapter_sqlserver?
|
|
116
|
-
@adapter_sqlserver
|
|
134
|
+
return @adapter_sqlserver if defined?(@adapter_sqlserver)
|
|
135
|
+
|
|
136
|
+
@adapter_sqlserver = active_storage_db_adapter_name == "SQLServer"
|
|
117
137
|
end
|
|
118
138
|
|
|
119
139
|
def active_storage_db_adapter_name
|
|
@@ -131,7 +151,7 @@ module ActiveStorage
|
|
|
131
151
|
key: key,
|
|
132
152
|
disposition: content_disposition,
|
|
133
153
|
content_type: content_type,
|
|
134
|
-
service_name:
|
|
154
|
+
service_name: service_name_for_token
|
|
135
155
|
},
|
|
136
156
|
expires_in: expires_in,
|
|
137
157
|
purpose: :blob_key
|
|
@@ -149,35 +169,38 @@ module ActiveStorage
|
|
|
149
169
|
)
|
|
150
170
|
end
|
|
151
171
|
|
|
152
|
-
def ensure_integrity_of(key, checksum)
|
|
153
|
-
return if Digest::MD5.base64digest(object_for(key).data) == checksum
|
|
154
|
-
|
|
155
|
-
delete(key)
|
|
156
|
-
raise ActiveStorage::IntegrityError
|
|
157
|
-
end
|
|
158
|
-
|
|
159
172
|
def retrieve_file(key)
|
|
160
173
|
file = object_for(key)
|
|
161
|
-
raise
|
|
174
|
+
raise ActiveStorage::FileNotFoundError unless file
|
|
162
175
|
|
|
163
176
|
file.data
|
|
164
177
|
end
|
|
165
178
|
|
|
166
179
|
def object_for(key, fields: nil)
|
|
167
180
|
comment = "DBService#object_for"
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
scope = ::ActiveStorageDB::File.annotate(comment)
|
|
182
|
+
scope = scope.select(fields) if fields
|
|
183
|
+
scope.find_by(ref: key)
|
|
170
184
|
end
|
|
171
185
|
|
|
172
186
|
def stream(key)
|
|
173
|
-
|
|
174
|
-
size = object_for(key, fields: "#{data_size} AS size")&.size || raise(ActiveStorage::FileNotFoundError)
|
|
187
|
+
size = object_for(key, fields: data_size)&.size || raise(ActiveStorage::FileNotFoundError)
|
|
175
188
|
(size / @chunk_size.to_f).ceil.times.each do |i|
|
|
176
189
|
range = (i * @chunk_size)..(((i + 1) * @chunk_size) - 1)
|
|
177
190
|
yield download_chunk(key, range)
|
|
178
191
|
end
|
|
179
192
|
end
|
|
180
193
|
|
|
194
|
+
def data_size
|
|
195
|
+
if adapter_sqlserver?
|
|
196
|
+
"DATALENGTH(data) AS size"
|
|
197
|
+
elsif adapter_sqlite?
|
|
198
|
+
"LENGTH(data) AS size"
|
|
199
|
+
else
|
|
200
|
+
"OCTET_LENGTH(data) AS size"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
181
204
|
def url_helpers
|
|
182
205
|
@url_helpers ||= ::ActiveStorageDB::Engine.routes.url_helpers
|
|
183
206
|
end
|
|
@@ -6,11 +6,13 @@ module ActiveStorage
|
|
|
6
6
|
buffer = nil
|
|
7
7
|
comment = "DBService#compose"
|
|
8
8
|
source_keys.each do |source_key|
|
|
9
|
-
|
|
9
|
+
record = ::ActiveStorageDB::File.annotate(comment).select(:data).find_by(ref: source_key)
|
|
10
|
+
raise ActiveStorage::FileNotFoundError unless record
|
|
11
|
+
|
|
10
12
|
if buffer
|
|
11
|
-
buffer << data
|
|
13
|
+
buffer << record.data
|
|
12
14
|
else
|
|
13
|
-
buffer = +data
|
|
15
|
+
buffer = +record.data
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
::ActiveStorageDB::File.create!(ref: destination_key, data: buffer) if buffer
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveStorageDB
|
|
4
|
+
# :nocov:
|
|
5
|
+
UNPROCESSABLE_STATUS = if Rails::VERSION::MAJOR > 7 || (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1)
|
|
6
|
+
:unprocessable_content
|
|
7
|
+
else
|
|
8
|
+
:unprocessable_entity
|
|
9
|
+
end
|
|
10
|
+
# :nocov:
|
|
11
|
+
|
|
4
12
|
class Engine < ::Rails::Engine
|
|
5
13
|
isolate_namespace ActiveStorageDB
|
|
6
14
|
end
|
|
@@ -5,22 +5,38 @@ module ActiveStorage
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def print_blob_header(digits: 0)
|
|
8
|
-
puts [
|
|
8
|
+
puts ["Size".rjust(8), "Date".rjust(18), "Id".rjust(digits + 2), " Filename"].join
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def print_blob(blob, digits: 0)
|
|
12
|
-
size = (blob.byte_size
|
|
13
|
-
date = blob.created_at.strftime(
|
|
14
|
-
puts "#{size}
|
|
12
|
+
size = format_size(blob.byte_size)
|
|
13
|
+
date = blob.created_at.strftime("%Y-%m-%d %H:%M")
|
|
14
|
+
puts "#{size} #{date} #{blob.id.to_s.rjust(digits)} #{blob.filename}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def format_size(bytes)
|
|
18
|
+
if bytes >= 1.gigabyte
|
|
19
|
+
"#{(bytes / 1.gigabyte.to_f).round(1)}G".rjust(8)
|
|
20
|
+
elsif bytes >= 1.megabyte
|
|
21
|
+
"#{(bytes / 1.megabyte.to_f).round(1)}M".rjust(8)
|
|
22
|
+
elsif bytes >= 1.kilobyte
|
|
23
|
+
"#{bytes / 1024}K".rjust(8)
|
|
24
|
+
else
|
|
25
|
+
"#{bytes}B".rjust(8)
|
|
26
|
+
end
|
|
15
27
|
end
|
|
16
28
|
end
|
|
17
29
|
end
|
|
18
30
|
|
|
19
31
|
namespace :asdb do
|
|
20
|
-
desc
|
|
21
|
-
task list: [:environment] do |_t,
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
desc "ActiveStorageDB: list attachments ordered by blob id desc"
|
|
33
|
+
task :list, [:count] => [:environment] do |_t, args|
|
|
34
|
+
count = (args[:count] || 100).to_i
|
|
35
|
+
query = ActiveStorage::Blob.order(id: :desc).limit(count)
|
|
36
|
+
digits = query.ids.inject(0) { |ret, id|
|
|
37
|
+
size = id.to_s.size
|
|
38
|
+
[size, ret].max
|
|
39
|
+
}
|
|
24
40
|
|
|
25
41
|
ActiveStorage::Tasks.print_blob_header(digits: digits)
|
|
26
42
|
query.each do |blob|
|
|
@@ -28,14 +44,14 @@ namespace :asdb do
|
|
|
28
44
|
end
|
|
29
45
|
end
|
|
30
46
|
|
|
31
|
-
desc
|
|
47
|
+
desc "ActiveStorageDB: download attachment by blob id"
|
|
32
48
|
task :download, [:blob_id, :destination] => [:environment] do |_t, args|
|
|
33
49
|
blob_id = args[:blob_id]&.strip
|
|
34
50
|
destination = args[:destination]&.strip || Dir.pwd
|
|
35
|
-
abort(
|
|
51
|
+
abort("Required arguments: source blob id, destination path") if blob_id.blank? || destination.blank?
|
|
36
52
|
|
|
37
53
|
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
|
38
|
-
abort(
|
|
54
|
+
abort("Source file not found") unless blob
|
|
39
55
|
|
|
40
56
|
destination = "#{destination}/#{blob.filename}" if Dir.exist?(destination)
|
|
41
57
|
dir = File.dirname(destination)
|
|
@@ -45,20 +61,23 @@ namespace :asdb do
|
|
|
45
61
|
puts "#{ret} bytes written - #{destination}"
|
|
46
62
|
end
|
|
47
63
|
|
|
48
|
-
desc
|
|
64
|
+
desc "ActiveStorageDB: search attachment by filename (or part of it)"
|
|
49
65
|
task :search, [:filename] => [:environment] do |_t, args|
|
|
50
66
|
filename = args[:filename]&.strip
|
|
51
|
-
abort(
|
|
67
|
+
abort("Required arguments: filename") if filename.blank?
|
|
52
68
|
|
|
53
|
-
blobs = ActiveStorage::Blob.where(
|
|
69
|
+
blobs = ActiveStorage::Blob.where("filename LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(filename)}%").order(id: :desc)
|
|
54
70
|
if blobs.any?
|
|
55
|
-
digits = blobs.ids.inject(0) { |ret, id|
|
|
71
|
+
digits = blobs.ids.inject(0) { |ret, id|
|
|
72
|
+
size = id.to_s.size
|
|
73
|
+
[size, ret].max
|
|
74
|
+
}
|
|
56
75
|
ActiveStorage::Tasks.print_blob_header(digits: digits)
|
|
57
76
|
blobs.each do |blob|
|
|
58
77
|
ActiveStorage::Tasks.print_blob(blob, digits: digits)
|
|
59
78
|
end
|
|
60
79
|
else
|
|
61
|
-
puts
|
|
80
|
+
puts "No results"
|
|
62
81
|
end
|
|
63
82
|
end
|
|
64
83
|
end
|