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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02204d5bc6e8a8e41d30c3d38ae5308345dce145c9b80f98127dacd8c74ac3ba
4
- data.tar.gz: 2e15d75e150d8d3be1634f21f7c75b6018fa6a4b75f079aa301de2a526ff3f43
3
+ metadata.gz: 1212a9c234be19bf6444a03c0bdf66ab92397ad4527ac9e0ce029d0440724a34
4
+ data.tar.gz: 7ca4cb2634cac459ee8caf886887293f69d00f989128a41d108b3023bf6ee7e0
5
5
  SHA512:
6
- metadata.gz: 0f94e2258f91ef67cba241bca7936225f9f11076b307114a81df7b6f9b5d20aa7d126dd136c94d31813e045395635f3ad6de36b5f8eaf638d4b1c91d319db28a
7
- data.tar.gz: c244fed09f914b4ce819577949ae12a33f80c8d4f0cee9987d3efd0952eb8a9048a52ba5aaaec959e1ef1b51d1045585313fb0ca63a1827d6684605a1853b5fe
6
+ metadata.gz: 6773b507eae6af885133918d77876e310b946f19221aeeea2a4b122805b852699350d86556a0b04e0dbdd3dda98def4580756e51af0bc44f2611afe45209b04d
7
+ data.tar.gz: 6b1617c425df194ae4bcf2b415e11fd8cb16eb76698677036a7a981188120dd71ece4a6307654a336a74d86327cb94bb7fc223347f4db973f9f9e87aff8be944
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # ActiveStorage::PostgreSQL
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/active_storage-postgresql.svg)](https://badge.fury.io/rb/active_storage-postgresql)
4
+ [![Build Status](https://travis-ci.com/lsylvester/active_storage-postgresql.svg?branch=master)](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', git: "https://github.com/lsylvester/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
- before_create do
5
- self.oid ||= lo_creat
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, 0)
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
- transaction do
37
- self.oid = lo_import(path)
38
- end
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 = lo_tell(@lo)
43
- lo_seek(@lo, 0,2)
44
- lo_tell(@lo).tap do
45
- lo_seek(@lo, current_position,0)
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
- before_destroy do
50
- self.class.connection.raw_connection.lo_unlink(oid)
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
 
@@ -1,5 +1,5 @@
1
1
  module ActiveStorage
2
2
  module PostgreSQL
3
- VERSION = '0.1.0'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
@@ -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
- if io.respond_to?(:to_path)
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 url(key, expires_in:, filename:, disposition:, content_type:)
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
- verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
92
-
93
- generated_url =
94
- url_helpers.rails_disk_service_url(
95
- verified_key_with_expiration,
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
- { expires_in: expires_in,
118
- purpose: :blob_token }
118
+ expires_in: expires_in,
119
+ purpose: :blob_token
119
120
  )
120
121
 
121
- generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
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 current_host
140
- ActiveStorage::Current.host
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.1.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: 2018-07-27 00:00:00.000000000 Z
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: '5.2'
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: '5.2'
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
- rubyforge_project:
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