active_storage-postgresql 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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