class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user!
+ before_action :set_media_attachment, except: [:create]
+ before_action :check_processing, except: [:create]
def create
- @media = current_account.media_attachments.create!(media_params)
- render json: @media, serializer: REST::MediaAttachmentSerializer
+ @media_attachment = current_account.media_attachments.create!(media_attachment_params)
+ render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
rescue Paperclip::Error
render json: processing_error, status: 500
end
+ def show
+ render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
+ end
+
def update
- @media = current_account.media_attachments.where(status_id: nil).find(params[:id])
- @media.update!(media_params)
- render json: @media, serializer: REST::MediaAttachmentSerializer
+ @media_attachment.update!(media_attachment_params)
+ render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
private
- def media_params
+ def status_code_for_media_attachment
+ @media_attachment.not_processed? ? 206 : 200
+ end
+
+ def set_media_attachment
+ @media_attachment = current_account.media_attachments.unattached.find(params[:id])
+ end
+
+ def check_processing
+ render json: processing_error, status: 422 if @media_attachment.processing_failed?
+ end
+
+ def media_attachment_params
params.permit(:file, :description, :focus)
end
--- /dev/null
+# frozen_string_literal: true
+
+class Api::V2::MediaController < Api::V1::MediaController
+ def create
+ @media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params))
+ render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: 202
+ rescue Paperclip::Errors::NotIdentifiedByImageMagickError
+ render json: file_type_error, status: 422
+ rescue Paperclip::Error
+ render json: processing_error, status: 500
+ end
+end
// Account for disparity in size of original image and resized data
total += file.size - f.size;
- return api(getState).post('/api/v1/media', data, {
+ return api(getState).post('/api/v2/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
- }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
+ }).then(({ status, data }) => {
+ // If server-side processing of the media attachment has not completed yet,
+ // poll the server until it is, before showing the media attachment as uploaded
+
+ if (status === 200) {
+ dispatch(uploadComposeSuccess(data, f));
+ } else if (status === 202) {
+ const poll = () => {
+ api(getState).get(`/api/v1/media/${data.id}`).then(response => {
+ if (response.status === 200) {
+ dispatch(uploadComposeSuccess(data, f));
+ } else if (response.status === 206) {
+ setTimeout(() => poll(), 1000);
+ }
+ }).catch(error => dispatch(uploadComposeFail(error)));
+ };
+
+ poll();
+ }
+ });
}).catch(error => dispatch(uploadComposeFail(error)));
};
};
self.class.attachment_definitions.each_key do |attachment_name|
attachment = send(attachment_name)
- next if attachment.blank? || attachment.queued_for_write[:original].blank?
+ next if attachment.blank? || attachment.queued_for_write[:original].blank? || attachment.options[:preserve_files]
attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
end
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
+# processing :integer
#
class MediaAttachment < ApplicationRecord
self.inheritance_column = nil
enum type: [:image, :gifv, :video, :unknown, :audio]
+ enum processing: [:queued, :in_progress, :complete, :failed], _prefix: true
MAX_DESCRIPTION_LENGTH = 1_500
remote_url.blank?
end
+ def not_processed?
+ processing.present? && !processing_complete?
+ end
+
def needs_redownload?
file.blank? && remote_url.present?
end
"#{x},#{y}"
end
+ attr_writer :delay_processing
+
+ def delay_processing?
+ @delay_processing
+ end
+
+ after_commit :enqueue_processing, on: :create
after_commit :reset_parent_cache, on: :update
before_create :prepare_description, unless: :local?
before_create :set_shortcode
+ before_create :set_processing
before_post_process :set_type_and_extension
end
end
+ def set_processing
+ self.processing = delay_processing? ? :queued : :complete
+ end
+
def set_meta
meta = populate_meta
}.compact
end
- def reset_parent_cache
- return if status_id.nil?
+ def enqueue_processing
+ PostProcessMediaWorker.perform_async(id) if delay_processing?
+ end
- Rails.cache.delete("statuses/#{status_id}")
+ def reset_parent_cache
+ Rails.cache.delete("statuses/#{status_id}") if status_id.present?
end
end
end
def url
- if object.needs_redownload?
+ if object.not_processed?
+ nil
+ elsif object.needs_redownload?
media_proxy_url(object.id, :original)
else
full_asset_url(object.file.url(:original))
@media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?)
+ raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if @media.any?(&:not_processed?)
end
def language_from_option(str)
backup_id = msg['args'].first
ActiveRecord::Base.connection_pool.with_connection do
- backup = Backup.find(backup_id)
- backup&.destroy
+ begin
+ backup = Backup.find(backup_id)
+ backup.destroy
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
end
end
--- /dev/null
+# frozen_string_literal: true
+
+class PostProcessMediaWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: 1, dead: false
+
+ sidekiq_retries_exhausted do |msg|
+ media_attachment_id = msg['args'].first
+
+ ActiveRecord::Base.connection_pool.with_connection do
+ begin
+ media_attachment = MediaAttachment.find(media_attachment_id)
+ media_attachment.processing = :failed
+ media_attachment.save
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+ end
+
+ Sidekiq.logger.error("Processing media attachment #{media_attachment_id} failed with #{msg['error_message']}")
+ end
+
+ def perform(media_attachment_id)
+ media_attachment = MediaAttachment.find(media_attachment_id)
+ media_attachment.processing = :in_progress
+ media_attachment.save
+ media_attachment.file.reprocess_original!
+ media_attachment.processing = :complete
+ media_attachment.save
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
Bundler.require(*Rails.groups)
require_relative '../app/lib/exceptions'
+require_relative '../lib/paperclip/attachment_extensions'
require_relative '../lib/paperclip/lazy_thumbnail'
require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder'
media_attachments:
validations:
images_and_video: Cannot attach a video to a status that already contains images
+ not_ready: Cannot attach files that have not finished processing. Try again in a moment!
too_many: Cannot attach more than 4 files
migrations:
acct: Moved to
end
end
- resources :media, only: [:create, :update]
+ resources :media, only: [:create, :update, :show]
resources :blocks, only: [:index]
resources :mutes, only: [:index]
resources :favourites, only: [:index]
end
namespace :v2 do
+ resources :media, only: [:create]
get '/search', to: 'search#index', as: :search
end
--- /dev/null
+class AddProcessingToMediaAttachments < ActiveRecord::Migration[5.2]
+ def change
+ add_column :media_attachments, :processing, :integer
+ end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_01_26_203551) do
+ActiveRecord::Schema.define(version: 2020_03_06_035625) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.text "description"
t.bigint "scheduled_status_id"
t.string "blurhash"
+ t.integer "processing"
t.index ["account_id"], name: "index_media_attachments_on_account_id"
t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
--- /dev/null
+# frozen_string_literal: true
+
+module Paperclip
+ module AttachmentExtensions
+ # We overwrite this method to support delayed processing in
+ # Sidekiq. Since we process the original file to reduce disk
+ # usage, and we still want to generate thumbnails straight
+ # away, it's the only style we need to exclude
+ def process_style?(style_name, style_args)
+ if style_name == :original && instance.respond_to?(:delay_processing?) && instance.delay_processing?
+ false
+ else
+ style_args.empty? || style_args.include?(style_name)
+ end
+ end
+
+ def reprocess_original!
+ old_original_path = path(:original)
+ reprocess!(:original)
+ new_original_path = path(:original)
+
+ if new_original_path != old_original_path
+ @queued_for_delete << old_original_path
+ flush_deletes
+ end
+ end
+ end
+end
+
+Paperclip::Attachment.prepend(Paperclip::AttachmentExtensions)