The code powering m.abunchtell.com https://m.abunchtell.com
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

217 rader
8.5 KiB

  1. # frozen_string_literal: true
  2. require_relative '../../config/boot'
  3. require_relative '../../config/environment'
  4. require_relative 'cli_helper'
  5. module Mastodon
  6. class MediaCLI < Thor
  7. include ActionView::Helpers::NumberHelper
  8. include CLIHelper
  9. def self.exit_on_failure?
  10. true
  11. end
  12. option :days, type: :numeric, default: 7, aliases: [:d]
  13. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  14. option :verbose, type: :boolean, default: false, aliases: [:v]
  15. option :dry_run, type: :boolean, default: false
  16. desc 'remove', 'Remove remote media files'
  17. long_desc <<-DESC
  18. Removes locally cached copies of media attachments from other servers.
  19. The --days option specifies how old media attachments have to be before
  20. they are removed. It defaults to 7 days.
  21. DESC
  22. def remove
  23. time_ago = options[:days].days.ago
  24. dry_run = options[:dry_run] ? '(DRY RUN)' : ''
  25. processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment|
  26. next if media_attachment.file.blank?
  27. size = media_attachment.file_file_size
  28. unless options[:dry_run]
  29. media_attachment.file.destroy
  30. media_attachment.save
  31. end
  32. size
  33. end
  34. say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)}) #{dry_run}", :green, true)
  35. end
  36. option :start_after
  37. option :dry_run, type: :boolean, default: false
  38. desc 'remove-orphans', 'Scan storage and check for files that do not belong to existing media attachments'
  39. long_desc <<~LONG_DESC
  40. Scans file storage for files that do not belong to existing media attachments. Because this operation
  41. requires iterating over every single file individually, it will be slow.
  42. Please mind that some storage providers charge for the necessary API requests to list objects.
  43. LONG_DESC
  44. def remove_orphans
  45. progress = create_progress_bar(nil)
  46. reclaimed_bytes = 0
  47. removed = 0
  48. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  49. case Paperclip::Attachment.default_options[:storage]
  50. when :s3
  51. paperclip_instance = MediaAttachment.new.file
  52. s3_interface = paperclip_instance.s3_interface
  53. bucket = s3_interface.bucket(Paperclip::Attachment.default_options[:s3_credentials][:bucket])
  54. last_key = options[:start_after]
  55. loop do
  56. objects = bucket.objects(start_after: last_key, prefix: 'media_attachments/files/').limit(1000).map { |x| x }
  57. break if objects.empty?
  58. last_key = objects.last.key
  59. 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 }
  60. objects.each do |object|
  61. attachment_id = object.key.split('/')[2..-2].join.to_i
  62. filename = object.key.split('/').last
  63. progress.increment
  64. next unless attachments_map[attachment_id].nil? || !attachments_map[attachment_id].variant?(filename)
  65. reclaimed_bytes += object.size
  66. removed += 1
  67. object.delete unless options[:dry_run]
  68. progress.log("Found and removed orphan: #{object.key}")
  69. end
  70. end
  71. when :fog
  72. say('The fog storage driver is not supported for this operation at this time', :red)
  73. exit(1)
  74. when :filesystem
  75. require 'find'
  76. root_path = ENV.fetch('RAILS_ROOT_PATH', File.join(':rails_root', 'public', 'system')).gsub(':rails_root', Rails.root.to_s)
  77. Find.find(File.join(root_path, 'media_attachments', 'files')) do |path|
  78. next if File.directory?(path)
  79. key = path.gsub("#{root_path}#{File::SEPARATOR}", '')
  80. attachment_id = key.split(File::SEPARATOR)[2..-2].join.to_i
  81. filename = key.split(File::SEPARATOR).last
  82. attachment = MediaAttachment.find_by(id: attachment_id)
  83. progress.increment
  84. next unless attachment.nil? || !attachment.variant?(filename)
  85. reclaimed_bytes += File.size(path)
  86. removed += 1
  87. File.delete(path) unless options[:dry_run]
  88. progress.log("Found and removed orphan: #{key}")
  89. end
  90. end
  91. progress.total = progress.progress
  92. progress.finish
  93. say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true)
  94. end
  95. option :account, type: :string
  96. option :domain, type: :string
  97. option :status, type: :numeric
  98. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  99. option :verbose, type: :boolean, default: false, aliases: [:v]
  100. option :dry_run, type: :boolean, default: false
  101. option :force, type: :boolean, default: false
  102. desc 'refresh', 'Fetch remote media files'
  103. long_desc <<-DESC
  104. Re-downloads media attachments from other servers. You must specify the
  105. source of media attachments with one of the following options:
  106. Use the --status option to download attachments from a specific status,
  107. using the status local numeric ID.
  108. Use the --account option to download attachments from a specific account,
  109. using username@domain handle of the account.
  110. Use the --domain option to download attachments from a specific domain.
  111. By default, attachments that are believed to be already downloaded will
  112. not be re-downloaded. To force re-download of every URL, use --force.
  113. DESC
  114. def refresh
  115. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  116. if options[:status]
  117. scope = MediaAttachment.where(status_id: options[:status])
  118. elsif options[:account]
  119. username, domain = username.split('@')
  120. account = Account.find_remote(username, domain)
  121. if account.nil?
  122. say('No such account', :red)
  123. exit(1)
  124. end
  125. scope = MediaAttachment.where(account_id: account.id)
  126. elsif options[:domain]
  127. scope = MediaAttachment.joins(:account).merge(Account.by_domain_and_subdomains(options[:domain]))
  128. else
  129. exit(1)
  130. end
  131. processed, aggregate = parallelize_with_progress(scope) do |media_attachment|
  132. next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?)
  133. unless options[:dry_run]
  134. media_attachment.reset_file!
  135. media_attachment.save
  136. end
  137. media_attachment.file_file_size
  138. end
  139. say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
  140. end
  141. desc 'usage', 'Calculate disk space consumed by Mastodon'
  142. def usage
  143. say("Attachments:\t#{number_to_human_size(MediaAttachment.sum(:file_file_size))} (#{number_to_human_size(MediaAttachment.where(account: Account.local).sum(:file_file_size))} local)")
  144. say("Custom emoji:\t#{number_to_human_size(CustomEmoji.sum(:image_file_size))} (#{number_to_human_size(CustomEmoji.local.sum(:image_file_size))} local)")
  145. say("Preview cards:\t#{number_to_human_size(PreviewCard.sum(:image_file_size))}")
  146. say("Avatars:\t#{number_to_human_size(Account.sum(:avatar_file_size))} (#{number_to_human_size(Account.local.sum(:avatar_file_size))} local)")
  147. say("Headers:\t#{number_to_human_size(Account.sum(:header_file_size))} (#{number_to_human_size(Account.local.sum(:header_file_size))} local)")
  148. say("Backups:\t#{number_to_human_size(Backup.sum(:dump_file_size))}")
  149. say("Imports:\t#{number_to_human_size(Import.sum(:data_file_size))}")
  150. say("Settings:\t#{number_to_human_size(SiteUpload.sum(:file_file_size))}")
  151. end
  152. desc 'lookup', 'Lookup where media is displayed by passing a media URL'
  153. def lookup
  154. prompt = TTY::Prompt.new
  155. url = prompt.ask('Please enter a URL to the media to lookup:', required: true)
  156. attachment_id = url
  157. .split('/')[0..-2]
  158. .grep(/\A\d+\z/)
  159. .join('')
  160. if url.split('/')[0..-2].include? 'media_attachments'
  161. model = MediaAttachment.find(attachment_id).status
  162. prompt.say(ActivityPub::TagManager.instance.url_for(model))
  163. elsif url.split('/')[0..-2].include? 'accounts'
  164. model = Account.find(attachment_id)
  165. prompt.say(ActivityPub::TagManager.instance.url_for(model))
  166. else
  167. prompt.say('Not found')
  168. end
  169. end
  170. end
  171. end