solid_storage 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +14 -0
- data/Rakefile +8 -0
- data/app/controllers/solid_storage/files_controller.rb +31 -0
- data/app/models/solid_storage/file.rb +16 -0
- data/app/models/solid_storage/record.rb +8 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20241016013117_create_solid_storage_files.rb +11 -0
- data/lib/active_storage/service/solid_storage_service.rb +142 -0
- data/lib/generators/solid_storage/install/install_generator.rb +14 -0
- data/lib/generators/solid_storage/install/templates/config/storage.yml +39 -0
- data/lib/generators/solid_storage/install/templates/db/storage_schema.rb +9 -0
- data/lib/solid_storage/engine.rb +5 -0
- data/lib/solid_storage/version.rb +3 -0
- data/lib/solid_storage.rb +7 -0
- data/lib/tasks/solid_storage_tasks.rake +6 -0
- metadata +104 -0
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,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
|
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,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
|
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: []
|