Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/jobs/account/adjust_storage_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Account::AdjustStorageJob < ApplicationJob
queue_as :backend

limits_concurrency to: 1, key: ->(account, delta) { account }

def perform(account, delta)
account.adjust_storage(delta)
end
end
2 changes: 1 addition & 1 deletion app/models/account.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Account < ApplicationRecord
include Entropic, Seedeable
include Entropic, Seedeable, StorageTracking

has_one :join_code
has_many :users, dependent: :destroy
Expand Down
16 changes: 16 additions & 0 deletions app/models/account/storage_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Account::StorageTracking
extend ActiveSupport::Concern

def adjust_storage(delta)
increment!(:bytes_used, delta)
end

def adjust_storage_later(delta)
Account::AdjustStorageJob.perform_later(self, delta) unless delta.zero?
end

def recalculate_bytes_used
boards.find_each(&:recalculate_bytes_used)
update_columns bytes_used: boards.sum(:bytes_used)
end
end
2 changes: 1 addition & 1 deletion app/models/board.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Board < ApplicationRecord
include Accessible, AutoPostponing, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable
include Accessible, AutoPostponing, Broadcastable, Cards, Entropic, Filterable, Publishable, StorageTracking, Triageable

belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :account, default: -> { creator.account }
Expand Down
24 changes: 24 additions & 0 deletions app/models/board/storage_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Board::StorageTracking
extend ActiveSupport::Concern

def bytes_used_changed(delta)
increment!(:bytes_used, delta)
account.adjust_storage_later(delta)
end

def recalculate_bytes_used
update_columns bytes_used: count_bytes_used
end

private
def count_bytes_used
total_bytes = 0

cards.with_rich_text_description_and_embeds.find_each do |card|
total_bytes += card.bytes_used
total_bytes += card.comments.with_rich_text_body_and_embeds.sum(&:bytes_used)
end

total_bytes
end
Comment on lines +14 to +23
Copy link
Member

@packagethief packagethief Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I think we'd just iterate over the cards and ask them to recalculate themselves, taking care of their own dependents.

Each class that includes ::StorageTracking is responsible for providing recalculate_bytes_used and having it return the new value. They can implement their own caching policy as they see fit. Cards and comments, for example, don't cache the value, but having them do so would be easy to add.

# Account::StorageTracking
def recalculate_bytes_used
  boards.find_each(&:recalculate_bytes_used).sum.tap do |value|
    update_column :bytes_used, value
  end
end

# Board::StorageTracking
def recalculate_bytes_used
  cards.find_each(&:recalculate_bytes_used).sum.tap do |value|
    update_column :bytes_used, value
  end
end

# Card::StorageTracking (doesn't cache the result)
def recalculate_bytes_used
  bytes_used = rich_text_associations.sum { # ... }
  bytes_used + comments.find_each(&:recalculate_bytes_used).sum
end

If we need to in the future, we could implement a more efficient calculate_bytes_used. It would use cached values where possible without performing a full recalculation.

end
2 changes: 1 addition & 1 deletion app/models/card.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Card < ApplicationRecord
include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, Eventable,
Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable
Readable, Searchable, Stallable, Statuses, StorageTracking, Taggable, Triageable, Watchable

belongs_to :account, default: -> { board.account }
belongs_to :board
Expand Down
11 changes: 11 additions & 0 deletions app/models/card/storage_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Card::StorageTracking
extend ActiveSupport::Concern

included do
include ::StorageTracking
end

def bytes_used_changed(delta)
board.bytes_used_changed(delta)
end
end
2 changes: 1 addition & 1 deletion app/models/comment.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Comment < ApplicationRecord
include Attachments, Eventable, Mentions, Promptable, Searchable
include Attachments, Eventable, Mentions, Promptable, Searchable, StorageTracking

belongs_to :account, default: -> { card.account }
belongs_to :card, touch: true
Expand Down
11 changes: 11 additions & 0 deletions app/models/comment/storage_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Comment::StorageTracking
extend ActiveSupport::Concern

included do
include ::StorageTracking
end

def bytes_used_changed(delta)
card.bytes_used_changed(delta)
end
end
34 changes: 34 additions & 0 deletions app/models/concerns/storage_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module StorageTracking
extend ActiveSupport::Concern

included do
after_save :track_storage_updated
after_destroy :track_storage_removed
end

def bytes_used
rich_text_associations.sum { |association| send(association.name)&.bytes_used || 0 }
end

private
def rich_text_associations
self.class.reflect_on_all_associations(:has_one).filter { |association| association.klass == ActionText::RichText }
end

def track_storage_updated
bytes_used_changed(calculate_changed_storage_delta)
end

def calculate_changed_storage_delta
rich_text_associations.sum do |association|
rich_text = send(association.name)
next 0 unless rich_text&.body_previously_changed?

rich_text.bytes_used - rich_text.body_previously_was&.bytes_used.to_i
end
end

def track_storage_removed
bytes_used_changed(-bytes_used)
end
end
5 changes: 5 additions & 0 deletions db/migrate/20251205112423_add_bytes_used_to_accounts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddBytesUsedToAccounts < ActiveRecord::Migration[8.2]
def change
add_column :accounts, :bytes_used, :bigint, default: 0
end
end
5 changes: 5 additions & 0 deletions db/migrate/20251205150747_add_bytes_used_to_boards.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddBytesUsedToBoards < ActiveRecord::Migration[8.2]
def change
add_column :boards, :bytes_used, :bigint, default: 0
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion db/schema_sqlite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.2].define(version: 2025_12_01_100607) do
ActiveRecord::Schema[8.2].define(version: 2025_12_05_150747) do
create_table "accesses", id: :uuid, force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
Expand Down Expand Up @@ -52,6 +52,7 @@
end

create_table "accounts", id: :uuid, force: :cascade do |t|
t.bigint "bytes_used", default: 0
t.bigint "cards_count", default: 0, null: false
t.datetime "created_at", null: false
t.bigint "external_account_id"
Expand Down Expand Up @@ -145,6 +146,7 @@
create_table "boards", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.boolean "all_access", default: false, null: false
t.bigint "bytes_used", default: 0
t.datetime "created_at", null: false
t.uuid "creator_id", null: false
t.string "name", limit: 255, null: false
Expand Down
19 changes: 19 additions & 0 deletions lib/rails_ext/action_text_storage_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module ActionTextContentStorageTracking
def bytes_used
attachables.sum { |attachable| attachable.try(:attachable_filesize) || 0 }
end
end

module ActionTextRichTextStorageTracking
def bytes_used
body&.bytes_used || 0
end
end

ActiveSupport.on_load :action_text_content do
include ActionTextContentStorageTracking
end

ActiveSupport.on_load :action_text_rich_text do
include ActionTextRichTextStorageTracking
end
8 changes: 8 additions & 0 deletions script/migrations/20251205-populate_bytes_used_on_accounts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env ruby

require_relative "../../config/environment"

Account.find_each do |account|
account.recalculate_bytes_used
puts "#{account.id}: #{account.bytes_used} bytes"
end
2 changes: 2 additions & 0 deletions test/fixtures/active_storage/blobs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello_txt: <%= ActiveStorage::FixtureSet.blob filename: "hello.txt", service_name: "local", account_id: ActiveRecord::FixtureSet.identify("37s", :uuid) %>
list_pdf: <%= ActiveStorage::FixtureSet.blob filename: "list.pdf", service_name: "local", account_id: ActiveRecord::FixtureSet.identify("37s", :uuid) %>
1 change: 1 addition & 0 deletions test/fixtures/files/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hi!
Binary file added test/fixtures/files/list.pdf
Binary file not shown.
45 changes: 45 additions & 0 deletions test/models/account/storage_tracking_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require "test_helper"

class Account::StorageTrackingTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
@account = Current.account
end

test "track storage deltas" do
@account.adjust_storage(1000)
assert_equal 1000, @account.reload.bytes_used

@account.adjust_storage(-100)
assert_equal 900, @account.reload.bytes_used
end

test "track storage deltas in jobs" do
assert_enqueued_with(job: Account::AdjustStorageJob, args: [ @account, 1000 ]) do
@account.adjust_storage_later(1000)
end

assert_no_enqueued_jobs only: Account::AdjustStorageJob do
@account.adjust_storage_later(0)
end
end

test "recalculate bytes used from cards and comments across boards" do
board1 = boards(:writebook)
board2 = boards(:private)

card1 = board1.cards.create!(title: "Test 1", description: attachment_html(active_storage_blobs(:hello_txt)), status: :published)
card1.comments.create!(body: attachment_html(active_storage_blobs(:hello_txt)))

card2 = board2.cards.create!(title: "Test 2", description: attachment_html(active_storage_blobs(:list_pdf)), status: :published)

@account.recalculate_bytes_used

board1_expected = active_storage_blobs(:hello_txt).byte_size * 2
board2_expected = active_storage_blobs(:list_pdf).byte_size

assert_equal board1_expected, board1.reload.bytes_used
assert_equal board2_expected, board2.reload.bytes_used
assert_equal board1_expected + board2_expected, @account.bytes_used
end
end
20 changes: 20 additions & 0 deletions test/models/board/storage_tracking_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require "test_helper"

class Board::StorageTrackingTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
@board = boards(:writebook)
end

test "recalculate bytes used from cards and comments" do
card = @board.cards.create!(title: "Test", description: attachment_html(active_storage_blobs(:hello_txt)), status: :published)
card.comments.create!(body: attachment_html(active_storage_blobs(:hello_txt)))
card.comments.create!(body: attachment_html(active_storage_blobs(:list_pdf)))

@board.update_columns(bytes_used: 0)
@board.recalculate_bytes_used

expected_bytes = active_storage_blobs(:hello_txt).byte_size * 2 + active_storage_blobs(:list_pdf).byte_size
assert_equal expected_bytes, @board.bytes_used
end
end
Loading