better_storage 0.2.0 → 1.0.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: c648e03cd0078c3c9db4ca303108a6e7d94ead8cf9209c8254a142ab6e76bfe0
4
- data.tar.gz: 8ed57e8a1d12ba42c0d5131783a1dafeb421fc1f22d2b1a85003e80df004aa85
3
+ metadata.gz: 19e4e2bb937a1d4d57791bc27846be1bcea266eb868b0b37ffcb9049a98236bd
4
+ data.tar.gz: 0f7ccaf756b3a4e1200546d0e2aab58a703b6780937326ea5256a3d6c54a2d36
5
5
  SHA512:
6
- metadata.gz: 7e823c08ad67aa4abf3f88d989c10839fa88b511f31fded4793d61af4acc4c37f16a601ceca9b59d284d242785f0c6e866aa56c48d04e81f768133e87eadc137
7
- data.tar.gz: 49e72c46273de6ebd737674cf284477968ba548701edf5a42e55768119c461a7c49eaba2588331d23cbe0a3ad57d71abfb440174625b58f263248876f3b82186
6
+ metadata.gz: a5f40011f175c89f4ac285210bc1f4cce1338ead78b2addd9b3ea7064eb93f589b2bc062589a696b00f32da67bd76afc2c3c9d05f4fbc88b62c8c0efa3d89004
7
+ data.tar.gz: 7e04667198c980e608c2474529e72b489602214324908791a712ce92371e6967702a730bdc2695872705a6240591c751338ae44f28129e8ed20ec29ee87ccb51
data/README.md CHANGED
@@ -1,20 +1,63 @@
1
1
  # BetterStorage
2
2
 
3
- * Prefix ActiveStorage uploads with `dev` and customized datetime format.
4
- * Protect production files from deletion in development environment.
5
- * `public_url` method for ActiveStorage attachments and blobs.
3
+ A Rails ActiveStorage extension for the S3 service: predictable object paths, protection against accidental deletion of production files during local development, and zero-DB-query variant URL resolution.
6
4
 
7
- ## Usage
5
+ English | [繁體中文](README.zh-TW.md)
6
+
7
+ ## Why
8
+
9
+ | Pain point | Solution |
10
+ |------------|----------|
11
+ | 1. ActiveStorage uploads land at the bucket root with no structured path; multiple apps cannot share a bucket cleanly | `namespace` prefix with automatic date partitioning establishes a predictable path hierarchy and enables bucket sharing across applications |
12
+ | 2. Local development against production data risks deleting real production files when destroying records | Development uploads automatically receive a `dev/` prefix; deletes against keys outside the dev path are intercepted |
13
+ | 3. Resolving a variant URL via ActiveStorage costs multiple database queries, becoming a bottleneck when rendering many derived images | Variant blob keys are persisted in the source blob's metadata; URL resolution reads from in-memory metadata with zero database queries |
14
+
15
+ ## Supported Versions
16
+
17
+ | Rails | Ruby |
18
+ |-------|--------|
19
+ | 7.1.x | 2.7+ |
20
+ | 7.2.x | 3.1+ |
21
+ | 8.0.x | 3.2+ |
22
+ | 8.1.x | 3.2+ |
23
+
24
+ ## Installation
8
25
 
9
26
  ```ruby
10
- # better_storage.rb initializer
27
+ # Gemfile
28
+ gem "better_storage"
29
+ ```
30
+
31
+ ```bash
32
+ bundle install
33
+ ```
11
34
 
35
+ ## Configuration
36
+
37
+ Configure in `config/initializers/better_storage.rb`:
38
+
39
+ ```ruby
12
40
  BetterStorage.configure do |config|
13
- config.protect_production_files = true # default: Rails.env.development?
14
- config.prefix_date_format = "%Y/%m/%d" # default: "%Y%m"
41
+ config.namespace = "my_app"
42
+ # config.s3_endpoint = "https://my-bucket.s3.region.amazonaws.com"
43
+ # config.protect_production_files = true
44
+ # config.prefix_date_format = "%Y/%m/%d"
15
45
  end
16
46
  ```
17
47
 
48
+ ### Configuration fields
49
+
50
+ | Field | Default | Description |
51
+ |-------|---------|-------------|
52
+ | `namespace` | `nil` | The outermost prefix applied to all upload paths. Trailing `/` is stripped automatically. |
53
+ | `s3_endpoint` | Auto-derived | The base URL used when assembling public URLs. Lazily derived from `ActiveStorage::Blob.service.bucket.url` on first read; explicit values take precedence. May be set to a CDN URL (e.g. CloudFront) so that `public_url` returns CDN-served paths directly. |
54
+ | `protect_production_files` | `Rails.env.development?` | Enables the production file protection mechanism (see below). |
55
+ | `prefix_date_format` | `"%Y%m"` | The `strftime` format used for date partitioning. Set to `nil` or `false` to disable date partitioning. |
56
+
57
+ ## Usage
58
+
59
+ ### Public URL
60
+
18
61
  ```ruby
19
62
  class User < ApplicationRecord
20
63
  has_one_attached :avatar do |attachable|
@@ -23,25 +66,73 @@ class User < ApplicationRecord
23
66
  end
24
67
 
25
68
  user = User.first
26
- user.avatar.public_url
27
- user.avatar.public_url(:thumb)
69
+
70
+ user.avatar.public_url # original
71
+ user.avatar.public_url(:thumb) # named variant
72
+
73
+ # For any ActiveStorage::Blob
74
+ blob.public_url
28
75
  ```
29
76
 
30
- ## Installation
31
- Add this line to your application's Gemfile:
77
+ `public_url` concatenates `s3_endpoint` and the key directly without going through ActiveStorage's `service_url` signing flow. It is intended for publicly readable buckets. For private buckets, use ActiveStorage's native `attachment.url` to get a signed URL.
78
+
79
+ ### Upload paths
80
+
81
+ The blob key structure produced under `namespace = "my_app"` and `prefix_date_format = "%Y%m"`:
82
+
83
+ | Environment | Key example |
84
+ |-------------|-------------|
85
+ | development | `my_app/dev/202605/<token>` |
86
+ | production | `my_app/202605/<token>` |
87
+
88
+ Variant blobs are independent `ActiveStorage::Blob` instances and follow the same prefix scheme.
89
+
90
+ ### Production file protection
91
+
92
+ When `protect_production_files` is true:
93
+
94
+ - Any `delete` or `delete_prefixed` call against a key not under `<namespace>/dev/` is intercepted and returns `false`
95
+ - Enabled automatically when `Rails.env.development?` returns true
96
+
97
+ Typical use case: developing locally with a snapshot of production data, where operations such as `User#destroy_all` would otherwise wipe production S3 objects.
98
+
99
+ To temporarily disable:
32
100
 
33
101
  ```ruby
34
- gem "better_storage"
102
+ BetterStorage.config.protect_production_files = false
35
103
  ```
36
104
 
37
- ## Warning
105
+ ### Variant URL caching
106
+
107
+ When an attachment's `attachment.public_url(:style)` is invoked:
108
+
109
+ 1. **First call**: Triggers ActiveStorage's normal variant processing flow and writes the resulting variant blob key into the source blob's `metadata["bs_variants"][variation_digest]`
110
+ 2. **Subsequent calls**: Read the variant key directly from the source blob's `metadata` — **zero database queries**
111
+
112
+ Properties:
113
+
114
+ - **Persistent and consistent across workers / deploys**: the cache lives in the database `metadata` column, not in process-local memory
115
+ - **No manual invalidation needed**: the variant blob key never changes, so the metadata entry remains valid
116
+ - **Idempotent**: writes are skipped when the digest entry already exists
38
117
 
39
- `ActiveStorage.track_variants = true` is assumed. This gem is not tested with `ActiveStorage.track_variants = false`.
40
- With the false setting, `variant_class` is ActiveStorage::Variant instead of ActiveStorage::VariantWithRecord.
41
- ActiveStorage::Variant implements its own special blob key.
118
+ > **Note**: Dynamic transformations (e.g. `image.variant(resize_to_limit: [N, N])` with varying `N`) accumulate metadata entries on the source blob without bound. This is acceptable in practice — each entry is small (~150 bytes); 10000 entries ≈ 1.5MB. Named variants (the typical use case) are constant in size.
42
119
 
43
- ## Contributing
44
- Contribution directions go here.
120
+ ## Assumptions / Limitations
121
+
122
+ - **S3 service only**: All protection mechanisms and `public_url` semantics assume S3. Disk / GCS / Azure are not supported.
123
+ - **`ActiveStorage.track_variants = true` (Rails default)**: The variant cache mechanism depends on the `VariantWithRecord` flow. `track_variants = false` is not supported.
124
+ - **`public_url` assumes a publicly readable bucket**: For private buckets, use ActiveStorage's native `attachment.url` (which produces signed URLs).
125
+ - **Protection only intercepts deletes**: Upload, update, and other write operations are unaffected.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ bundle install
131
+ bundle exec rake test # default Rails version
132
+ bundle exec appraisal rails-7.1 rake test # specific Rails version
133
+ bundle exec rake coverage # merged coverage report across all 4 Rails versions
134
+ ```
45
135
 
46
136
  ## License
47
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
137
+
138
+ Released under the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -8,4 +8,15 @@ Rake::TestTask.new(:test) do |t|
8
8
  t.verbose = false
9
9
  end
10
10
 
11
- task default: :test
11
+ task default: :test
12
+
13
+ desc "跑 main + 全部 Appraisals 並合併 coverage 報告"
14
+ task :coverage do
15
+ rm_rf "coverage"
16
+ sh "bundle exec rake test"
17
+ sh "bundle exec appraisal rails-7.1 rake test"
18
+ sh "bundle exec appraisal rails-7.2 rake test"
19
+ sh "bundle exec appraisal rails-8.0 rake test"
20
+ sh "bundle exec appraisal rails-8.1 rake test"
21
+ puts "→ 報告:coverage/index.html"
22
+ end
@@ -0,0 +1,47 @@
1
+ module BetterStorage
2
+ class Configuration
3
+ UNSET = Object.new.freeze
4
+ private_constant :UNSET
5
+
6
+ def initialize
7
+ @s3_endpoint = UNSET
8
+ @namespace = UNSET
9
+ @protect_production_files = UNSET
10
+ @prefix_date_format = UNSET
11
+ end
12
+
13
+ def s3_endpoint
14
+ return @s3_endpoint unless @s3_endpoint.equal?(UNSET)
15
+ service = ActiveStorage::Blob.service
16
+ service.bucket.url if service.respond_to?(:bucket)
17
+ end
18
+
19
+ def s3_endpoint=(value)
20
+ @s3_endpoint = value
21
+ end
22
+
23
+ def namespace
24
+ @namespace.equal?(UNSET) ? nil : @namespace
25
+ end
26
+
27
+ def namespace=(value)
28
+ @namespace = value&.gsub(/\/$/, "")
29
+ end
30
+
31
+ def protect_production_files
32
+ @protect_production_files.equal?(UNSET) ? Rails.env.development? : @protect_production_files
33
+ end
34
+
35
+ def protect_production_files=(value)
36
+ @protect_production_files = value
37
+ end
38
+
39
+ def prefix_date_format
40
+ @prefix_date_format.equal?(UNSET) ? "%Y%m" : @prefix_date_format
41
+ end
42
+
43
+ def prefix_date_format=(value)
44
+ @prefix_date_format = value
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ module BetterStorage
2
+ module Patches
3
+ module Attachment
4
+ def public_url(style = :original)
5
+ return nil unless persisted?
6
+ key = style == :original ? self.key : variant_key_for(style)
7
+ BetterStorage.public_url(key)
8
+ end
9
+
10
+ private
11
+
12
+ def variant_key_for(style)
13
+ digest = variation_digest_for(style)
14
+ cached = BetterStorage::VariantMetadata.fetch(blob, digest)
15
+ return cached if cached
16
+ variant(style).processed
17
+ BetterStorage::VariantMetadata.fetch(blob, digest)
18
+ end
19
+
20
+ def variation_digest_for(style)
21
+ variant(style).variation.digest
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module BetterStorage
2
+ module Patches
3
+ module Blob
4
+ def key
5
+ self[:key] ||= begin
6
+ key = self.class.generate_unique_secure_token(length: self.class::MINIMUM_TOKEN_LENGTH)
7
+ BetterStorage.generate_blob_key(key)
8
+ end
9
+ end
10
+
11
+ def public_url
12
+ BetterStorage.public_url(key)
13
+ end
14
+
15
+ def delete
16
+ service.delete(key)
17
+ service.delete_prefixed(variant_prefix) if image?
18
+ end
19
+
20
+ def variant_prefix
21
+ "#{key}/variants/"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ module BetterStorage
2
+ # protect production files from deletion in development
3
+ module Patches
4
+ module S3ServiceProxy
5
+ def delete(key)
6
+ return false if should_protect?(key)
7
+ super
8
+ end
9
+
10
+ def delete_prefixed(prefix)
11
+ return false if should_protect?(prefix)
12
+ super
13
+ end
14
+
15
+ private
16
+
17
+ def should_protect?(key)
18
+ BetterStorage.protect_production_files && BetterStorage.protected_key?(key)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module BetterStorage
2
+ module Patches
3
+ module VariantRecord
4
+ def destroy
5
+ result = super
6
+ BetterStorage::VariantMetadata.delete(blob, variation_digest) if destroyed? && blob
7
+ result
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module BetterStorage
2
+ module Patches
3
+ module VariantWithRecord
4
+ def processed
5
+ super
6
+ cache_variant_key_in_metadata
7
+ self
8
+ end
9
+
10
+ private
11
+
12
+ def cache_variant_key_in_metadata
13
+ return if BetterStorage::VariantMetadata.fetch(blob, variation.digest)
14
+ BetterStorage::VariantMetadata.store(blob, variation.digest, @record.image.key)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,34 +1,18 @@
1
1
  module BetterStorage
2
2
  class Railtie < ::Rails::Railtie
3
-
4
- initializer "better_storage.configs" do
5
- config.after_initialize do |app|
6
- BetterStorage.s3_endpoint ||= ActiveStorage::Blob.service.bucket.url
7
- end
8
- end
9
-
10
- initializer "better_storage.blob" do
11
- ActiveSupport.on_load(:active_storage_blob) do
12
- prepend BetterStorage::Blob
13
- end
14
- end
15
-
16
- initializer "better_storage.variant" do
17
- ActiveSupport.on_load(:active_storage_blob) do
18
- ActiveStorage::Variant.prepend BetterStorage::Variant
19
- end
20
- end
21
-
22
- initializer "better_storage.attachment" do
3
+ initializer "better_storage.active_storage_patches" do
23
4
  ActiveSupport.on_load(:active_storage_blob) do
24
- ActiveStorage::Attachment.include BetterStorage::Attachment
5
+ prepend BetterStorage::Patches::Blob
6
+ ActiveStorage::Attachment.include BetterStorage::Patches::Attachment
7
+ ActiveStorage::VariantRecord.prepend BetterStorage::Patches::VariantRecord
8
+ ActiveStorage::VariantWithRecord.prepend BetterStorage::Patches::VariantWithRecord
25
9
  end
26
10
  end
27
11
 
28
- initializer "better_storage.service" do
12
+ initializer "better_storage.s3_service_patch" do
29
13
  config.after_initialize do
30
14
  require "active_storage/service/s3_service"
31
- ActiveStorage::Service::S3Service.prepend BetterStorage::S3ServiceProxy
15
+ ActiveStorage::Service::S3Service.prepend BetterStorage::Patches::S3ServiceProxy
32
16
  end
33
17
  end
34
18
  end
@@ -0,0 +1,31 @@
1
+ module BetterStorage
2
+ module VariantMetadata
3
+ KEY = "bs_variants"
4
+
5
+ module_function
6
+
7
+ def fetch(blob, digest)
8
+ blob.metadata.dig(KEY, digest)
9
+ end
10
+
11
+ def store(blob, digest, variant_blob_key)
12
+ blob.with_lock do
13
+ return if blob.metadata.dig(KEY, digest) == variant_blob_key
14
+ new_metadata = blob.metadata.deep_merge(KEY => { digest => variant_blob_key })
15
+ blob.update_columns(metadata: new_metadata)
16
+ blob.metadata = new_metadata
17
+ end
18
+ end
19
+
20
+ def delete(blob, digest)
21
+ blob.with_lock do
22
+ return unless blob.metadata.dig(KEY, digest)
23
+ new_metadata = blob.metadata.deep_dup
24
+ new_metadata[KEY].delete(digest)
25
+ new_metadata.delete(KEY) if new_metadata[KEY].empty?
26
+ blob.update_columns(metadata: new_metadata)
27
+ blob.metadata = new_metadata
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,3 +1,3 @@
1
1
  module BetterStorage
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,48 +1,58 @@
1
1
  require "better_storage/version"
2
+ require "better_storage/configuration"
3
+ require "better_storage/variant_metadata"
2
4
  require "better_storage/railtie"
3
5
 
4
6
  module BetterStorage
5
7
  extend ActiveSupport::Autoload
6
8
 
7
- autoload :Blob
8
- autoload :S3ServiceProxy
9
- autoload :Attachment
10
- autoload :Variant
11
-
12
- mattr_accessor :s3_endpoint
13
- mattr_accessor :namespace
14
- mattr_accessor :protect_production_files, default: Rails.env.development?
15
- mattr_accessor :prefix_date_format, default: "%Y%m"
16
-
17
- def self.configure
18
- yield self
19
- end
20
-
21
- def namespace=(value)
22
- @namespace = value.gsub(/\/$/, "")
23
- end
24
-
25
- def self.public_url(key)
26
- url = URI.parse(s3_endpoint)
27
- url.path += '/' unless url.path[-1] == '/'
28
- url.path += key
29
- url.to_s
30
- end
31
-
32
- def self.protected_key?(key)
33
- dev_prefix = [namespace, "dev"].compact.join("/")
34
- !key.start_with?(dev_prefix)
35
- end
36
-
37
- def self.prefix
38
- parts = []
39
- parts << namespace if namespace
40
- parts << "dev" if Rails.env.development?
41
- parts << Date.today.strftime(prefix_date_format) if prefix_date_format
42
- parts.join("/")
9
+ module Patches
10
+ extend ActiveSupport::Autoload
11
+ autoload :Blob
12
+ autoload :Attachment
13
+ autoload :VariantRecord
14
+ autoload :VariantWithRecord
15
+ autoload :S3ServiceProxy
43
16
  end
44
17
 
45
- def self.generate_blob_key(*key)
46
- [BetterStorage.prefix, key].compact.join("/")
18
+ class << self
19
+ delegate :s3_endpoint, :s3_endpoint=,
20
+ :namespace, :namespace=,
21
+ :protect_production_files, :protect_production_files=,
22
+ :prefix_date_format, :prefix_date_format=,
23
+ to: :config
24
+
25
+ def configure
26
+ yield config
27
+ config
28
+ end
29
+
30
+ def config
31
+ @config ||= Configuration.new
32
+ end
33
+
34
+ def prefix
35
+ parts = []
36
+ parts << config.namespace if config.namespace
37
+ parts << "dev" if Rails.env.development?
38
+ parts << Date.today.strftime(config.prefix_date_format) if config.prefix_date_format
39
+ parts.join("/")
40
+ end
41
+
42
+ def generate_blob_key(token)
43
+ [prefix, token].compact_blank.join("/")
44
+ end
45
+
46
+ def protected_key?(key)
47
+ dev_prefix = [config.namespace, "dev"].compact.join("/")
48
+ !key.start_with?(dev_prefix)
49
+ end
50
+
51
+ def public_url(key)
52
+ uri = URI.parse(config.s3_endpoint)
53
+ uri.path += "/" unless uri.path.end_with?("/")
54
+ uri.path += key
55
+ uri.to_s
56
+ end
47
57
  end
48
58
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yi Feng Xie
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-06-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -16,71 +15,20 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 7.0.5
18
+ version: '7.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 7.0.5
27
- - !ruby/object:Gem::Dependency
28
- name: sqlite3
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: pry
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: aws-sdk-s3
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: mocha
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'
83
- description: enhance active_storage
25
+ version: '7.1'
26
+ description: |
27
+ Extends Rails ActiveStorage's S3 service with three improvements:
28
+ structured upload paths via configurable namespace and date partitioning;
29
+ protection against accidental deletion of production files during local
30
+ development; and zero-database-query variant URL resolution backed by
31
+ source blob metadata.
84
32
  email:
85
33
  - yfxie@me.com
86
34
  executables: []
@@ -91,17 +39,19 @@ files:
91
39
  - README.md
92
40
  - Rakefile
93
41
  - lib/better_storage.rb
94
- - lib/better_storage/attachment.rb
95
- - lib/better_storage/blob.rb
42
+ - lib/better_storage/configuration.rb
43
+ - lib/better_storage/patches/attachment.rb
44
+ - lib/better_storage/patches/blob.rb
45
+ - lib/better_storage/patches/s3_service_proxy.rb
46
+ - lib/better_storage/patches/variant_record.rb
47
+ - lib/better_storage/patches/variant_with_record.rb
96
48
  - lib/better_storage/railtie.rb
97
- - lib/better_storage/s3_service_proxy.rb
98
- - lib/better_storage/variant.rb
49
+ - lib/better_storage/variant_metadata.rb
99
50
  - lib/better_storage/version.rb
100
51
  homepage: https://github.com/yfxie/better_storage/
101
52
  licenses:
102
53
  - MIT
103
54
  metadata: {}
104
- post_install_message:
105
55
  rdoc_options: []
106
56
  require_paths:
107
57
  - lib
@@ -116,8 +66,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
66
  - !ruby/object:Gem::Version
117
67
  version: '0'
118
68
  requirements: []
119
- rubygems_version: 3.4.10
120
- signing_key:
69
+ rubygems_version: 3.6.9
121
70
  specification_version: 4
122
- summary: enhance active_storage
71
+ summary: 'Rails ActiveStorage S3 extension: structured paths, production file protection,
72
+ zero-DB-query variant URLs'
123
73
  test_files: []
@@ -1,24 +0,0 @@
1
- module BetterStorage
2
- module Attachment
3
- def public_url(style = :original)
4
- return nil unless persisted?
5
-
6
- key = if style == :original
7
- self.key
8
- else
9
- variants = record.attachment_reflections[name]&.variants
10
- transformations = variants.fetch(style) do
11
- record_model_name = record.to_model.model_name.name
12
- raise ArgumentError, "Cannot find variant :#{style} for #{record_model_name}##{name}"
13
- end
14
- transformation_key = ActiveStorage::Variation.wrap(transformations).digest
15
- variant_cache_key = "#{id}-#{transformation_key}"
16
-
17
- Rails.cache.fetch([:better_storage_public_url, variant_cache_key, BetterStorage::VERSION]) do
18
- variant(style).processed.key
19
- end
20
- end
21
- BetterStorage.public_url(key)
22
- end
23
- end
24
- end
@@ -1,24 +0,0 @@
1
- module BetterStorage
2
- module Blob
3
- def key
4
- self[:key] ||= begin
5
- key = self.class.generate_unique_secure_token(length: self.class::MINIMUM_TOKEN_LENGTH)
6
- BetterStorage.generate_blob_key(key)
7
- end
8
- end
9
-
10
- def public_url
11
- BetterStorage.public_url(key)
12
- end
13
-
14
- def delete
15
- service.delete(key)
16
- # original implementation is `service.delete_prefixed("variants/#{key}/") if image?`
17
- service.delete_prefixed(variant_prefix) if image?
18
- end
19
-
20
- def variant_prefix
21
- "#{key}/variants/"
22
- end
23
- end
24
- end
@@ -1,18 +0,0 @@
1
- module BetterStorage
2
- # protect production files from deletion in development
3
- module S3ServiceProxy
4
- def delete(key)
5
- return false if should_protect?(key)
6
- super
7
- end
8
-
9
- def delete_prefixed(prefix)
10
- return false if should_protect?(prefix)
11
- super
12
- end
13
-
14
- def should_protect?(key)
15
- BetterStorage.protect_production_files && BetterStorage.protected_key?(key)
16
- end
17
- end
18
- end
@@ -1,11 +0,0 @@
1
- module BetterStorage
2
- module Variant
3
- # original implementation:
4
- # def key
5
- # "variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
6
- # end
7
- def key
8
- "#{blob.key}/variants/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
9
- end
10
- end
11
- end