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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +404 -0
  5. data/app/controllers/active_storage_quota/direct_uploads_controller.rb +96 -0
  6. data/db/migrate/20260917000001_create_active_storage_quota_tables.rb +72 -0
  7. data/db/migrate/20260918000001_create_active_storage_quota_charges.rb +45 -0
  8. data/lib/active_storage_quota/account.rb +200 -0
  9. data/lib/active_storage_quota/attachable.rb +44 -0
  10. data/lib/active_storage_quota/attachment_accounting.rb +144 -0
  11. data/lib/active_storage_quota/audit/counter_audit.rb +62 -0
  12. data/lib/active_storage_quota/audit/finding.rb +17 -0
  13. data/lib/active_storage_quota/audit/ledger_audit.rb +164 -0
  14. data/lib/active_storage_quota/audit/result.rb +106 -0
  15. data/lib/active_storage_quota/audit.rb +75 -0
  16. data/lib/active_storage_quota/backfill/cursor.rb +64 -0
  17. data/lib/active_storage_quota/backfill/result.rb +146 -0
  18. data/lib/active_storage_quota/backfill.rb +310 -0
  19. data/lib/active_storage_quota/blob_accounting.rb +44 -0
  20. data/lib/active_storage_quota/charge.rb +379 -0
  21. data/lib/active_storage_quota/configuration.rb +37 -0
  22. data/lib/active_storage_quota/definition.rb +100 -0
  23. data/lib/active_storage_quota/engine.rb +28 -0
  24. data/lib/active_storage_quota/errors.rb +115 -0
  25. data/lib/active_storage_quota/owner.rb +154 -0
  26. data/lib/active_storage_quota/owner_resolver.rb +402 -0
  27. data/lib/active_storage_quota/owner_source.rb +88 -0
  28. data/lib/active_storage_quota/reconciliation/budget.rb +70 -0
  29. data/lib/active_storage_quota/reconciliation/counter_reconciler.rb +78 -0
  30. data/lib/active_storage_quota/reconciliation/ledger_reconciler.rb +250 -0
  31. data/lib/active_storage_quota/reconciliation/result.rb +107 -0
  32. data/lib/active_storage_quota/reconciliation.rb +72 -0
  33. data/lib/active_storage_quota/record.rb +20 -0
  34. data/lib/active_storage_quota/reservation.rb +297 -0
  35. data/lib/active_storage_quota/version.rb +5 -0
  36. data/lib/active_storage_quota.rb +207 -0
  37. data/lib/tasks/active_storage_quota.rake +11 -0
  38. data/lib/tasks/active_storage_quota_audit.rake +15 -0
  39. data/lib/tasks/active_storage_quota_backfill.rake +104 -0
  40. data/lib/tasks/active_storage_quota_reconcile.rake +100 -0
  41. 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: []