active_storage_quota 0.1.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 +7 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +404 -0
- data/app/controllers/active_storage_quota/direct_uploads_controller.rb +96 -0
- data/db/migrate/20260917000001_create_active_storage_quota_tables.rb +72 -0
- data/db/migrate/20260918000001_create_active_storage_quota_charges.rb +45 -0
- data/lib/active_storage_quota/account.rb +200 -0
- data/lib/active_storage_quota/attachable.rb +44 -0
- data/lib/active_storage_quota/attachment_accounting.rb +144 -0
- data/lib/active_storage_quota/audit/counter_audit.rb +62 -0
- data/lib/active_storage_quota/audit/finding.rb +17 -0
- data/lib/active_storage_quota/audit/ledger_audit.rb +164 -0
- data/lib/active_storage_quota/audit/result.rb +106 -0
- data/lib/active_storage_quota/audit.rb +75 -0
- data/lib/active_storage_quota/backfill/cursor.rb +64 -0
- data/lib/active_storage_quota/backfill/result.rb +146 -0
- data/lib/active_storage_quota/backfill.rb +310 -0
- data/lib/active_storage_quota/blob_accounting.rb +44 -0
- data/lib/active_storage_quota/charge.rb +379 -0
- data/lib/active_storage_quota/configuration.rb +37 -0
- data/lib/active_storage_quota/definition.rb +100 -0
- data/lib/active_storage_quota/engine.rb +28 -0
- data/lib/active_storage_quota/errors.rb +115 -0
- data/lib/active_storage_quota/owner.rb +154 -0
- data/lib/active_storage_quota/owner_resolver.rb +402 -0
- data/lib/active_storage_quota/owner_source.rb +88 -0
- data/lib/active_storage_quota/reconciliation/budget.rb +70 -0
- data/lib/active_storage_quota/reconciliation/counter_reconciler.rb +78 -0
- data/lib/active_storage_quota/reconciliation/ledger_reconciler.rb +250 -0
- data/lib/active_storage_quota/reconciliation/result.rb +107 -0
- data/lib/active_storage_quota/reconciliation.rb +72 -0
- data/lib/active_storage_quota/record.rb +20 -0
- data/lib/active_storage_quota/reservation.rb +297 -0
- data/lib/active_storage_quota/version.rb +5 -0
- data/lib/active_storage_quota.rb +207 -0
- data/lib/tasks/active_storage_quota.rake +11 -0
- data/lib/tasks/active_storage_quota_audit.rake +15 -0
- data/lib/tasks/active_storage_quota_backfill.rake +104 -0
- data/lib/tasks/active_storage_quota_reconcile.rake +100 -0
- metadata +150 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :active_storage_quota do
|
|
4
|
+
# The safe name does the safe thing. Applying repairs has its own task rather
|
|
5
|
+
# than a flag on this one, so a mistyped environment variable can never turn a
|
|
6
|
+
# plan into a write.
|
|
7
|
+
desc "Show what reconciliation would repair (dry run, never writes)"
|
|
8
|
+
task reconcile: :environment do
|
|
9
|
+
ActiveStorageQuota::RakeSupport.run(dry_run: true)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
namespace :reconcile do
|
|
13
|
+
desc "Apply reconciliation repairs (writes)"
|
|
14
|
+
task apply: :environment do
|
|
15
|
+
ActiveStorageQuota::RakeSupport.run(dry_run: false)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ActiveStorageQuota
|
|
21
|
+
module RakeSupport # :nodoc:
|
|
22
|
+
class << self
|
|
23
|
+
def run(dry_run:)
|
|
24
|
+
limits = destructive_limits
|
|
25
|
+
owners = resolve_owners(dry_run: dry_run)
|
|
26
|
+
|
|
27
|
+
owners.each do |owner|
|
|
28
|
+
result = ActiveStorageQuota.reconcile!(owner: owner, dry_run: dry_run, **limits)
|
|
29
|
+
puts "#{owner.class.name}##{owner.id}: #{result}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "Destructive removal disabled (no MAX_CHARGE_DELETIONS or " \
|
|
33
|
+
"MAX_REMOVED_FRACTION given)." if limits.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
# Refuses to guess. An unscoped apply is the most dangerous operation
|
|
38
|
+
# this gem offers, so it has to be asked for by name.
|
|
39
|
+
def resolve_owners(dry_run:)
|
|
40
|
+
owner_type = ENV["OWNER_TYPE"].presence
|
|
41
|
+
owner_id = ENV["OWNER_ID"].presence
|
|
42
|
+
|
|
43
|
+
if owner_type && owner_id
|
|
44
|
+
klass = owner_type.safe_constantize
|
|
45
|
+
abort "OWNER_TYPE #{owner_type.inspect} could not be resolved" if klass.nil?
|
|
46
|
+
|
|
47
|
+
return [ klass.find(owner_id) ]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if owner_type || owner_id
|
|
51
|
+
abort "OWNER_TYPE and OWNER_ID must be given together"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
unless ENV["ALL"] == "1"
|
|
55
|
+
abort "Refusing to run across every owner. Pass OWNER_TYPE and OWNER_ID, or ALL=1."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
all_owners
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def all_owners
|
|
62
|
+
ActiveStorageQuota::Account
|
|
63
|
+
.where(scope_name: ActiveStorageQuota::DEFAULT_SCOPE)
|
|
64
|
+
.pluck(:owner_type, :owner_id)
|
|
65
|
+
.filter_map do |owner_type, owner_id|
|
|
66
|
+
klass = owner_type.safe_constantize
|
|
67
|
+
next warn("skipping unresolvable owner type #{owner_type}") if klass.nil?
|
|
68
|
+
|
|
69
|
+
klass.find_by(klass.primary_key => owner_id)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A malformed value must never become zero or unlimited.
|
|
74
|
+
def destructive_limits
|
|
75
|
+
limits = {}
|
|
76
|
+
limits[:max_charge_deletions] = integer_env("MAX_CHARGE_DELETIONS")
|
|
77
|
+
limits[:max_removed_fraction] = float_env("MAX_REMOVED_FRACTION")
|
|
78
|
+
limits.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def integer_env(name)
|
|
82
|
+
raw = ENV[name].presence
|
|
83
|
+
return nil if raw.nil?
|
|
84
|
+
|
|
85
|
+
Integer(raw)
|
|
86
|
+
rescue ArgumentError, TypeError
|
|
87
|
+
abort "#{name} must be a positive Integer, got #{raw.inspect}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def float_env(name)
|
|
91
|
+
raw = ENV[name].presence
|
|
92
|
+
return nil if raw.nil?
|
|
93
|
+
|
|
94
|
+
Float(raw)
|
|
95
|
+
rescue ArgumentError, TypeError
|
|
96
|
+
abort "#{name} must be a number greater than 0 and at most 1, got #{raw.inspect}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_storage_quota
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mubasher Khan
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.2'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.1'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.2'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: activestorage
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '8.2'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '7.1'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '8.2'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: railties
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '7.1'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '8.2'
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '7.1'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '8.2'
|
|
73
|
+
description: |
|
|
74
|
+
Enforce per-owner storage quotas in Rails applications. Provides an atomic
|
|
75
|
+
reservation system so concurrent uploads across multiple processes cannot
|
|
76
|
+
overshoot a quota, with usage accounted in O(1) rather than by summing blob
|
|
77
|
+
sizes on every request.
|
|
78
|
+
email:
|
|
79
|
+
- mubasherkhan32@outlook.com
|
|
80
|
+
executables: []
|
|
81
|
+
extensions: []
|
|
82
|
+
extra_rdoc_files: []
|
|
83
|
+
files:
|
|
84
|
+
- CHANGELOG.md
|
|
85
|
+
- LICENSE.txt
|
|
86
|
+
- README.md
|
|
87
|
+
- app/controllers/active_storage_quota/direct_uploads_controller.rb
|
|
88
|
+
- db/migrate/20260917000001_create_active_storage_quota_tables.rb
|
|
89
|
+
- db/migrate/20260918000001_create_active_storage_quota_charges.rb
|
|
90
|
+
- lib/active_storage_quota.rb
|
|
91
|
+
- lib/active_storage_quota/account.rb
|
|
92
|
+
- lib/active_storage_quota/attachable.rb
|
|
93
|
+
- lib/active_storage_quota/attachment_accounting.rb
|
|
94
|
+
- lib/active_storage_quota/audit.rb
|
|
95
|
+
- lib/active_storage_quota/audit/counter_audit.rb
|
|
96
|
+
- lib/active_storage_quota/audit/finding.rb
|
|
97
|
+
- lib/active_storage_quota/audit/ledger_audit.rb
|
|
98
|
+
- lib/active_storage_quota/audit/result.rb
|
|
99
|
+
- lib/active_storage_quota/backfill.rb
|
|
100
|
+
- lib/active_storage_quota/backfill/cursor.rb
|
|
101
|
+
- lib/active_storage_quota/backfill/result.rb
|
|
102
|
+
- lib/active_storage_quota/blob_accounting.rb
|
|
103
|
+
- lib/active_storage_quota/charge.rb
|
|
104
|
+
- lib/active_storage_quota/configuration.rb
|
|
105
|
+
- lib/active_storage_quota/definition.rb
|
|
106
|
+
- lib/active_storage_quota/engine.rb
|
|
107
|
+
- lib/active_storage_quota/errors.rb
|
|
108
|
+
- lib/active_storage_quota/owner.rb
|
|
109
|
+
- lib/active_storage_quota/owner_resolver.rb
|
|
110
|
+
- lib/active_storage_quota/owner_source.rb
|
|
111
|
+
- lib/active_storage_quota/reconciliation.rb
|
|
112
|
+
- lib/active_storage_quota/reconciliation/budget.rb
|
|
113
|
+
- lib/active_storage_quota/reconciliation/counter_reconciler.rb
|
|
114
|
+
- lib/active_storage_quota/reconciliation/ledger_reconciler.rb
|
|
115
|
+
- lib/active_storage_quota/reconciliation/result.rb
|
|
116
|
+
- lib/active_storage_quota/record.rb
|
|
117
|
+
- lib/active_storage_quota/reservation.rb
|
|
118
|
+
- lib/active_storage_quota/version.rb
|
|
119
|
+
- lib/tasks/active_storage_quota.rake
|
|
120
|
+
- lib/tasks/active_storage_quota_audit.rake
|
|
121
|
+
- lib/tasks/active_storage_quota_backfill.rake
|
|
122
|
+
- lib/tasks/active_storage_quota_reconcile.rake
|
|
123
|
+
homepage: https://github.com/mubasher-khan/active_storage_quota
|
|
124
|
+
licenses:
|
|
125
|
+
- MIT
|
|
126
|
+
metadata:
|
|
127
|
+
source_code_uri: https://github.com/mubasher-khan/active_storage_quota
|
|
128
|
+
changelog_uri: https://github.com/mubasher-khan/active_storage_quota/blob/main/CHANGELOG.md
|
|
129
|
+
bug_tracker_uri: https://github.com/mubasher-khan/active_storage_quota/issues
|
|
130
|
+
rubygems_mfa_required: 'true'
|
|
131
|
+
post_install_message:
|
|
132
|
+
rdoc_options: []
|
|
133
|
+
require_paths:
|
|
134
|
+
- lib
|
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '3.1'
|
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
requirements: []
|
|
146
|
+
rubygems_version: 3.5.11
|
|
147
|
+
signing_key:
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Race-safe storage quotas for Rails Active Storage.
|
|
150
|
+
test_files: []
|