|
|
|
@ -45,6 +45,7 @@ module Mastodon |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
option :start_after |
|
|
|
|
option :prefix |
|
|
|
|
option :dry_run, type: :boolean, default: false |
|
|
|
|
desc 'remove-orphans', 'Scan storage and check for files that do not belong to existing media attachments' |
|
|
|
|
long_desc <<~LONG_DESC |
|
|
|
@ -58,6 +59,7 @@ module Mastodon |
|
|
|
|
reclaimed_bytes = 0 |
|
|
|
|
removed = 0 |
|
|
|
|
dry_run = options[:dry_run] ? ' (DRY RUN)' : '' |
|
|
|
|
prefix = options[:prefix] |
|
|
|
|
|
|
|
|
|
case Paperclip::Attachment.default_options[:storage] |
|
|
|
|
when :s3 |
|
|
|
@ -69,7 +71,7 @@ module Mastodon |
|
|
|
|
loop do |
|
|
|
|
objects = begin |
|
|
|
|
begin |
|
|
|
|
bucket.objects(start_after: last_key, prefix: 'media_attachments/files/').limit(1000).map { |x| x } |
|
|
|
|
bucket.objects(start_after: last_key, prefix: prefix).limit(1000).map { |x| x } |
|
|
|
|
rescue => e |
|
|
|
|
progress.log(pastel.red("Error fetching list of files: #{e}")) |
|
|
|
|
progress.log("If you want to continue from this point, add --start-after=#{last_key} to your command") if last_key |
|
|
|
@ -80,15 +82,20 @@ module Mastodon |
|
|
|
|
break if objects.empty? |
|
|
|
|
|
|
|
|
|
last_key = objects.last.key |
|
|
|
|
attachments_map = MediaAttachment.where(id: objects.map { |object| object.key.split('/')[2..-2].join.to_i }).each_with_object({}) { |attachment, map| map[attachment.id] = attachment } |
|
|
|
|
record_map = preload_records_from_mixed_objects(objects) |
|
|
|
|
|
|
|
|
|
objects.each do |object| |
|
|
|
|
attachment_id = object.key.split('/')[2..-2].join.to_i |
|
|
|
|
filename = object.key.split('/').last |
|
|
|
|
path_segments = object.key.split('/') |
|
|
|
|
model_name = path_segments.first.classify |
|
|
|
|
attachment_name = path_segments[1].singularize |
|
|
|
|
record_id = path_segments[2..-2].join.to_i |
|
|
|
|
file_name = path_segments.last |
|
|
|
|
record = record_map.dig(model_name, record_id) |
|
|
|
|
attachment = record&.public_send(attachment_name) |
|
|
|
|
|
|
|
|
|
progress.increment |
|
|
|
|
|
|
|
|
|
next unless attachments_map[attachment_id].nil? || !attachments_map[attachment_id].variant?(filename) |
|
|
|
|
next unless attachment.blank? || !attachment.variant?(file_name) |
|
|
|
|
|
|
|
|
|
begin |
|
|
|
|
object.delete unless options[:dry_run] |
|
|
|
@ -110,17 +117,24 @@ module Mastodon |
|
|
|
|
|
|
|
|
|
root_path = ENV.fetch('RAILS_ROOT_PATH', File.join(':rails_root', 'public', 'system')).gsub(':rails_root', Rails.root.to_s) |
|
|
|
|
|
|
|
|
|
Find.find(File.join(root_path, 'media_attachments', 'files')) do |path| |
|
|
|
|
Find.find(File.join(*[root_path, prefix].compact)) do |path| |
|
|
|
|
next if File.directory?(path) |
|
|
|
|
|
|
|
|
|
key = path.gsub("#{root_path}#{File::SEPARATOR}", '') |
|
|
|
|
attachment_id = key.split(File::SEPARATOR)[2..-2].join.to_i |
|
|
|
|
filename = key.split(File::SEPARATOR).last |
|
|
|
|
attachment = MediaAttachment.find_by(id: attachment_id) |
|
|
|
|
path_segments = key.split(File::SEPARATOR) |
|
|
|
|
model_name = path_segments.first.classify |
|
|
|
|
record_id = path_segments[2..-2].join.to_i |
|
|
|
|
attachment_name = path_segments[1].singularize |
|
|
|
|
file_name = path_segments.last |
|
|
|
|
|
|
|
|
|
next unless PRELOAD_MODEL_WHITELIST.include?(model_name) |
|
|
|
|
|
|
|
|
|
record = model_name.constantize.find_by(id: record_id) |
|
|
|
|
attachment = record&.public_send(attachment_name) |
|
|
|
|
|
|
|
|
|
progress.increment |
|
|
|
|
|
|
|
|
|
next unless attachment.nil? || !attachment.variant?(filename) |
|
|
|
|
next unless attachment.blank? || !attachment.variant?(file_name) |
|
|
|
|
|
|
|
|
|
begin |
|
|
|
|
size = File.size(path) |
|
|
|
@ -213,25 +227,66 @@ module Mastodon |
|
|
|
|
say("Settings:\t#{number_to_human_size(SiteUpload.sum(:file_file_size))}") |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
desc 'lookup', 'Lookup where media is displayed by passing a media URL' |
|
|
|
|
def lookup |
|
|
|
|
prompt = TTY::Prompt.new |
|
|
|
|
desc 'lookup URL', 'Lookup where media is displayed by passing a media URL' |
|
|
|
|
def lookup(url) |
|
|
|
|
path = Addressable::URI.parse(url).path |
|
|
|
|
path_segments = path.split('/')[2..-1] |
|
|
|
|
model_name = path_segments.first.classify |
|
|
|
|
record_id = path_segments[2..-2].join.to_i |
|
|
|
|
|
|
|
|
|
url = prompt.ask('Please enter a URL to the media to lookup:', required: true) |
|
|
|
|
unless PRELOAD_MODEL_WHITELIST.include?(model_name) |
|
|
|
|
say("Cannot find corresponding model: #{model_name}", :red) |
|
|
|
|
exit(1) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
attachment_id = url |
|
|
|
|
.split('/')[0..-2] |
|
|
|
|
.grep(/\A\d+\z/) |
|
|
|
|
.join('') |
|
|
|
|
record = model_name.constantize.find_by(id: record_id) |
|
|
|
|
record = record.status if record.respond_to?(:status) |
|
|
|
|
|
|
|
|
|
if url.split('/')[0..-2].include? 'media_attachments' |
|
|
|
|
model = MediaAttachment.find(attachment_id).status |
|
|
|
|
prompt.say(ActivityPub::TagManager.instance.url_for(model)) |
|
|
|
|
elsif url.split('/')[0..-2].include? 'accounts' |
|
|
|
|
model = Account.find(attachment_id) |
|
|
|
|
prompt.say(ActivityPub::TagManager.instance.url_for(model)) |
|
|
|
|
else |
|
|
|
|
prompt.say('Not found') |
|
|
|
|
unless record |
|
|
|
|
say('Cannot find corresponding record', :red) |
|
|
|
|
exit(1) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
display_url = ActivityPub::TagManager.instance.url_for(record) |
|
|
|
|
|
|
|
|
|
if display_url.blank? |
|
|
|
|
say('No public URL for this type of record', :red) |
|
|
|
|
exit(1) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
say(display_url, :blue) |
|
|
|
|
rescue Addressable::URI::InvalidURIError |
|
|
|
|
say('Invalid URL', :red) |
|
|
|
|
exit(1) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
private |
|
|
|
|
|
|
|
|
|
PRELOAD_MODEL_WHITELIST = %w( |
|
|
|
|
Account |
|
|
|
|
Backup |
|
|
|
|
CustomEmoji |
|
|
|
|
Import |
|
|
|
|
MediaAttachment |
|
|
|
|
PreviewCard |
|
|
|
|
SiteUpload |
|
|
|
|
).freeze |
|
|
|
|
|
|
|
|
|
def preload_records_from_mixed_objects(objects) |
|
|
|
|
preload_map = Hash.new { |hash, key| hash[key] = [] } |
|
|
|
|
|
|
|
|
|
objects.map do |object| |
|
|
|
|
segments = object.key.split('/').first |
|
|
|
|
model_name = segments.first.classify |
|
|
|
|
record_id = segments[2..-2].join.to_i |
|
|
|
|
|
|
|
|
|
next unless PRELOAD_MODEL_WHITELIST.include?(model_name) |
|
|
|
|
|
|
|
|
|
preload_map[model_name] << record_id |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
preload_map.each_with_object({}) do |(model_name, record_ids), model_map| |
|
|
|
|
model_map[model_name] = model_name.constantize.where(id: record_ids).each_with_object({}) { |record, record_map| record_map[record.id] = record } |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|