solid_storage 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8f89a01c7211be77a1c7dcb74fe3480c419161a94da0361cbfbd41744e5cdfd
4
+ data.tar.gz: 479ad5ec90754640e64e0f42d75ca8368bb2b6f5c84af394354a1fef7ce8a655
5
+ SHA512:
6
+ metadata.gz: adc433251f4bdf44454cdc0b541977f909786969beef04c056d96ec6da171852c1c2f695b68a56750f5416273ff83161619c73742aebfa44859769c639875cf0
7
+ data.tar.gz: 00cab3fcf0359e82363a7f6251f116bd98c4b3699f8ba183982c8739333a1c1ca73cd392dffa8fea5aa361a82dc9d73fe93ae498ea2ac4f71bdd8e9c469f82f6
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Nick Pezza
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.
data/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # Solid Storage
2
+
3
+ Active Storage service adapter that stores blobs in the database and serves
4
+ them using X-Sendfile.
5
+
6
+ ## Installation
7
+
8
+ 1. `bundle add solid_storage`
9
+ 2. `bin/rails solid_storage:install`
10
+
11
+ This will configure Solid Storage as an Active Storage adapter by overwritting `config/storage.yml`, create `db/storage_schema.rb`, and set the production adapter to `:solid_storage`.
12
+
13
+ ## License
14
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidStorage::FilesController < ActiveStorage::DiskController
4
+ include ActiveStorage::FileServer
5
+
6
+ def show
7
+ if key = decode_verified_key
8
+ @file = named_disk_service(key[:service_name]).find(key[:key])
9
+ serve_file @file.tempfile, content_type: key[:content_type],
10
+ disposition: key[:disposition]
11
+ else
12
+ head :not_found
13
+ end
14
+ end
15
+
16
+ def update
17
+ if token = decode_verified_token
18
+ if acceptable_content?(token)
19
+ named_disk_service(token[:service_name]).upload token[:key],
20
+ request.body, checksum: token[:checksum]
21
+ head :no_content
22
+ else
23
+ head :unprocessable_entity
24
+ end
25
+ else
26
+ head :not_found
27
+ end
28
+ rescue ActiveStorage::IntegrityError
29
+ head :unprocessable_entity
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ class SolidStorage::File < SolidStorage::Record
2
+ belongs_to :blob, class_name: "ActiveStorage::Blob",
3
+ primary_key: :key, foreign_key: :key, optional: true
4
+
5
+ def tempfile
6
+ @tempfile ||= Tempfile.new.tap do |file|
7
+ file.binmode
8
+ file.write(data)
9
+ file.rewind
10
+ end
11
+ end
12
+
13
+ def io
14
+ StringIO.new(data)
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+
2
+ module SolidStorage
3
+ class Record < ActiveRecord::Base
4
+ self.abstract_class = true
5
+
6
+ connects_to(**SolidStorage.connects_to) if SolidStorage.connects_to.present?
7
+ end
8
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Rails.application.routes.draw do
2
+ scope ActiveStorage.routes_prefix do
3
+ get "/solid_storage/:encoded_key/*filename", to: "solid_storage/files#show", as: :rails_solid_storage_service
4
+ put "/solid_storage/:encoded_token", to: "solid_storage/files#update", as: :update_rails_solid_storage_service
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSolidStorageFiles < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :solid_storage_files do |t|
4
+ t.binary :data, limit: 536870912
5
+ t.string :key
6
+ t.index :key
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,142 @@
1
+ require "active_storage/service"
2
+
3
+ module ActiveStorage
4
+ class Service::SolidStorageService < ::ActiveStorage::Service
5
+ def initialize(public: false, **options)
6
+ @public = public
7
+ end
8
+
9
+ def find(key)
10
+ SolidStorage::File.find_by!(key:)
11
+ end
12
+
13
+ def upload(key, io, checksum: nil, **)
14
+ instrument(:upload, key:, checksum:) do
15
+ file = SolidStorage::File.create!(key:, data: io.read)
16
+ ensure_integrity_of(key, checksum) if checksum
17
+ file
18
+ end
19
+ end
20
+
21
+ def download(key, &block)
22
+ io = SolidStorage::File.find_by!(key:).io
23
+ if block
24
+ instrument(:streaming_download, key:) do
25
+ while data = io.read(5.megabytes)
26
+ yield data
27
+ end
28
+ end
29
+ else
30
+ instrument(:download, key:) { io.read }
31
+ end
32
+ rescue ActiveRecord::RecordNotFound
33
+ raise ActiveStorage::FileNotFoundError
34
+ end
35
+
36
+ def download_chunk(key, range)
37
+ instrument(:download_chunk, key:, range:) do
38
+ args =
39
+ if sqlite? then "data, #{range.begin + 1}, #{range.size}"
40
+ else
41
+ "data FROM #{range.begin + 1} FOR #{range.size}"
42
+ end
43
+
44
+ SolidStorage::File.select("SUBSTRING(#{args}) as chunk").
45
+ find_by!(key:).chunk
46
+ rescue ActiveRecord::RecordNotFound
47
+ raise ActiveStorage::FileNotFoundError
48
+ end
49
+ end
50
+
51
+ def compose(source_keys, destination_key, **)
52
+ SolidStorage::File.create(
53
+ key: destination_key,
54
+ data: SolidStorage::File.where(key: source_keys).in_order_of(:key, source_keys).map(&:data).join
55
+ )
56
+ end
57
+
58
+ def delete_prefixed(prefix)
59
+ instrument :delete_prefixed, prefix: do
60
+ SolidStorage::File.where(
61
+ SolidStorage::File.arel_table[:key].matches("#{prefix}%").to_sql
62
+ ).destroy_all
63
+ end
64
+ end
65
+
66
+ def delete(key)
67
+ instrument(:delete, key:) { SolidStorage::File.where(key:).destroy_all }
68
+ end
69
+
70
+ def exist?(key)
71
+ instrument(:exist, key:) do |payload|
72
+ payload[:exist] = SolidStorage::File.exists?(key:)
73
+ end
74
+ end
75
+
76
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
77
+ instrument :url, key: do |payload|
78
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
79
+ { key:, content_type:, content_length:, checksum:,
80
+ service_name: name }, expires_in:, purpose: :blob_token
81
+ )
82
+
83
+ payload[:url] = url_helpers.update_rails_solid_storage_service_url(
84
+ verified_token_with_expiration, url_options
85
+ )
86
+ end
87
+ end
88
+
89
+ def headers_for_direct_upload(key, content_type:, **)
90
+ { "Content-Type" => content_type }
91
+ end
92
+
93
+ private
94
+
95
+ def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
96
+ generate_url(key, expires_in:, filename:, content_type:, disposition:)
97
+ end
98
+
99
+ def public_url(key, filename:, content_type: nil, disposition: :attachment, **)
100
+ generate_url(key, expires_in: nil, filename:, content_type:, disposition:)
101
+ end
102
+
103
+ def generate_url(key, expires_in:, filename:, content_type:, disposition:)
104
+ content_disposition = content_disposition_with(type: disposition, filename:)
105
+ verified_key_with_expiration =
106
+ generate_key(key:, expires_in:, filename:, content_type: content_type, disposition: content_disposition,
107
+ purpose: :blob_key)
108
+
109
+ if url_options.blank?
110
+ raise ArgumentError, "Cannot generate URL for #{filename} using Solid Storage, please set ActiveStorage::Current.url_options."
111
+ end
112
+
113
+ url_helpers.rails_solid_storage_service_url(verified_key_with_expiration, filename: filename, **url_options)
114
+ end
115
+
116
+ def generate_key(key:, expires_in:, filename:, content_type:, disposition:, purpose:)
117
+ verified_key_with_expiration = ActiveStorage.verifier.generate(
118
+ { key:, disposition:, content_type:,
119
+ service_name: name }, expires_in:, purpose:
120
+ )
121
+ end
122
+
123
+ def sqlite?
124
+ SolidStorage::File.connection.adapter_name.downcase.inquiry.sqlite?
125
+ end
126
+
127
+ def url_helpers
128
+ @url_helpers ||= Rails.application.routes.url_helpers
129
+ end
130
+
131
+ def url_options
132
+ ActiveStorage::Current.url_options
133
+ end
134
+
135
+ def ensure_integrity_of(key, checksum)
136
+ return if OpenSSL::Digest::MD5.base64digest(find(key).data) == checksum
137
+
138
+ delete key
139
+ raise ActiveStorage::IntegrityError
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidStorage::InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def copy_files
7
+ template "db/storage_schema.rb"
8
+ template "config/storage.yml", force: true
9
+
10
+ gsub_file("config/environments/production.rb", /config.active_storage.service = .*$/) do
11
+ "config.active_storage.service = :solid_storage"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ test:
2
+ service: Disk
3
+ root: <%%= Rails.root.join("tmp/storage") %>
4
+
5
+ local:
6
+ service: Disk
7
+ root: <%%= Rails.root.join("storage") %>
8
+ public: true
9
+ cache_control: "public, max-age=3600"
10
+
11
+ solid_storage:
12
+ service: SolidStorage
13
+
14
+ # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
15
+ # amazon:
16
+ # service: S3
17
+ # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
18
+ # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
19
+ # region: us-east-1
20
+ # bucket: your_own_bucket-<%= Rails.env %>
21
+
22
+ # Remember not to checkin your GCS keyfile to a repository
23
+ # google:
24
+ # service: GCS
25
+ # project: your_project
26
+ # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
27
+ # bucket: your_own_bucket-<%= Rails.env %>
28
+
29
+ # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
30
+ # microsoft:
31
+ # service: AzureStorage
32
+ # storage_account_name: your_account_name
33
+ # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
34
+ # container: your_container_name-<%= Rails.env %>
35
+
36
+ # mirror:
37
+ # service: Mirror
38
+ # primary: local
39
+ # mirrors: [ amazon, google, microsoft ]
@@ -0,0 +1,9 @@
1
+ ActiveRecord::Schema[7.1].define(version: 1) do
2
+ create_table "solid_storage_files", force: :cascade do |t|
3
+ t.binary "data", limit: 536870912
4
+ t.string "key"
5
+ t.datetime "created_at", null: false
6
+ t.datetime "updated_at", null: false
7
+ t.index ["key"], name: "index_solid_storage_files_on_key"
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module SolidStorage
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace SolidStorage
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module SolidStorage
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ require "solid_storage/version"
2
+ require "solid_storage/engine"
3
+ require "active_storage/service/solid_storage_service"
4
+
5
+ module SolidStorage
6
+ mattr_accessor :connects_to
7
+ end
@@ -0,0 +1,6 @@
1
+ desc "Copy over migrations"
2
+ namespace :solid_storage do
3
+ task install: :environment do
4
+ Rails::Command.invoke :generate, [ "solid_storage:install" ]
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solid_storage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Pezza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activestorage
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: railties
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '7.2'
55
+ description: Database-backed Active Stroage adapter.
56
+ email:
57
+ - pezza@hey.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - app/controllers/solid_storage/files_controller.rb
66
+ - app/models/solid_storage/file.rb
67
+ - app/models/solid_storage/record.rb
68
+ - config/routes.rb
69
+ - db/migrate/20241016013117_create_solid_storage_files.rb
70
+ - lib/active_storage/service/solid_storage_service.rb
71
+ - lib/generators/solid_storage/install/install_generator.rb
72
+ - lib/generators/solid_storage/install/templates/config/storage.yml
73
+ - lib/generators/solid_storage/install/templates/db/storage_schema.rb
74
+ - lib/solid_storage.rb
75
+ - lib/solid_storage/engine.rb
76
+ - lib/solid_storage/version.rb
77
+ - lib/tasks/solid_storage_tasks.rake
78
+ homepage: http://github.com/npezza93/solid_storage
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: http://github.com/npezza93/solid_storage
83
+ source_code_uri: http://github.com/npezza93/solid_storage
84
+ rubygems_mfa_required: 'true'
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 3.1.0
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.5.18
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Database-backed Active Storage adapter.
104
+ test_files: []