Change local media attachments to perform heavy processing asynchronously (#13210)
Fix #9106
This commit is contained in:
parent
764b89939f
commit
9660aa4543
@ -3,25 +3,42 @@
|
||||
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
|
||||
|
||||
|
12
app/controllers/api/v2/media_controller.rb
Normal file
12
app/controllers/api/v2/media_controller.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# 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
|
@ -230,12 +230,31 @@ export function uploadCompose(files) {
|
||||
// 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)));
|
||||
};
|
||||
};
|
||||
|
@ -74,7 +74,7 @@ module Attachmentable
|
||||
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
|
||||
|
@ -19,12 +19,14 @@
|
||||
# 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
|
||||
|
||||
@ -156,6 +158,10 @@ class MediaAttachment < ApplicationRecord
|
||||
remote_url.blank?
|
||||
end
|
||||
|
||||
def not_processed?
|
||||
processing.present? && !processing_complete?
|
||||
end
|
||||
|
||||
def needs_redownload?
|
||||
file.blank? && remote_url.present?
|
||||
end
|
||||
@ -203,10 +209,18 @@ class MediaAttachment < ApplicationRecord
|
||||
"#{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
|
||||
|
||||
@ -277,6 +291,10 @@ class MediaAttachment < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def set_processing
|
||||
self.processing = delay_processing? ? :queued : :complete
|
||||
end
|
||||
|
||||
def set_meta
|
||||
meta = populate_meta
|
||||
|
||||
@ -322,9 +340,11 @@ class MediaAttachment < ApplicationRecord
|
||||
}.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
|
||||
|
@ -12,7 +12,9 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
|
||||
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))
|
||||
|
@ -101,6 +101,7 @@ class PostStatusService < BaseService
|
||||
@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)
|
||||
|
@ -9,8 +9,12 @@ class BackupWorker
|
||||
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
|
||||
|
||||
|
34
app/workers/post_process_media_worker.rb
Normal file
34
app/workers/post_process_media_worker.rb
Normal file
@ -0,0 +1,34 @@
|
||||
# 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
|
@ -7,6 +7,7 @@ require 'rails/all'
|
||||
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'
|
||||
|
@ -853,6 +853,7 @@ en:
|
||||
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
|
||||
|
@ -343,7 +343,7 @@ Rails.application.routes.draw do
|
||||
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]
|
||||
@ -455,6 +455,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
namespace :v2 do
|
||||
resources :media, only: [:create]
|
||||
get '/search', to: 'search#index', as: :search
|
||||
end
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
class AddProcessingToMediaAttachments < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :media_attachments, :processing, :integer
|
||||
end
|
||||
end
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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"
|
||||
@ -459,6 +459,7 @@ ActiveRecord::Schema.define(version: 2020_01_26_203551) do
|
||||
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
|
||||
|
30
lib/paperclip/attachment_extensions.rb
Normal file
30
lib/paperclip/attachment_extensions.rb
Normal file
@ -0,0 +1,30 @@
|
||||
# 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)
|
Loading…
Reference in New Issue
Block a user