better_storage 0.2.1 → 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 +4 -4
- data/README.md +110 -20
- data/Rakefile +12 -1
- data/lib/better_storage/configuration.rb +47 -0
- data/lib/better_storage/patches/attachment.rb +25 -0
- data/lib/better_storage/patches/blob.rb +25 -0
- data/lib/better_storage/patches/s3_service_proxy.rb +22 -0
- data/lib/better_storage/patches/variant_record.rb +11 -0
- data/lib/better_storage/patches/variant_with_record.rb +18 -0
- data/lib/better_storage/railtie.rb +7 -25
- data/lib/better_storage/variant_metadata.rb +31 -0
- data/lib/better_storage/version.rb +1 -1
- data/lib/better_storage.rb +48 -38
- metadata +20 -84
- data/lib/better_storage/attachment.rb +0 -33
- data/lib/better_storage/blob.rb +0 -24
- data/lib/better_storage/s3_service_proxy.rb +0 -18
- data/lib/better_storage/variant.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19e4e2bb937a1d4d57791bc27846be1bcea266eb868b0b37ffcb9049a98236bd
|
|
4
|
+
data.tar.gz: 0f7ccaf756b3a4e1200546d0e2aab58a703b6780937326ea5256a3d6c54a2d36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5f40011f175c89f4ac285210bc1f4cce1338ead78b2addd9b3ea7064eb93f589b2bc062589a696b00f32da67bd76afc2c3c9d05f4fbc88b62c8c0efa3d89004
|
|
7
|
+
data.tar.gz: 7e04667198c980e608c2474529e72b489602214324908791a712ce92371e6967702a730bdc2695872705a6240591c751338ae44f28129e8ed20ec29ee87ccb51
|
data/README.md
CHANGED
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
# BetterStorage
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.namespace = "
|
|
14
|
-
config.
|
|
15
|
-
config.
|
|
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"
|
|
16
45
|
end
|
|
17
46
|
```
|
|
18
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
|
+
|
|
19
61
|
```ruby
|
|
20
62
|
class User < ApplicationRecord
|
|
21
63
|
has_one_attached :avatar do |attachable|
|
|
@@ -24,25 +66,73 @@ class User < ApplicationRecord
|
|
|
24
66
|
end
|
|
25
67
|
|
|
26
68
|
user = User.first
|
|
27
|
-
|
|
28
|
-
user.avatar.public_url
|
|
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
|
|
29
75
|
```
|
|
30
76
|
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
33
100
|
|
|
34
101
|
```ruby
|
|
35
|
-
|
|
102
|
+
BetterStorage.config.protect_production_files = false
|
|
36
103
|
```
|
|
37
104
|
|
|
38
|
-
|
|
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
|
|
39
117
|
|
|
40
|
-
`
|
|
41
|
-
With the false setting, `variant_class` is ActiveStorage::Variant instead of ActiveStorage::VariantWithRecord.
|
|
42
|
-
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.
|
|
43
119
|
|
|
44
|
-
##
|
|
45
|
-
|
|
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
|
+
```
|
|
46
135
|
|
|
47
136
|
## License
|
|
48
|
-
|
|
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,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,36 +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 ||= if ActiveStorage::Blob.service.respond_to?(:bucket)
|
|
7
|
-
ActiveStorage::Blob.service.bucket.url
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
initializer "better_storage.blob" do
|
|
13
|
-
ActiveSupport.on_load(:active_storage_blob) do
|
|
14
|
-
prepend BetterStorage::Blob
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
initializer "better_storage.variant" do
|
|
19
|
-
ActiveSupport.on_load(:active_storage_blob) do
|
|
20
|
-
ActiveStorage::Variant.prepend BetterStorage::Variant
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
initializer "better_storage.attachment" do
|
|
3
|
+
initializer "better_storage.active_storage_patches" do
|
|
25
4
|
ActiveSupport.on_load(:active_storage_blob) do
|
|
26
|
-
|
|
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
|
|
27
9
|
end
|
|
28
10
|
end
|
|
29
11
|
|
|
30
|
-
initializer "better_storage.
|
|
12
|
+
initializer "better_storage.s3_service_patch" do
|
|
31
13
|
config.after_initialize do
|
|
32
14
|
require "active_storage/service/s3_service"
|
|
33
|
-
ActiveStorage::Service::S3Service.prepend BetterStorage::S3ServiceProxy
|
|
15
|
+
ActiveStorage::Service::S3Service.prepend BetterStorage::Patches::S3ServiceProxy
|
|
34
16
|
end
|
|
35
17
|
end
|
|
36
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
|
data/lib/better_storage.rb
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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.
|
|
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:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -16,85 +15,20 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 7.
|
|
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.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: appraisal
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - ">="
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '0'
|
|
90
|
-
type: :development
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ">="
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '0'
|
|
97
|
-
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.
|
|
98
32
|
email:
|
|
99
33
|
- yfxie@me.com
|
|
100
34
|
executables: []
|
|
@@ -105,17 +39,19 @@ files:
|
|
|
105
39
|
- README.md
|
|
106
40
|
- Rakefile
|
|
107
41
|
- lib/better_storage.rb
|
|
108
|
-
- lib/better_storage/
|
|
109
|
-
- lib/better_storage/
|
|
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
|
|
110
48
|
- lib/better_storage/railtie.rb
|
|
111
|
-
- lib/better_storage/
|
|
112
|
-
- lib/better_storage/variant.rb
|
|
49
|
+
- lib/better_storage/variant_metadata.rb
|
|
113
50
|
- lib/better_storage/version.rb
|
|
114
51
|
homepage: https://github.com/yfxie/better_storage/
|
|
115
52
|
licenses:
|
|
116
53
|
- MIT
|
|
117
54
|
metadata: {}
|
|
118
|
-
post_install_message:
|
|
119
55
|
rdoc_options: []
|
|
120
56
|
require_paths:
|
|
121
57
|
- lib
|
|
@@ -130,8 +66,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
130
66
|
- !ruby/object:Gem::Version
|
|
131
67
|
version: '0'
|
|
132
68
|
requirements: []
|
|
133
|
-
rubygems_version: 3.
|
|
134
|
-
signing_key:
|
|
69
|
+
rubygems_version: 3.6.9
|
|
135
70
|
specification_version: 4
|
|
136
|
-
summary:
|
|
71
|
+
summary: 'Rails ActiveStorage S3 extension: structured paths, production file protection,
|
|
72
|
+
zero-DB-query variant URLs'
|
|
137
73
|
test_files: []
|
|
@@ -1,33 +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
|
-
transformations = if Rails.version.to_f >= 7.1
|
|
10
|
-
variants = record.attachment_reflections[name]&.named_variants
|
|
11
|
-
variants.fetch(style) do
|
|
12
|
-
record_model_name = record.to_model.model_name.name
|
|
13
|
-
raise ArgumentError, "Cannot find variant :#{style} for #{record_model_name}##{name}"
|
|
14
|
-
end.transformations
|
|
15
|
-
else
|
|
16
|
-
variants = record.attachment_reflections[name]&.variants
|
|
17
|
-
variants.fetch(style) do
|
|
18
|
-
record_model_name = record.to_model.model_name.name
|
|
19
|
-
raise ArgumentError, "Cannot find variant :#{style} for #{record_model_name}##{name}"
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
transformation_key = ActiveStorage::Variation.wrap(transformations).digest
|
|
24
|
-
variant_cache_key = "#{id}-#{transformation_key}"
|
|
25
|
-
|
|
26
|
-
Rails.cache.fetch([:better_storage_public_url, variant_cache_key, BetterStorage::VERSION]) do
|
|
27
|
-
variant(style).processed.key
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
BetterStorage.public_url(key)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
data/lib/better_storage/blob.rb
DELETED
|
@@ -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
|