solid_storage 0.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 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: []