diff --git a/app/assets/javascripts/posts.js b/app/assets/javascripts/posts.js index 9177546aa..a54408d8f 100644 --- a/app/assets/javascripts/posts.js +++ b/app/assets/javascripts/posts.js @@ -23,6 +23,7 @@ this.initialize_post_image_resize_links(); this.initialize_post_image_resize_to_window_link(); this.initialize_similar(); + this.initialize_replace_image_dialog(); if (Danbooru.meta("always-resize-images") === "true") { $("#image-resize-to-window-link").click(); @@ -606,6 +607,32 @@ e.preventDefault(); }); } + + Danbooru.Post.initialize_replace_image_dialog = function() { + $("#replace-image-dialog").dialog({ + autoOpen: false, + width: 700, + modal: true, + buttons: { + "Submit": function() { + $("#replace-image-dialog form").submit(); + $(this).dialog("close"); + }, + "Cancel": function() { + $(this).dialog("close"); + } + } + }); + + $('#replace-image-dialog form').submit(function() { + $('#replace-image-dialog').dialog('close'); + }); + + $("#replace-image").click(function(e) { + e.preventDefault(); + $("#replace-image-dialog").dialog("open"); + }); + }; })(); $(document).ready(function() { diff --git a/app/controllers/moderator/post/posts_controller.rb b/app/controllers/moderator/post/posts_controller.rb index 94ceb1166..10e7e701d 100644 --- a/app/controllers/moderator/post/posts_controller.rb +++ b/app/controllers/moderator/post/posts_controller.rb @@ -1,10 +1,12 @@ module Moderator module Post class PostsController < ApplicationController - before_filter :approver_only, :only => [:delete, :undelete, :move_favorites, :ban, :unban, :confirm_delete, :confirm_move_favorites, :confirm_ban] + before_filter :approver_only, :only => [:delete, :undelete, :move_favorites, :replace, :ban, :unban, :confirm_delete, :confirm_move_favorites, :confirm_ban] before_filter :admin_only, :only => [:expunge] skip_before_filter :api_check + respond_to :html, :json, :xml + def confirm_delete @post = ::Post.find(params[:id]) end @@ -35,6 +37,15 @@ module Moderator redirect_to(post_path(@post)) end + def replace + @post = ::Post.find(params[:id]) + @post.replace!(params[:post][:source]) + + respond_with(@post) do |format| + format.html { redirect_to(@post) } + end + end + def expunge @post = ::Post.find(params[:id]) @post.expunge! diff --git a/app/helpers/delayed_jobs_helper.rb b/app/helpers/delayed_jobs_helper.rb index 8b90b3512..13d75c5f3 100644 --- a/app/helpers/delayed_jobs_helper.rb +++ b/app/helpers/delayed_jobs_helper.rb @@ -61,6 +61,9 @@ module DelayedJobsHelper when "Pool#update_category_pseudo_tags_for_posts" "update pool category pseudo tags for posts" + when "Post.delete_files" + "delete old files" + else h(job.name) end @@ -122,6 +125,9 @@ module DelayedJobsHelper when "Pool#update_category_pseudo_tags_for_posts" %{#{h(job.payload_object.name)}} + when "Post.delete_files" + %{post ##{job.payload_object.args.first}} + else h(job.handler) end diff --git a/app/models/post.rb b/app/models/post.rb index 678c2f251..70c32496e 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -7,6 +7,8 @@ class Post < ActiveRecord::Base class RevertError < Exception ; end class SearchError < Exception ; end + DELETION_GRACE_PERIOD = 30.days + before_validation :initialize_uploader, :on => :create before_validation :merge_old_changes before_validation :normalize_tags @@ -30,7 +32,6 @@ class Post < ActiveRecord::Base after_save :expire_essential_tag_string_cache after_destroy :remove_iqdb_async after_destroy :delete_files - after_destroy :delete_remote_files after_commit :update_iqdb_async, :on => :create after_commit :notify_pubsub @@ -61,24 +62,31 @@ class Post < ActiveRecord::Base attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning, :view_count module FileMethods + extend ActiveSupport::Concern + + module ClassMethods + def delete_files(post_id, file_path, large_file_path, preview_file_path) + # the large file and the preview don't necessarily exist. if so errors will be ignored. + FileUtils.rm_f(file_path) + FileUtils.rm_f(large_file_path) + FileUtils.rm_f(preview_file_path) + + RemoteFileManager.new(file_path).delete + RemoteFileManager.new(large_file_path).delete + RemoteFileManager.new(preview_file_path).delete + end + end + + def delete_files + Post.delete_files(id, file_path, large_file_path, preview_file_path) + end + def distribute_files RemoteFileManager.new(file_path).distribute RemoteFileManager.new(preview_file_path).distribute if has_preview? RemoteFileManager.new(large_file_path).distribute if has_large? end - def delete_remote_files - RemoteFileManager.new(file_path).delete - RemoteFileManager.new(preview_file_path).delete if has_preview? - RemoteFileManager.new(large_file_path).delete if has_large? - end - - def delete_files - FileUtils.rm_f(file_path) - FileUtils.rm_f(large_file_path) - FileUtils.rm_f(preview_file_path) - end - def file_path_prefix Rails.env == "test" ? "test." : "" end @@ -1421,6 +1429,52 @@ class Post < ActiveRecord::Base Post.expire_cache_for_all(tag_array) ModAction.log("undeleted post ##{id}") end + + def replace!(url, replacer = CurrentUser.user) + # TODO for posts with notes we need to rescale the notes if the dimensions change. + if notes.size > 0 + raise NotImplementedError.new("Replacing images with notes not yet supported.") + end + + # TODO for ugoiras we need to replace the frame data. + if is_ugoira? + raise NotImplementedError.new("Replacing ugoira images not yet supported.") + end + + # TODO images hosted on s3 need to be deleted from s3 instead of the local filesystem. + if Danbooru.config.use_s3_proxy?(self) + raise NotImplementedError.new("Replacing S3 hosted images not yet supported.") + end + + transaction do + upload = Upload.create!(source: url, rating: self.rating, tag_string: self.tag_string) + upload.process_upload + upload.update(status: "completed", post_id: id) + + # queue the deletion *before* updating the post so that we use the old + # md5/file_ext to delete the old files. if saving the post fails, + # this is rolled back so the job won't run. + Post.delay(queue: "default", run_at: Time.now + DELETION_GRACE_PERIOD).delete_files(id, file_path, large_file_path, preview_file_path) + + self.md5 = upload.md5 + self.file_ext = upload.file_ext + self.image_width = upload.image_width + self.image_height = upload.image_height + self.file_size = upload.file_size + self.source = upload.source + self.tag_string = upload.tag_string + + comments.create!({creator: User.system, body: presenter.comment_replacement_message(replacer), do_not_bump_post: true}, without_protection: true) + ModAction.log(presenter.modaction_replacement_message) + + save! + end + + # point of no return: these things can't be rolled back, so we do them + # only after the transaction successfully commits. + distribute_files + update_iqdb_async + end end module VersionMethods diff --git a/app/models/upload.rb b/app/models/upload.rb index 9a883e0e4..1d5a37e6b 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -105,7 +105,7 @@ class Upload < ActiveRecord::Base end module ConversionMethods - def process_once + def process_upload CurrentUser.scoped(uploader, uploader_ip_addr) do update_attribute(:status, "processing") self.source = strip_source @@ -129,16 +129,19 @@ class Upload < ActiveRecord::Base move_file validate_md5_confirmation_after_move save - post = convert_to_post - post.distribute_files - if post.save - User.where(id: CurrentUser.id).update_all("post_upload_count = post_upload_count + 1") - create_artist_commentary(post) if include_artist_commentary? - ugoira_service.save_frame_data(post) if is_ugoira? - update_attributes(:status => "completed", :post_id => post.id) - else - update_attribute(:status, "error: " + post.errors.full_messages.join(", ")) - end + end + end + + def create_post_from_upload + post = convert_to_post + post.distribute_files + if post.save + User.where(id: CurrentUser.id).update_all("post_upload_count = post_upload_count + 1") + create_artist_commentary(post) if include_artist_commentary? + ugoira_service.save_frame_data(post) if is_ugoira? + update_attributes(:status => "completed", :post_id => post.id) + else + update_attribute(:status, "error: " + post.errors.full_messages.join(", ")) end end @@ -146,7 +149,8 @@ class Upload < ActiveRecord::Base @tries ||= 0 return if !force && status =~ /processing|completed|error/ - process_once + process_upload + create_post_from_upload rescue Timeout::Error, Net::HTTP::Persistent::Error => x if @tries > 3 diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb index eaee5e70e..69d74bb33 100644 --- a/app/presenters/post_presenter.rb +++ b/app/presenters/post_presenter.rb @@ -279,4 +279,53 @@ class PostPresenter < Presenter pool_html << "" pool_html end + + def comment_replacement_message(replacer = CurrentUser.user) + "@#{replacer.name} replaced this post with a new image:\n\n#{replacement_message}" + end + + def modaction_replacement_message + "replaced post ##{@post.id}:\n\n#{replacement_message}" + end + + def replacement_message + linked_source = linked_source(@post.source) + linked_source_was = linked_source(@post.source_was) + + <<-EOS.strip_heredoc + [table] + [tbody] + [tr] + [th]Old[/th] + [td]#{linked_source_was}[/td] + [td]#{@post.md5_was}[/td] + [td]#{@post.file_ext_was}[/td] + [td]#{@post.image_width_was} x #{@post.image_height_was}[/td] + [td]#{@post.file_size_was.to_s(:human_size, precision: 4)}[/td] + [/tr] + [tr] + [th]New[/th] + [td]#{linked_source}[/td] + [td]#{@post.md5}[/td] + [td]#{@post.file_ext}[/td] + [td]#{@post.image_width} x #{@post.image_height}[/td] + [td]#{@post.file_size.to_s(:human_size, precision: 4)}[/td] + [/tr] + [/tbody] + [/table] + EOS + end + +protected + + def linked_source(source) + # truncate long sources in the middle: "www.pixiv.net...lust_id=23264933" + truncated_source = source.gsub(%r{\Ahttps?://}, "").truncate(64, omission: "...#{source.last(32)}") + + if source =~ %r{\Ahttps?://}i + %("#{truncated_source}":[#{source}]) + else + truncated_source + end + end end diff --git a/app/views/moderator/post/posts/_replace.html.erb b/app/views/moderator/post/posts/_replace.html.erb new file mode 100644 index 000000000..7b43d6e0c --- /dev/null +++ b/app/views/moderator/post/posts/_replace.html.erb @@ -0,0 +1,11 @@ +<%= simple_form_for(@post, url: replace_moderator_post_post_path, method: :post) do |f| %> +
+ Delete the current image and replace it with another one, keeping + everything else in the post intact. This is meant for upgrading + lower-quality images, such as image samples, to higher-quality versions. +
+ + <%= f.input :source, label: "New Source", input_html: { value: "" } %> +<% end %> diff --git a/app/views/moderator/post/posts/replace.html.erb b/app/views/moderator/post/posts/replace.html.erb new file mode 100644 index 000000000..3ef8b5d68 --- /dev/null +++ b/app/views/moderator/post/posts/replace.html.erb @@ -0,0 +1,5 @@ +