active_storage_db 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6159ecad022c402fd439ccb18c5acd208a6158817f8e4f6b98f3f63b0d2a8d97
4
+ data.tar.gz: 588c92f89efdb609b4127ae4f37183f97133feab7db3be798a971c769bbe1266
5
+ SHA512:
6
+ metadata.gz: 89f0b63ec3b310f68fefc448b8dd5d03409d19bac1a5f9fc15ae58eaea3b1276b3431f2f6a685756c724e0c77e094b0e141ad9c3e4f767de05d771c0111bcf03
7
+ data.tar.gz: fd416152e2fbc2fd13161b4929b261753262571a6b76e3fd54dcaeae2faa7e7d83729c0a4049724c0b09efce63649e9186ac8625e434837ad915070f67db7577
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Mattia Roccoberton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ # Active Storage DB
2
+ An Active Storage service upload/download plugin that stores files in a PostgreSQL or MySQL database.
3
+
4
+ Main features:
5
+ - all service methods implemented;
6
+ - data is saved using a binary field (or blob);
7
+ - RSpec tests.
8
+
9
+ ## Installation
10
+ - Setup Active Storage in your Rails application
11
+ - Add this line to your Gemfile: `gem 'active_storage_db'`
12
+ - And execute: `bundle`
13
+ - Install gem migrations: `bin/rails active_storage_db:install:migrations`
14
+ - And execute: `bin/rails db:migrate`
15
+ - Add to your `config/routes.rb`: `mount ActiveStorageDB::Engine => '/active_storage_db'`
16
+ - Change Active Storage service in *config/environments/development.rb* to: `config.active_storage.service = :db`
17
+ - Add to *config/storage.yml*:
18
+ ```
19
+ db:
20
+ service: DB
21
+ ```
22
+
23
+ ## Do you like it? Star it!
24
+ If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
25
+
26
+ ## Contributors
27
+ - [Mattia Roccoberton](https://blocknot.es/): author
28
+ - Inspired by [activestorage-database-service](https://github.com/TitovDigital/activestorage-database-service) project
29
+
30
+ ## License
31
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'ActiveStorageDB'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
20
+ load 'rails/tasks/engine.rake'
21
+
22
+ load 'rails/tasks/statistics.rake'
23
+
24
+ require 'bundler/gem_tasks'
25
+
26
+ require 'rake/testtask'
27
+
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'spec'
30
+ t.pattern = 'spec/**/*_spec.rb'
31
+ t.verbose = false
32
+ end
33
+
34
+ task default: :test
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageDB
4
+ class FilesController < ActiveStorage::BaseController
5
+ skip_forgery_protection
6
+
7
+ def show
8
+ if (key = decode_verified_key)
9
+ serve_file(key[:key], content_type: key[:content_type], disposition: key[:disposition])
10
+ else
11
+ head :not_found
12
+ end
13
+ rescue ActiveStorage::FileNotFoundError
14
+ head :not_found
15
+ end
16
+
17
+ def update
18
+ if (token = decode_verified_token)
19
+ if acceptable_content?(token)
20
+ db_service.upload(token[:key], request.body, checksum: token[:checksum])
21
+ else
22
+ head :unprocessable_entity
23
+ end
24
+ else
25
+ head :not_found
26
+ end
27
+ rescue ActiveStorage::IntegrityError
28
+ head :unprocessable_entity
29
+ end
30
+
31
+ private
32
+
33
+ def db_service
34
+ ActiveStorage::Blob.service
35
+ end
36
+
37
+ def decode_verified_key
38
+ ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
39
+ end
40
+
41
+ def serve_file(key, content_type:, disposition:)
42
+ options = {
43
+ type: content_type || DEFAULT_SEND_FILE_TYPE,
44
+ disposition: disposition || DEFAULT_SEND_FILE_DISPOSITION
45
+ }
46
+ send_data db_service.download(key), options
47
+ end
48
+
49
+ def decode_verified_token
50
+ ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
51
+ end
52
+
53
+ def acceptable_content?(token)
54
+ token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageDB
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageDB
4
+ class File < ApplicationRecord
5
+ validates :ref,
6
+ presence: true,
7
+ allow_blank: false,
8
+ uniqueness: { case_sensitive: false }
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
4
+ inflect.acronym 'DB'
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveStorageDB::Engine.routes.draw do
4
+ get '/files/:encoded_key/*filename', to: 'files#show', as: :service
5
+ put '/files/:encoded_token', to: 'files#update', as: :update_service
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActiveStorageDBFiles < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :active_storage_db_files do |t|
6
+ t.string :ref, null: false
7
+ t.binary :data, null: false
8
+ t.datetime :created_at, null: false
9
+
10
+ t.index [:ref], unique: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Service::DBService < Service # rubocop:disable Style/ClassAndModuleChildren
5
+ def initialize(**_config)
6
+ @chunk_size = ENV.fetch('ASDB_CHUNK_SIZE') { 1.megabytes }
7
+ end
8
+
9
+ def upload(key, io, checksum: nil, **)
10
+ instrument :upload, key: key, checksum: checksum do
11
+ file = ::ActiveStorageDB::File.create!(ref: key, data: io.read)
12
+ ensure_integrity_of(key, checksum) if checksum
13
+ file
14
+ end
15
+ end
16
+
17
+ def download(key, &block)
18
+ if block_given?
19
+ instrument :streaming_download, key: key do
20
+ stream(key, &block)
21
+ end
22
+ else
23
+ instrument :download, key: key do
24
+ record = ::ActiveStorageDB::File.find_by(ref: key)
25
+ record&.data || raise(ActiveStorage::FileNotFoundError)
26
+ end
27
+ end
28
+ end
29
+
30
+ def download_chunk(key, range)
31
+ instrument :download_chunk, key: key, range: range do
32
+ 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)
35
+ end
36
+ end
37
+
38
+ def delete(key)
39
+ instrument :delete, key: key do
40
+ ::ActiveStorageDB::File.find_by(ref: key)&.destroy
41
+ end
42
+ end
43
+
44
+ def delete_prefixed(prefix)
45
+ instrument :delete_prefixed, prefix: prefix do
46
+ ::ActiveStorageDB::File.where('ref LIKE ?', "#{prefix}%").destroy_all
47
+ end
48
+ end
49
+
50
+ def exist?(key)
51
+ instrument :exist, key: key do |payload|
52
+ answer = ::ActiveStorageDB::File.where(ref: key).exists?
53
+ payload[:exist] = answer
54
+ answer
55
+ end
56
+ end
57
+
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
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
87
+ instrument :url, key: key do |payload|
88
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
89
+ {
90
+ key: key,
91
+ content_type: content_type,
92
+ content_length: content_length,
93
+ checksum: checksum
94
+ },
95
+ expires_in: expires_in,
96
+ purpose: :blob_token
97
+ )
98
+ generated_url = url_helpers.update_service_url(
99
+ verified_token_with_expiration,
100
+ host: current_host
101
+ )
102
+ payload[:url] = generated_url
103
+
104
+ generated_url
105
+ end
106
+ end
107
+
108
+ def headers_for_direct_upload(_key, content_type:, **)
109
+ { 'Content-Type' => content_type }
110
+ end
111
+
112
+ private
113
+
114
+ def current_host
115
+ ActiveStorage::Current.host
116
+ end
117
+
118
+ def ensure_integrity_of(key, checksum)
119
+ file = ::ActiveStorageDB::File.find_by(ref: key)
120
+ return if Digest::MD5.base64digest(file.data) == checksum
121
+
122
+ delete(key)
123
+ raise ActiveStorage::IntegrityError
124
+ end
125
+
126
+ def stream(key)
127
+ size =
128
+ ::ActiveStorageDB::File.select('OCTET_LENGTH(data) AS size').find_by(ref: key)&.size ||
129
+ raise(ActiveStorage::FileNotFoundError)
130
+ (size / @chunk_size.to_f).ceil.times.each do |i|
131
+ range = (i * @chunk_size..(i + 1) * @chunk_size - 1)
132
+ yield download_chunk(key, range)
133
+ end
134
+ end
135
+
136
+ def url_helpers
137
+ @url_helpers ||= ::ActiveStorageDB::Engine.routes.url_helpers
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_storage_db/engine'
4
+ require 'active_storage/service/db_service'
5
+
6
+ module ActiveStorageDB
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageDB
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace ActiveStorageDB
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageDB
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc 'ActiveStorageDB sync'
4
+ # task :active_storage_db do
5
+ # end
metadata ADDED
@@ -0,0 +1,225 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_storage_db
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattia Roccoberton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-11 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: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.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: 6.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 6.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: capybara
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.33.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.33.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: database_cleaner-active_record
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.8.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.8.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_bot_rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 6.1.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 6.1.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: mysql2
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.5.3
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.5.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: pg
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.2.3
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.2.3
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.13.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.13.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec-rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 4.0.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 4.0.1
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.89.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.89.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: selenium-webdriver
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 3.142.7
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 3.142.7
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.18.5
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.18.5
181
+ description: An ActiveStorage service plugin to store files in database.
182
+ email:
183
+ - mattiaroccoberton@nebulab.it
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - MIT-LICENSE
189
+ - README.md
190
+ - Rakefile
191
+ - app/controllers/active_storage_db/files_controller.rb
192
+ - app/models/active_storage_db/application_record.rb
193
+ - app/models/active_storage_db/file.rb
194
+ - config/initializers/inflections.rb
195
+ - config/routes.rb
196
+ - db/migrate/20200702202022_create_active_storage_db_files.rb
197
+ - lib/active_storage/service/db_service.rb
198
+ - lib/active_storage_db.rb
199
+ - lib/active_storage_db/engine.rb
200
+ - lib/active_storage_db/version.rb
201
+ - lib/tasks/active_storage_db_tasks.rake
202
+ homepage: https://blocknot.es
203
+ licenses:
204
+ - MIT
205
+ metadata: {}
206
+ post_install_message:
207
+ rdoc_options: []
208
+ require_paths:
209
+ - lib
210
+ required_ruby_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ required_rubygems_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ">="
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ requirements: []
221
+ rubygems_version: 3.1.4
222
+ signing_key:
223
+ specification_version: 4
224
+ summary: ActiveStorage DB Service
225
+ test_files: []