123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- # frozen_string_literal: true
- require 'zip'
- class BackupService < BaseService
- include Payloadable
- include ContextHelper
- attr_reader :account, :backup
- def call(backup)
- @backup = backup
- @account = backup.user.account
- build_archive!
- end
- private
- def build_outbox_json!(file)
- skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer)
- skeleton[:@context] = full_context
- skeleton[:orderedItems] = ['!PLACEHOLDER!']
- skeleton = Oj.dump(skeleton)
- prepend, append = skeleton.split('"!PLACEHOLDER!"')
- add_comma = false
- file.write(prepend)
- account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
- file.write(',') if add_comma
- add_comma = true
- file.write(statuses.map do |status|
- item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer)
- item.delete('@context')
- unless item[:type] == 'Announce' || item[:object][:attachment].blank?
- item[:object][:attachment].each do |attachment|
- attachment[:url] = Addressable::URI.parse(attachment[:url]).path.delete_prefix('/system/')
- end
- end
- Oj.dump(item)
- end.join(','))
- GC.start
- end
- file.write(append)
- end
- def build_archive!
- tmp_file = Tempfile.new(%w(archive .zip))
- Zip::File.open(tmp_file, create: true) do |zipfile|
- dump_outbox!(zipfile)
- dump_media_attachments!(zipfile)
- dump_likes!(zipfile)
- dump_bookmarks!(zipfile)
- dump_actor!(zipfile)
- end
- archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.zip"
- @backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename)
- @backup.processed = true
- @backup.save!
- ensure
- tmp_file.close
- tmp_file.unlink
- end
- def dump_media_attachments!(zipfile)
- MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments|
- media_attachments.each do |m|
- path = m.file&.path
- next unless path
- path = path.gsub(%r{\A.*/system/}, '')
- path = path.gsub(%r{\A/+}, '')
- download_to_zip(zipfile, m.file, path)
- end
- GC.start
- end
- end
- def dump_outbox!(zipfile)
- zipfile.get_output_stream('outbox.json') do |io|
- build_outbox_json!(io)
- end
- end
- def dump_actor!(zipfile)
- actor = serialize(account, ActivityPub::ActorSerializer)
- actor[:icon][:url] = "avatar#{File.extname(actor[:icon][:url])}" if actor[:icon]
- actor[:image][:url] = "header#{File.extname(actor[:image][:url])}" if actor[:image]
- actor[:outbox] = 'outbox.json'
- actor[:likes] = 'likes.json'
- actor[:bookmarks] = 'bookmarks.json'
- download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
- download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
- json = Oj.dump(actor)
- zipfile.get_output_stream('actor.json') do |io|
- io.write(json)
- end
- end
- def dump_likes!(zipfile)
- skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
- skeleton.delete(:totalItems)
- skeleton[:orderedItems] = ['!PLACEHOLDER!']
- skeleton = Oj.dump(skeleton)
- prepend, append = skeleton.split('"!PLACEHOLDER!"')
- zipfile.get_output_stream('likes.json') do |io|
- io.write(prepend)
- add_comma = false
- Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses|
- io.write(',') if add_comma
- add_comma = true
- io.write(statuses.map do |status|
- Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
- end.join(','))
- GC.start
- end
- io.write(append)
- end
- end
- def dump_bookmarks!(zipfile)
- skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
- skeleton.delete(:totalItems)
- skeleton[:orderedItems] = ['!PLACEHOLDER!']
- skeleton = Oj.dump(skeleton)
- prepend, append = skeleton.split('"!PLACEHOLDER!"')
- zipfile.get_output_stream('bookmarks.json') do |io|
- io.write(prepend)
- add_comma = false
- Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses|
- io.write(',') if add_comma
- add_comma = true
- io.write(statuses.map do |status|
- Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
- end.join(','))
- GC.start
- end
- io.write(append)
- end
- end
- def collection_presenter
- ActivityPub::CollectionPresenter.new(
- id: 'outbox.json',
- type: :ordered,
- size: account.statuses_count,
- items: []
- )
- end
- def serialize(object, serializer)
- ActiveModelSerializers::SerializableResource.new(
- object,
- serializer: serializer,
- adapter: ActivityPub::Adapter
- ).as_json
- end
- CHUNK_SIZE = 1.megabyte
- def download_to_zip(zipfile, attachment, filename)
- adapter = Paperclip.io_adapters.for(attachment)
- zipfile.get_output_stream(filename) do |io|
- while (buffer = adapter.read(CHUNK_SIZE))
- io.write(buffer)
- end
- end
- rescue Errno::ENOENT, Seahorse::Client::NetworkingError => e
- Rails.logger.warn "Could not backup file #{filename}: #{e}"
- end
- end
|