active_storage-postgresql 0.1.0 → 0.3.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 +5 -2
- data/app/controllers/active_storage/postgresql_controller.rb +75 -0
- data/config/routes.rb +6 -0
- data/lib/active_storage/postgresql/file.rb +42 -13
- data/lib/active_storage/postgresql/version.rb +1 -1
- data/lib/active_storage/service/postgresql_service.rb +48 -43
- metadata +25 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1212a9c234be19bf6444a03c0bdf66ab92397ad4527ac9e0ce029d0440724a34
|
4
|
+
data.tar.gz: 7ca4cb2634cac459ee8caf886887293f69d00f989128a41d108b3023bf6ee7e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6773b507eae6af885133918d77876e310b946f19221aeeea2a4b122805b852699350d86556a0b04e0dbdd3dda98def4580756e51af0bc44f2611afe45209b04d
|
7
|
+
data.tar.gz: 6b1617c425df194ae4bcf2b415e11fd8cb16eb76698677036a7a981188120dd71ece4a6307654a336a74d86327cb94bb7fc223347f4db973f9f9e87aff8be944
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# ActiveStorage::PostgreSQL
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/active_storage-postgresql)
|
4
|
+
[](https://travis-ci.com/lsylvester/active_storage-postgresql)
|
5
|
+
|
3
6
|
ActiveStorage Service to store files PostgeSQL.
|
4
7
|
|
5
8
|
Files are stored in PostgreSQL as Large Objects, which provide streaming style access.
|
@@ -7,13 +10,13 @@ More information about Large Objects can be found [here](https://www.postgresql.
|
|
7
10
|
|
8
11
|
This allows use of ActiveStorage on hosting platforms with ephemeral file systems such as Heroku without relying on third party storage services.
|
9
12
|
|
10
|
-
There are [some limits](https://dba.stackexchange.com/questions/127270/what-are-the-limits-of-postgresqls-large-object-facility) to the storage of Large Objects in PostgerSQL, so this is only recommended for prototyping and very small sites.
|
13
|
+
There are [some limits](https://dba.stackexchange.com/questions/127270/what-are-the-limits-of-postgresqls-large-object-facility) to the storage of Large Objects in PostgerSQL, so this is only recommended for prototyping and very small sites.
|
11
14
|
|
12
15
|
## Installation
|
13
16
|
Add this line to your application's Gemfile:
|
14
17
|
|
15
18
|
```ruby
|
16
|
-
gem 'active_storage-postgresql'
|
19
|
+
gem 'active_storage-postgresql'
|
17
20
|
```
|
18
21
|
|
19
22
|
And then execute:
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Serves files stored with the disk service in the same way that the cloud services do.
|
4
|
+
# This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
|
5
|
+
# Always go through the BlobsController, or your own authenticated controller, rather than directly
|
6
|
+
# to the service url.
|
7
|
+
|
8
|
+
class ActiveStorage::PostgresqlController < ActiveStorage::BaseController
|
9
|
+
|
10
|
+
skip_forgery_protection
|
11
|
+
|
12
|
+
def show
|
13
|
+
if key = decode_verified_key
|
14
|
+
response.headers["Content-Type"] = params[:content_type] || DEFAULT_SEND_FILE_TYPE
|
15
|
+
response.headers["Content-Disposition"] = params[:disposition] || DEFAULT_SEND_FILE_DISPOSITION
|
16
|
+
size = ActiveStorage::PostgreSQL::File.open(key[:key], &:size)
|
17
|
+
|
18
|
+
ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
|
19
|
+
|
20
|
+
if ranges.nil? || ranges.length > 1
|
21
|
+
# # No ranges, or multiple ranges (which we don't support):
|
22
|
+
# # TODO: Support multiple byte-ranges
|
23
|
+
self.status = :ok
|
24
|
+
range = 0..size-1
|
25
|
+
|
26
|
+
elsif ranges.empty?
|
27
|
+
head 416, content_range: "bytes */#{size}"
|
28
|
+
return
|
29
|
+
else
|
30
|
+
range = ranges[0]
|
31
|
+
self.status = :partial_content
|
32
|
+
response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
|
33
|
+
end
|
34
|
+
self.response_body = postgresql_service.download_chunk(key[:key], range)
|
35
|
+
else
|
36
|
+
head :not_found
|
37
|
+
end
|
38
|
+
rescue ActiveRecord::RecordNotFound
|
39
|
+
head :not_found
|
40
|
+
end
|
41
|
+
|
42
|
+
def update
|
43
|
+
if token = decode_verified_token
|
44
|
+
if acceptable_content?(token)
|
45
|
+
postgresql_service.upload token[:key], request.body, checksum: token[:checksum]
|
46
|
+
head :no_content
|
47
|
+
else
|
48
|
+
head :unprocessable_entity
|
49
|
+
end
|
50
|
+
else
|
51
|
+
head :not_found
|
52
|
+
end
|
53
|
+
rescue ActiveStorage::IntegrityError
|
54
|
+
head :unprocessable_entity
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def postgresql_service
|
59
|
+
ActiveStorage::Blob.service
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def decode_verified_key
|
64
|
+
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def decode_verified_token
|
69
|
+
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
70
|
+
end
|
71
|
+
|
72
|
+
def acceptable_content?(token)
|
73
|
+
token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length
|
74
|
+
end
|
75
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.routes.draw do
|
4
|
+
get "/rails/active_storage/postgresql/:encoded_key/*filename" => "active_storage/postgresql#show", as: :rails_postgresql_service
|
5
|
+
put "/rails/active_storage/postgresql/:encoded_token" => "active_storage/postgresql#update", as: :update_rails_postgresql_service
|
6
|
+
end
|
@@ -1,8 +1,31 @@
|
|
1
1
|
class ActiveStorage::PostgreSQL::File < ActiveRecord::Base
|
2
2
|
self.table_name = "active_storage_postgresql_files"
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
attribute :oid, :integer, default: ->{ connection.raw_connection.lo_creat }
|
5
|
+
attr_accessor :checksum, :io
|
6
|
+
attr_writer :digest
|
7
|
+
|
8
|
+
def digest
|
9
|
+
@digest ||= Digest::MD5.new
|
10
|
+
end
|
11
|
+
|
12
|
+
before_create :write_or_import, if: :io
|
13
|
+
before_create :verify_checksum, if: :checksum
|
14
|
+
|
15
|
+
def write_or_import
|
16
|
+
if io.respond_to?(:to_path)
|
17
|
+
import(io.to_path)
|
18
|
+
else
|
19
|
+
open(::PG::INV_WRITE) do |file|
|
20
|
+
while data = io.read(5.megabytes)
|
21
|
+
write(data)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def verify_checksum
|
28
|
+
raise ActiveStorage::IntegrityError unless digest.base64digest == checksum
|
6
29
|
end
|
7
30
|
|
8
31
|
def self.open(key, &block)
|
@@ -22,34 +45,40 @@ class ActiveStorage::PostgreSQL::File < ActiveRecord::Base
|
|
22
45
|
|
23
46
|
def write(content)
|
24
47
|
lo_write(@lo, content)
|
48
|
+
digest.update(content)
|
25
49
|
end
|
26
50
|
|
27
51
|
def read(bytes=size)
|
28
52
|
lo_read(@lo, bytes)
|
29
53
|
end
|
30
54
|
|
31
|
-
def seek(position)
|
32
|
-
lo_seek(@lo, position,
|
55
|
+
def seek(position, whence=PG::SEEK_SET)
|
56
|
+
lo_seek(@lo, position, whence)
|
33
57
|
end
|
34
58
|
|
35
59
|
def import(path)
|
36
|
-
|
37
|
-
|
38
|
-
|
60
|
+
self.oid = lo_import(path)
|
61
|
+
self.digest = Digest::MD5.file(path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def tell
|
65
|
+
lo_tell(@lo)
|
39
66
|
end
|
40
67
|
|
41
68
|
def size
|
42
|
-
current_position =
|
43
|
-
|
44
|
-
|
45
|
-
|
69
|
+
current_position = tell
|
70
|
+
seek(0, PG::SEEK_END)
|
71
|
+
tell.tap do
|
72
|
+
seek(current_position)
|
46
73
|
end
|
47
74
|
end
|
48
75
|
|
49
|
-
|
50
|
-
|
76
|
+
def unlink
|
77
|
+
lo_unlink(oid)
|
51
78
|
end
|
52
79
|
|
80
|
+
before_destroy :unlink
|
81
|
+
|
53
82
|
delegate :lo_seek, :lo_tell, :lo_import, :lo_read, :lo_write, :lo_open,
|
54
83
|
:lo_unlink, :lo_close, :lo_creat, to: 'self.class.connection.raw_connection'
|
55
84
|
|
@@ -6,36 +6,13 @@ module ActiveStorage
|
|
6
6
|
# Wraps a PostgreSQL database as an Active Storage service. See ActiveStorage::Service for the generic API
|
7
7
|
# documentation that applies to all services.
|
8
8
|
class Service::PostgreSQLService < Service
|
9
|
-
def initialize(**options)
|
9
|
+
def initialize(public: false, **options)
|
10
|
+
@public = public
|
10
11
|
end
|
11
12
|
|
12
|
-
def upload(key, io, checksum: nil)
|
13
|
+
def upload(key, io, checksum: nil, **)
|
13
14
|
instrument :upload, key: key, checksum: checksum do
|
14
|
-
|
15
|
-
ActiveStorage::PostgreSQL::File.create!(key: key) do |file|
|
16
|
-
file.import(io.to_path)
|
17
|
-
end
|
18
|
-
if checksum
|
19
|
-
unless Digest::MD5.file(io.to_path).base64digest == checksum
|
20
|
-
delete key
|
21
|
-
raise ActiveStorage::IntegrityError
|
22
|
-
end
|
23
|
-
end
|
24
|
-
else
|
25
|
-
md5 = Digest::MD5.new
|
26
|
-
ActiveStorage::PostgreSQL::File.create!(key: key).open(::PG::INV_WRITE) do |file|
|
27
|
-
while data = io.read(5.megabytes)
|
28
|
-
md5.update(data)
|
29
|
-
file.write(data)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
if checksum
|
33
|
-
unless md5.base64digest == checksum
|
34
|
-
delete key
|
35
|
-
raise ActiveStorage::IntegrityError
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
15
|
+
ActiveStorage::PostgreSQL::File.create!(key: key, io: io, checksum: checksum)
|
39
16
|
end
|
40
17
|
end
|
41
18
|
|
@@ -86,26 +63,50 @@ module ActiveStorage
|
|
86
63
|
end
|
87
64
|
end
|
88
65
|
|
89
|
-
def
|
66
|
+
def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
|
67
|
+
generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition)
|
68
|
+
end
|
69
|
+
|
70
|
+
def public_url(key, filename:, content_type: nil, disposition: :attachment, **)
|
71
|
+
generate_url(key, expires_in: nil, filename: filename, content_type: content_type, disposition: disposition)
|
72
|
+
end
|
73
|
+
|
74
|
+
def url(key, **options)
|
75
|
+
super
|
76
|
+
rescue NotImplementedError, ArgumentError
|
77
|
+
if @public
|
78
|
+
public_url(key, **options)
|
79
|
+
else
|
80
|
+
private_url(key, **options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate_url(key, expires_in:, filename:, disposition:, content_type:)
|
90
85
|
instrument :url, key: key do |payload|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
host: current_host,
|
97
|
-
filename: filename,
|
98
|
-
disposition: content_disposition_with(type: disposition, filename: filename),
|
86
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename)
|
87
|
+
verified_key_with_expiration = ActiveStorage.verifier.generate(
|
88
|
+
{
|
89
|
+
key: key,
|
90
|
+
disposition: content_disposition,
|
99
91
|
content_type: content_type
|
100
|
-
|
92
|
+
},
|
93
|
+
expires_in: expires_in,
|
94
|
+
purpose: :blob_key
|
95
|
+
)
|
101
96
|
|
97
|
+
generated_url = url_helpers.rails_postgresql_service_url(verified_key_with_expiration,
|
98
|
+
**url_options,
|
99
|
+
disposition: content_disposition,
|
100
|
+
content_type: content_type,
|
101
|
+
filename: filename
|
102
|
+
)
|
102
103
|
payload[:url] = generated_url
|
103
104
|
|
104
105
|
generated_url
|
105
106
|
end
|
106
107
|
end
|
107
108
|
|
108
|
-
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
109
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
109
110
|
instrument :url, key: key do |payload|
|
110
111
|
verified_token_with_expiration = ActiveStorage.verifier.generate(
|
111
112
|
{
|
@@ -114,11 +115,11 @@ module ActiveStorage
|
|
114
115
|
content_length: content_length,
|
115
116
|
checksum: checksum
|
116
117
|
},
|
117
|
-
|
118
|
-
purpose: :blob_token
|
118
|
+
expires_in: expires_in,
|
119
|
+
purpose: :blob_token
|
119
120
|
)
|
120
121
|
|
121
|
-
generated_url = url_helpers.
|
122
|
+
generated_url = url_helpers.update_rails_postgresql_service_url(verified_token_with_expiration, **url_options)
|
122
123
|
|
123
124
|
payload[:url] = generated_url
|
124
125
|
|
@@ -136,8 +137,12 @@ module ActiveStorage
|
|
136
137
|
@url_helpers ||= Rails.application.routes.url_helpers
|
137
138
|
end
|
138
139
|
|
139
|
-
def
|
140
|
-
ActiveStorage::Current.
|
140
|
+
def url_options
|
141
|
+
if ActiveStorage::Current.respond_to?(:url_options)
|
142
|
+
ActiveStorage::Current.url_options
|
143
|
+
else
|
144
|
+
{ host: ActiveStorage::Current.host }
|
145
|
+
end
|
141
146
|
end
|
142
147
|
end
|
143
148
|
end
|
metadata
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_storage-postgresql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lachlan Sylvester
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: pg
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: appraisal
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
description:
|
70
84
|
email:
|
71
85
|
- lachlan.sylvester@hypothetical.com.au
|
@@ -76,6 +90,8 @@ files:
|
|
76
90
|
- MIT-LICENSE
|
77
91
|
- README.md
|
78
92
|
- Rakefile
|
93
|
+
- app/controllers/active_storage/postgresql_controller.rb
|
94
|
+
- config/routes.rb
|
79
95
|
- db/migrate/20180530020601_create_active_storage_postgresql_tables.rb
|
80
96
|
- lib/active_storage/postgresql.rb
|
81
97
|
- lib/active_storage/postgresql/engine.rb
|
@@ -103,8 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
119
|
- !ruby/object:Gem::Version
|
104
120
|
version: '0'
|
105
121
|
requirements: []
|
106
|
-
|
107
|
-
rubygems_version: 2.7.3
|
122
|
+
rubygems_version: 3.3.4
|
108
123
|
signing_key:
|
109
124
|
specification_version: 4
|
110
125
|
summary: PostgreSQL Adapter for Active Storage
|