diff --git a/app/models/status.rb b/app/models/status.rb index 0bff4f2825d..c7dd9badd0c 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -90,6 +90,7 @@ class Status < ApplicationRecord has_many :local_favorited, -> { merge(Account.local) }, through: :favourites, source: :account has_many :local_reblogged, -> { merge(Account.local) }, through: :reblogs, source: :account has_many :local_bookmarked, -> { merge(Account.local) }, through: :bookmarks, source: :account + has_many :local_replied, -> { merge(Account.local) }, through: :replies, source: :account has_and_belongs_to_many :tags # rubocop:disable Rails/HasAndBelongsToMany @@ -136,6 +137,12 @@ class Status < ApplicationRecord scope :tagged_with_none, lambda { |tag_ids| where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids) } + scope :with_local_interaction, lambda { + Status.where(id: Status.joins(:local_favorited).select(:id)) + .or(Status.where(id: Status.joins(:local_bookmarked).select(:id))) + .or(Status.where(id: Status.joins(:local_replied).select(:id))) + .or(Status.where(id: Status.joins(:local_reblogged).select(:id))) + } after_create_commit :trigger_create_webhooks after_update_commit :trigger_update_webhooks diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb index 02c9894c36d..b58bfac14b9 100644 --- a/lib/mastodon/cli/media.rb +++ b/lib/mastodon/cli/media.rb @@ -17,6 +17,7 @@ module Mastodon::CLI option :concurrency, type: :numeric, default: 5, aliases: [:c] option :verbose, type: :boolean, default: false, aliases: [:v] option :dry_run, type: :boolean, default: false + option :keep_interacted, type: :boolean, default: false desc 'remove', 'Remove remote media files, headers or avatars' long_desc <<-DESC Removes locally cached copies of media attachments (and optionally profile @@ -26,6 +27,9 @@ module Mastodon::CLI they are removed. In case of avatars and headers, it specifies how old the last webfinger request and update to the user has to be before they are pruned. It defaults to 7 days. + If --keep-interacted is specified, any media attached to a status that + was favourited, bookmarked, replied to, or reblogged by a local account + will be preserved. If --prune-profiles is specified, only avatars and headers are removed. If --remove-headers is specified, only headers are removed. If --include-follows is specified along with --prune-profiles or @@ -61,7 +65,11 @@ module Mastodon::CLI end unless options[:prune_profiles] || options[:remove_headers] - processed, aggregate = parallelize_with_progress(MediaAttachment.cached.remote.where(created_at: ..time_ago)) do |media_attachment| + attachment_scope = MediaAttachment.cached.remote.where(created_at: ..time_ago) + + attachment_scope = attachment_scope.where.not(status_id: Status.with_local_interaction.select(:id)) if options[:keep_interacted] + + processed, aggregate = parallelize_with_progress(attachment_scope) do |media_attachment| next if media_attachment.file.blank? size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0) diff --git a/spec/lib/mastodon/cli/media_spec.rb b/spec/lib/mastodon/cli/media_spec.rb index fa7a3161d09..712feee653f 100644 --- a/spec/lib/mastodon/cli/media_spec.rb +++ b/spec/lib/mastodon/cli/media_spec.rb @@ -73,6 +73,46 @@ RSpec.describe Mastodon::CLI::Media do expect(media_attachment.reload.thumbnail).to be_blank end end + + context 'with --keep-interacted' do + let(:options) { { keep_interacted: true } } + + let!(:local_account) { Fabricate(:account, username: 'alice') } + let!(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } + + let!(:favourited_status) { Fabricate(:status, account: remote_account) } + let!(:favourited_media) { Fabricate(:media_attachment, created_at: 1.month.ago, remote_url: 'https://example.com/image.jpg', status: favourited_status) } + + let!(:bookmarked_status) { Fabricate(:status, account: remote_account) } + let!(:bookmarked_media) { Fabricate(:media_attachment, created_at: 1.month.ago, remote_url: 'https://example.com/image.jpg', status: bookmarked_status) } + + let!(:replied_to_status) { Fabricate(:status, account: remote_account) } + let!(:replied_to_media) { Fabricate(:media_attachment, created_at: 1.month.ago, remote_url: 'https://example.com/image.jpg', status: replied_to_status) } + + let!(:reblogged_status) { Fabricate(:status, account: remote_account) } + let!(:reblogged_media) { Fabricate(:media_attachment, created_at: 1.month.ago, remote_url: 'https://example.com/image.jpg', status: reblogged_status) } + + let!(:non_interacted_status) { Fabricate(:status, account: remote_account) } + let!(:non_interacted_media) { Fabricate(:media_attachment, created_at: 1.month.ago, remote_url: 'https://example.com/image.jpg', status: non_interacted_status) } + + before do + Fabricate(:favourite, account: local_account, status: favourited_status) + Fabricate(:bookmark, account: local_account, status: bookmarked_status) + Fabricate(:status, account: local_account, in_reply_to_id: replied_to_status.id) + Fabricate(:status, account: local_account, reblog: reblogged_status) + end + + it 'keeps media associated with statuses that have been favourited, bookmarked, replied to, or reblogged by a local account' do + expect { subject } + .to output_results('Removed 1') + + expect(favourited_media.reload.file).to be_present + expect(bookmarked_media.reload.file).to be_present + expect(replied_to_media.reload.file).to be_present + expect(reblogged_media.reload.file).to be_present + expect(non_interacted_media.reload.file).to be_blank + end + end end end