<%= link_to "Fetch source data", source_path, class: "source-data-fetch" %>
<%= spinner_icon class: "source-data-loading" %>
diff --git a/app/components/source_data_component/source_data_component.js b/app/components/source_data_component/source_data_component.js
index 64221339f..b7a69a7df 100644
--- a/app/components/source_data_component/source_data_component.js
+++ b/app/components/source_data_component/source_data_component.js
@@ -1,12 +1,11 @@
class SourceDataComponent {
static initialize() {
- $(document).on("change.danbooru", "#upload_source", SourceDataComponent.fetchData);
$(document).on("click.danbooru", ".source-data-fetch", SourceDataComponent.fetchData);
}
static async fetchData(e) {
- let url = $("#upload_source,#post_source").val();
- let ref = $("#upload_referer_url").val();
+ let url = $("#post_source").val();
+ let ref = $("#post_referer_url").val();
e.preventDefault();
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 166203e5c..109f840ad 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -63,6 +63,19 @@ class PostsController < ApplicationController
respond_with_post_after_update(@post)
end
+ def create
+ @post = authorize Post.new_from_upload(permitted_attributes(Post))
+ @post.save
+
+ if @post.errors.any?
+ @upload = UploadMediaAsset.find(params[:post][:upload_media_asset_id]).upload
+ flash[:notice] = @post.errors.full_messages.join("; ")
+ respond_with(@post, render: { template: "uploads/show" })
+ else
+ respond_with(@post)
+ end
+ end
+
def destroy
@post = authorize Post.find(params[:id])
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 7174c176a..67beb3c70 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -2,14 +2,16 @@
class UploadsController < ApplicationController
respond_to :html, :xml, :json, :js
- skip_before_action :verify_authenticity_token, only: [:preprocess]
+ skip_before_action :verify_authenticity_token, only: [:create], if: -> { request.xhr? }
def new
- authorize Upload
- @source = Sources::Strategies.find(params[:url], params[:ref]) if params[:url].present?
- @upload, @remote_size = UploadService::ControllerHelper.prepare(
- url: params[:url], ref: params[:ref]
- )
+ @upload = authorize Upload.new(uploader: CurrentUser.user, uploader_ip_addr: CurrentUser.ip_addr, rating: "q", tag_string: "", source: params[:url], referer_url: params[:ref], **permitted_attributes(Upload))
+ respond_with(@upload)
+ end
+
+ def create
+ @upload = authorize Upload.new(uploader: CurrentUser.user, uploader_ip_addr: CurrentUser.ip_addr, rating: "q", tag_string: "", **permitted_attributes(Upload))
+ @upload.save
respond_with(@upload)
end
@@ -28,38 +30,14 @@ class UploadsController < ApplicationController
def index
@uploads = authorize Upload.visible(CurrentUser.user).paginated_search(params, count_pages: true)
- @uploads = @uploads.includes(:uploader, post: [:media_asset, :uploader]) if request.format.html?
+ @uploads = @uploads.includes(:uploader, :media_assets) if request.format.html?
respond_with(@uploads)
end
def show
@upload = authorize Upload.find(params[:id])
- respond_with(@upload) do |format|
- format.html do
- if @upload.is_completed? && @upload.post_id
- redirect_to(post_path(@upload.post_id))
- end
- end
- end
- end
-
- def preprocess
- authorize Upload
- @upload, @remote_size = UploadService::ControllerHelper.prepare(
- url: params.dig(:upload, :source), file: params.dig(:upload, :file), ref: params.dig(:upload, :referer_url)
- )
- render body: nil
- end
-
- def create
- @service = authorize UploadService.new(permitted_attributes(Upload)), policy_class: UploadPolicy
- @upload = @service.start!
-
- if @service.warnings.any?
- flash[:notice] = @service.warnings.join(".\n \n")
- end
-
+ @post = Post.new(uploader: @upload.uploader, uploader_ip_addr: @upload.uploader_ip_addr, source: @upload.source, rating: nil, **permitted_attributes(Post))
respond_with(@upload)
end
end
diff --git a/app/helpers/uploads_helper.rb b/app/helpers/uploads_helper.rb
deleted file mode 100644
index 7a9b010db..000000000
--- a/app/helpers/uploads_helper.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module UploadsHelper
- def render_status(upload)
- case upload.status
- when /duplicate: (\d+)/
- dup_post_id = $1
- link_to(upload.status.gsub(/error: RuntimeError - /, ""), post_path(dup_post_id))
-
- when /\Aerror: /
- search_params = params[:search].permit!
- link_to(upload.sanitized_status, uploads_path(search: search_params.merge(status: upload.sanitized_status)))
-
- else
- search_params = params[:search].permit!
- link_to(upload.status, uploads_path(search: search_params.merge(status: upload.status)))
- end
- end
-end
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index a0caf04f5..a40a6d6da 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -39,6 +39,7 @@ import CommentVotesTooltipComponent from "../../components/comment_votes_tooltip
import CurrentUser from "../src/javascripts/current_user.js";
import Dtext from "../src/javascripts/dtext.js";
import FavoritesTooltipComponent from "../../components/favorites_tooltip_component/favorites_tooltip_component.js";
+import FileUploadComponent from "../../components/file_upload_component/file_upload_component.js";
import ForumPostComponent from "../../components/forum_post_component/forum_post_component.js";
import IqdbQuery from "../src/javascripts/iqdb_queries.js";
import Note from "../src/javascripts/notes.js";
@@ -64,6 +65,7 @@ Danbooru.CommentVotesTooltipComponent = CommentVotesTooltipComponent;
Danbooru.CurrentUser = CurrentUser;
Danbooru.Dtext = Dtext;
Danbooru.FavoritesTooltipComponent = FavoritesTooltipComponent;
+Danbooru.FileUploadComponent = FileUploadComponent;
Danbooru.ForumPostComponent = ForumPostComponent;
Danbooru.IqdbQuery = IqdbQuery;
Danbooru.Note = Note;
diff --git a/app/javascript/src/javascripts/posts.js b/app/javascript/src/javascripts/posts.js
index b2aaec89a..f1c1ab9a2 100644
--- a/app/javascript/src/javascripts/posts.js
+++ b/app/javascript/src/javascripts/posts.js
@@ -39,7 +39,7 @@ Post.initialize_all = function() {
this.initialize_ruffle_player();
}
- if ($("#c-posts #a-show, #c-uploads #a-new").length) {
+ if ($("#c-posts #a-show, #c-uploads #a-show").length) {
this.initialize_edit_dialog();
}
@@ -102,7 +102,7 @@ Post.open_edit_dialog = function() {
$("#post-sections li").removeClass("active");
$("#post-edit-link").parent("li").addClass("active");
- var $tag_string = $("#post_tag_string,#upload_tag_string");
+ var $tag_string = $("#post_tag_string");
$("#open-edit-dialog").hide();
var dialog = $("
").attr("id", "edit-dialog");
@@ -157,9 +157,9 @@ Post.open_edit_dialog = function() {
}
Post.close_edit_dialog = function(e, ui) {
- $("#form").appendTo($("#c-posts #edit,#c-uploads #a-new"));
+ $("#form").appendTo($("#c-posts #edit,#c-uploads #a-show"));
$("#edit-dialog").remove();
- var $tag_string = $("#post_tag_string,#upload_tag_string");
+ var $tag_string = $("#post_tag_string");
$("div.input").has($tag_string).prevAll().show();
$("#open-edit-dialog").show();
$tag_string.css({"resize": "", "width": ""});
diff --git a/app/javascript/src/javascripts/related_tag.js b/app/javascript/src/javascripts/related_tag.js
index beecac100..650f5ec3a 100644
--- a/app/javascript/src/javascripts/related_tag.js
+++ b/app/javascript/src/javascripts/related_tag.js
@@ -9,7 +9,7 @@ RelatedTag.initialize_all = function() {
$(document).on("click.danbooru", ".related-tags a", RelatedTag.toggle_tag);
$(document).on("click.danbooru", "#show-related-tags-link", RelatedTag.show);
$(document).on("click.danbooru", "#hide-related-tags-link", RelatedTag.hide);
- $(document).on("keyup.danbooru.relatedTags", "#upload_tag_string, #post_tag_string", RelatedTag.update_selected);
+ $(document).on("keyup.danbooru.relatedTags", "#post_tag_string", RelatedTag.update_selected);
$(document).on("danbooru:update-source-data", RelatedTag.on_update_source_data);
$(document).on("danbooru:open-post-edit-dialog", RelatedTag.hide);
@@ -21,7 +21,7 @@ RelatedTag.initialize_all = function() {
// Show the related tags automatically when the "Edit" tab is opened, or by default on the uploads page.
$(document).on("danbooru:open-post-edit-tab", RelatedTag.show);
- if ($("#c-uploads #a-new").length) {
+ if ($("#c-uploads #a-show").length) {
RelatedTag.show();
}
}
@@ -50,7 +50,7 @@ RelatedTag.current_tag = function() {
// 7. |abc def -> abc
// 8. | abc def -> abc
- var $field = $("#upload_tag_string,#post_tag_string");
+ var $field = $("#post_tag_string");
var string = $field.val();
var n = string.length;
var a = $field.prop('selectionStart');
@@ -105,12 +105,12 @@ RelatedTag.update_selected = function(e) {
}
RelatedTag.current_tags = function() {
- let tagString = $("#upload_tag_string,#post_tag_string").val().toLowerCase();
+ let tagString = $("#post_tag_string").val().toLowerCase();
return Utility.splitWords(tagString);
}
RelatedTag.toggle_tag = function(e) {
- var $field = $("#upload_tag_string,#post_tag_string");
+ var $field = $("#post_tag_string");
var tag = $(e.target).closest("li").text().trim().replace(/ /g, "_");
if (RelatedTag.current_tags().includes(tag)) {
diff --git a/app/javascript/src/javascripts/uploads.js b/app/javascript/src/javascripts/uploads.js
index e4f989690..5d34a8e1c 100644
--- a/app/javascript/src/javascripts/uploads.js
+++ b/app/javascript/src/javascripts/uploads.js
@@ -1,6 +1,3 @@
-import Dropzone from 'dropzone';
-import SparkMD5 from 'spark-md5';
-
let Upload = {};
Upload.IQDB_LIMIT = 5;
@@ -8,11 +5,9 @@ Upload.IQDB_MIN_SIMILARITY = 50;
Upload.IQDB_HIGH_SIMILARITY = 70;
Upload.initialize_all = function() {
- if ($("#c-uploads").length) {
+ if ($("#c-uploads #a-show").length) {
this.initialize_image();
this.initialize_similar();
- this.initialize_submit();
- $("#similar-button").click();
$("#toggle-artist-commentary").on("click.danbooru", function(e) {
Upload.toggle_commentary();
@@ -23,104 +18,57 @@ Upload.initialize_all = function() {
Upload.toggle_translation();
e.preventDefault();
});
+ }
+ if ($("#c-uploads #a-batch").length) {
$(document).on("click.danbooru", "#c-uploads #a-batch #link", Upload.batch_open_all);
}
-
- if ($("#c-uploads #a-new").length) {
- this.initialize_dropzone();
- }
-}
-
-Upload.initialize_submit = function() {
- $("#form").on("submit.danbooru", Upload.validate_upload);
-}
-
-Upload.validate_upload = function (e) {
- var error_messages = [];
- if (($("#upload_file").val() === undefined) && !/^https?:\/\/.+/i.test($("#upload_source").val()) && $("#upload_md5_confirmation").val() === "") {
- error_messages.push("Must choose file or specify source");
- } else if ($(".dz-progress:visible").length) {
- error_messages.push("File has not finished uploading yet")
- }
- if (!$("#upload_rating_s").prop("checked") && !$("#upload_rating_q").prop("checked") && !$("#upload_rating_e").prop("checked") &&
- ($("#upload_tag_string").val().search(/\brating:[sqe]/i) < 0)) {
- error_messages.push("Must specify a rating");
- }
- if (error_messages.length === 0) {
- $("#submit-button").prop("disabled", "true");
- $("#submit-button").prop("value", "Submitting...");
- $("#client-errors").hide();
- } else {
- $("#client-errors").html("
Error: " + error_messages.join(", "));
- $("#client-errors").show();
- e.preventDefault();
- }
}
Upload.initialize_similar = function() {
- $("#similar-button").on("click.danbooru", function(e) {
- e.preventDefault();
+ let source = $("#post_source").val();
- let source = $("#upload_source").val();
- if (/^https?:\/\//.test(source)) {
- $.get("/iqdb_queries.js", {
- limit: Upload.IQDB_LIMIT,
- search: {
- url: source,
- similarity: Upload.IQDB_MIN_SIMILARITY,
- high_similarity: Upload.IQDB_HIGH_SIMILARITY
- }
- });
- }
- });
+ if (/^https?:\/\//.test(source)) {
+ $.get("/iqdb_queries.js", {
+ limit: Upload.IQDB_LIMIT,
+ search: {
+ url: source,
+ similarity: Upload.IQDB_MIN_SIMILARITY,
+ high_similarity: Upload.IQDB_HIGH_SIMILARITY
+ }
+ });
+ }
}
Upload.initialize_image = function() {
- let $image = $("#image");
-
- if ($image.prop("complete")) {
- Upload.update_scale();
- } else {
- $image.on("load.danbooru", Upload.update_scale);
- }
-
- $(window).on("resize.danbooru", Upload.update_scale);
$(document).on("click.danbooru", "#image", Upload.toggle_size);
$(document).on("click.danbooru", "#upload-image-view-small", Upload.view_small);
$(document).on("click.danbooru", "#upload-image-view-large", Upload.view_large);
$(document).on("click.danbooru", "#upload-image-view-full", Upload.view_full);
}
-Upload.no_image_available = function(e) {
- $("#a-new").addClass("no-image-available");
-}
-
Upload.view_small = function(e) {
$("#image").addClass("fit-width fit-height");
- $("#a-new").attr("data-image-size", "small");
- Upload.update_scale();
+ $("#a-show").attr("data-image-size", "small");
e.preventDefault();
}
Upload.view_large = function(e) {
$("#image").removeClass("fit-height").addClass("fit-width");
- $("#a-new").attr("data-image-size", "large");
- Upload.update_scale();
+ $("#a-show").attr("data-image-size", "large");
e.preventDefault();
}
Upload.view_full = function(e) {
$("#image").removeClass("fit-width fit-height");
- $("#a-new").attr("data-image-size", "full");
- Upload.update_scale();
+ $("#a-show").attr("data-image-size", "full");
e.preventDefault();
}
Upload.toggle_size = function(e) {
let window_aspect_ratio = $(window).width() / $(window).height();
let image_aspect_ratio = $("#image").width() / $("#image").height();
- let image_size = $("#a-new").attr("data-image-size");
+ let image_size = $("#a-show").attr("data-image-size");
if (image_size === "small" && image_aspect_ratio >= window_aspect_ratio) {
Upload.view_full(e);
@@ -133,17 +81,6 @@ Upload.toggle_size = function(e) {
}
}
-Upload.update_scale = function() {
- let $image = $("#image");
-
- if ($image.length) {
- let natural_width = $image.get(0).naturalWidth;
- let natural_height = $image.get(0).naturalHeight;
- let scale_percentage = Math.round(100 * $image.width() / natural_width);
- $("#upload-image-metadata-resolution").html(`(${natural_width}x${natural_height}, resized to ${scale_percentage}%)`);
- }
-}
-
Upload.toggle_commentary = function() {
if ($(".artist-commentary").is(":visible")) {
$("#toggle-artist-commentary").text("show »");
@@ -165,69 +102,10 @@ Upload.toggle_translation = function() {
$(".commentary-translation").slideToggle();
};
-Upload.initialize_dropzone = function() {
- if (!window.FileReader) {
- $("#filedropzone").remove();
- return;
- }
-
- let dropzone = new Dropzone(document.body, {
- paramName: "upload[file]",
- url: "/uploads/preprocess",
- clickable: "#filedropzone",
- previewsContainer: "#filedropzone",
- thumbnailHeight: 150,
- thumbnailWidth: 150,
- thumbnailMethod: "contain",
- addRemoveLinks: false,
- maxFiles: 1,
- maxFilesize: Upload.max_file_size(),
- maxThumbnailFilesize: Upload.max_file_size(),
- timeout: 0,
- acceptedFiles: "image/jpeg,image/png,image/gif,video/mp4,video/webm",
- previewTemplate: $("#dropzone-preview-template").html(),
- init: function() {
- $(".fallback").hide();
- this.on("complete", function(file) {
- $("#filedropzone .dz-progress").hide();
- });
- this.on("addedfile", function(file) {
- $("#filedropzone .dropzone-hint").hide();
-
- // replace the previous file with the new one.
- dropzone.files.forEach(f => {
- if (f !== file) {
- dropzone.removeFile(f);
- }
- });
-
- let reader = new FileReader();
- reader.addEventListener("loadend", function() {
- let buf = new SparkMD5.ArrayBuffer();
- buf.append(this.result);
- let hash = buf.end();
- $("#upload_md5_confirmation").val(hash);
- });
- reader.readAsArrayBuffer(file);
- });
- this.on("success", function(file) {
- $("#filedropzone").addClass("success");
- });
- this.on("error", function(file, msg) {
- $("#filedropzone").addClass("error");
- });
- }
- });
-};
-
Upload.batch_open_all = function() {
$(".upload-preview > a").each((_i, link) => window.open(link.href));
};
-Upload.max_file_size = function() {
- return Number($("meta[name=max-file-size]").attr("content")) / (1024 * 1024);
-};
-
$(function() {
Upload.initialize_all();
});
diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss
index b3c3f889f..2788b33f4 100644
--- a/app/javascript/src/styles/common/utilities.scss
+++ b/app/javascript/src/styles/common/utilities.scss
@@ -12,6 +12,8 @@ $spacer: 0.25rem; /* 4px */
.font-monospace { font: var(--monospace-font); }
.font-bold { font-weight: bold; }
+.cursor-pointer { cursor: pointer; }
+
.hidden { display: none !important; }
.inline-block { display: inline-block; }
.block { display: block; }
@@ -36,14 +38,27 @@ $spacer: 0.25rem; /* 4px */
.leading-none { line-height: 1; }
.absolute { position: absolute; }
+.relative { position: relative; }
.top-0\.5 { top: 0.5 * $spacer; }
.bottom-0\.5 { bottom: 0.5 * $spacer; }
.left-0\.5 { left: 0.5 * $spacer; }
.right-0\.5 { right: 0.5 * $spacer; }
-.rounded-sm { border-radius: 0.5 * $spacer; }
-.rounded { border-radius: 1 * $spacer; }
+.border, %border { border-width: 1px; }
+
+.rounded-sm, %rounded-sm { border-radius: 0.5 * $spacer; }
+.rounded, %rounded { border-radius: 1 * $spacer; }
+.rounded-lg, %rounded-lg { border-radius: 2 * $spacer; }
+
+.rounded-t-sm, %rounded-t-sm { border-top-left-radius: 0.5 * $spacer; border-top-right-radius: 0.5 * $spacer; }
+.rounded-t, %rounded-t { border-top-left-radius: 1 * $spacer; border-top-right-radius: 1 * $spacer; }
+.rounded-t-lg, %rounded-t-lg { border-top-left-radius: 2 * $spacer; border-top-right-radius: 2 * $spacer; }
+
+.shadow-md, %shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
+.shadow-lg, %shadow-lg { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }
+
+.object-contain { object-fit: contain; }
.m-0 { margin: 0; }
.m-px { margin: 1px; }
@@ -69,15 +84,31 @@ $spacer: 0.25rem; /* 4px */
.p-0\.5 { padding: 0.5 * $spacer; }
.p-4 { padding: 4 * $spacer; }
+.px-4 { padding-left: 4 * $spacer; padding-right: 4 * $spacer; }
+.px-8 { padding-left: 8 * $spacer; padding-right: 8 * $spacer; }
+
+.py-4 { padding-top: 4 * $spacer; padding-bottom: 4 * $spacer; }
+.py-8 { padding-top: 8 * $spacer; padding-bottom: 8 * $spacer; }
+
+.pt-4 { padding-top: 4 * $spacer; }
+.pt-8 { padding-top: 8 * $spacer; }
+
.pr-2 { padding-right: 2 * $spacer; }
.pr-4 { padding-right: 4 * $spacer; }
+.w-sm { width: 24rem; }
+.w-md { width: 28rem; }
.w-1\/4 { width: 25%; }
.w-full { width: 100%; }
+.max-w-full { max-width: 100%; }
+
+.h-1 { height: 1 * $spacer; }
.h-3 { height: 3 * $spacer; }
.h-10 { height: 10 * $spacer; }
+.max-h-360px { max-height: 360px; }
+
.space-x-1 > * + * { margin-left: 1 * $spacer; }
.space-x-2 > * + * { margin-left: 2 * $spacer; }
.space-x-4 > * + * { margin-left: 4 * $spacer; }
@@ -111,6 +142,12 @@ $spacer: 0.25rem; /* 4px */
.grid-cols-8 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+.card {
+ @extend %border;
+ @extend %rounded-lg;
+ @extend %shadow-md;
+}
+
.thin-scrollbar {
overflow-x: hidden;
overflow-y: auto;
diff --git a/app/javascript/src/styles/specific/dropzone.scss b/app/javascript/src/styles/specific/dropzone.scss
deleted file mode 100644
index 1146d4082..000000000
--- a/app/javascript/src/styles/specific/dropzone.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-#filedropzone {
- background: var(--uploads-dropzone-background);
- padding: 0;
- min-height: 100px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- cursor: pointer;
- position: relative;
-
- .dz-preview {
- display: flex;
- flex-direction: column;
- text-align: center;
-
- &.dz-image-preview img {
- margin: 1em 0;
- object-fit: contain;
- }
-
- .dz-details {
- margin-bottom: 1em;
-
- .dz-filename, .dz-size {
- display: inline;
- }
- }
- }
-
- &.dz-started .dropzone-hint {
- display: none;
- }
-
- &.error {
- background: var(--error-background-color);
- }
-
- &.success {
- background: var(--success-background-color);
- }
-}
-
-.dz-progress {
- position: absolute;
- width: 100%;
- bottom: 0;
- left: 0;
- height: 5px;
-
- background-color: var(--uploads-dropzone-progress-bar-background-color);
-
- .dz-upload {
- background-color: var(--uploads-dropzone-progress-bar-foreground-color);
- display: block;
- height: 5px;
- }
-}
diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss
index 30f85e5a4..52839b4cf 100644
--- a/app/javascript/src/styles/specific/posts.scss
+++ b/app/javascript/src/styles/specific/posts.scss
@@ -25,7 +25,7 @@
#edit-dialog {
/* Hide everything but the rating and tags fields. */
.post_has_embedded_notes_fieldset, .post_lock_fieldset, .post_parent_id,
- .post_source, #filedropzone, .upload_as_pending, .upload_source_container,
+ .post_source, .dropzone-container, .upload_as_pending, .upload_source_container,
.upload_parent_id, .upload_artist_commentary_container, .upload_commentary_translation_container {
display: none;
}
diff --git a/app/javascript/src/styles/specific/uploads.scss b/app/javascript/src/styles/specific/uploads.scss
index 05c838ea9..33be911bf 100644
--- a/app/javascript/src/styles/specific/uploads.scss
+++ b/app/javascript/src/styles/specific/uploads.scss
@@ -1,19 +1,5 @@
div#c-uploads {
- div#a-new {
- #no-image-available {
- display: none;
- }
-
- &.no-image-available {
- #upload-image-metadata, #image, #iqdb-similar {
- display: none;
- }
-
- #no-image-available {
- display: block !important;
- }
- }
-
+ div#a-show {
&[data-image-size="small"] {
#image {
cursor: zoom-in;
@@ -74,10 +60,6 @@ div#c-uploads {
}
}
- textarea#upload_tag_string {
- width: 100%;
- }
-
div.field_with_errors {
display: inline;
}
diff --git a/app/jobs/process_upload_job.rb b/app/jobs/process_upload_job.rb
new file mode 100644
index 000000000..0476b5ccf
--- /dev/null
+++ b/app/jobs/process_upload_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ProcessUploadJob < ApplicationJob
+ queue_with_priority -1
+
+ def perform(upload)
+ upload.process_upload!
+ end
+end
diff --git a/app/jobs/upload_preprocessor_delayed_start_job.rb b/app/jobs/upload_preprocessor_delayed_start_job.rb
deleted file mode 100644
index d0ea808fe..000000000
--- a/app/jobs/upload_preprocessor_delayed_start_job.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-# A job that downloads and generates thumbnails in the background for an image
-# uploaded with the upload bookmarklet.
-class UploadPreprocessorDelayedStartJob < ApplicationJob
- queue_as :default
- queue_with_priority(-1)
-
- def perform(source, referer_url, uploader)
- UploadService::Preprocessor.new(source: source, referer_url: referer_url).delayed_start(uploader)
- end
-end
diff --git a/app/jobs/upload_service_delayed_start_job.rb b/app/jobs/upload_service_delayed_start_job.rb
deleted file mode 100644
index ff910e595..000000000
--- a/app/jobs/upload_service_delayed_start_job.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-# A job that tries to resume a preprocessed image upload job.
-class UploadServiceDelayedStartJob < ApplicationJob
- queue_as :default
- queue_with_priority(-1)
-
- def perform(params, uploader)
- UploadService.new(params).delayed_start(uploader)
- end
-end
diff --git a/app/logical/sources/strategies/base.rb b/app/logical/sources/strategies/base.rb
index a060c70b8..e17aa9541 100644
--- a/app/logical/sources/strategies/base.rb
+++ b/app/logical/sources/strategies/base.rb
@@ -214,15 +214,6 @@ module Sources
{}
end
- # Returns the size of the image resource without actually downloading the file.
- def remote_size
- response = http_downloader.head(image_url)
- return nil unless response.status == 200 && response.content_length.present?
-
- response.content_length.to_i
- end
- memoize :remote_size
-
# Download the file at the given url, or at the main image url by default.
def download_file!(download_url = image_url)
raise DownloadError, "Download failed: couldn't find download url for #{url}" if download_url.blank?
diff --git a/app/logical/upload_service.rb b/app/logical/upload_service.rb
deleted file mode 100644
index c2aca550b..000000000
--- a/app/logical/upload_service.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-# frozen_string_literal: true
-
-# A service object for uploading an image.
-class UploadService
- attr_reader :params, :post, :upload
-
- def initialize(params)
- @params = params
- end
-
- def delayed_start(uploader)
- CurrentUser.scoped(uploader) do
- start!
- end
- rescue ActiveRecord::RecordNotUnique
- end
-
- def start!
- preprocessor = Preprocessor.new(params)
-
- if preprocessor.in_progress?
- UploadServiceDelayedStartJob.set(wait: 5.seconds).perform_later(params, CurrentUser.user)
- return preprocessor.predecessor
- end
-
- if preprocessor.completed?
- @upload = preprocessor.finish!
-
- begin
- create_post_from_upload(@upload)
- rescue Exception => e
- @upload.update(status: "error: #{e.class} - #{e.message}", backtrace: e.backtrace.join("\n"))
- end
- return @upload
- end
-
- params[:rating] ||= "q"
- params[:tag_string] ||= "tagme"
- @upload = Upload.create!(params)
-
- begin
- if @upload.invalid?
- return @upload
- end
-
- @upload.update(status: "processing")
-
- @upload.file = Utils.get_file_for_upload(@upload.source_url, @upload.referer_url, @upload.file&.tempfile)
- Utils.process_file(upload, @upload.file)
-
- @upload.save!
- @post = create_post_from_upload(@upload)
- @upload
- rescue Exception => e
- @upload.update(status: "error: #{e.class} - #{e.message}", backtrace: e.backtrace.join("\n"))
- @upload
- end
- end
-
- def warnings
- return [] if @post.nil?
- @post.warnings.full_messages
- end
-
- def create_post_from_upload(upload)
- @post = convert_to_post(upload)
- @post.save!
-
- if upload.has_commentary?
- @post.create_artist_commentary(
- :original_title => upload.artist_commentary_title,
- :original_description => upload.artist_commentary_desc,
- :translated_title => upload.translated_commentary_title,
- :translated_description => upload.translated_commentary_desc
- )
- end
-
- @post.update_iqdb
-
- upload.update(status: "completed", post_id: @post.id)
-
- @post
- end
-
- def convert_to_post(upload)
- Post.new.tap do |p|
- p.tag_string = upload.tag_string
- p.md5 = upload.md5
- p.file_ext = upload.file_ext
- p.image_width = upload.image_width
- p.image_height = upload.image_height
- p.rating = upload.rating
- if upload.source.present?
- p.source = Sources::Strategies.find(upload.source, upload.referer_url).canonical_url || upload.source
- end
- p.file_size = upload.file_size
- p.uploader_id = upload.uploader_id
- p.uploader_ip_addr = upload.uploader_ip_addr
- p.parent_id = upload.parent_id
-
- if !upload.uploader.can_upload_free? || upload.upload_as_pending?
- p.is_pending = true
- end
- end
- end
-end
diff --git a/app/logical/upload_service/controller_helper.rb b/app/logical/upload_service/controller_helper.rb
deleted file mode 100644
index 677fe2c6f..000000000
--- a/app/logical/upload_service/controller_helper.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-class UploadService
- module ControllerHelper
- def self.prepare(url: nil, file: nil, ref: nil)
- upload = Upload.new
-
- if Utils.is_downloadable?(url) && file.nil?
- # this gets called from UploadsController#new so we need to preprocess async
- UploadPreprocessorDelayedStartJob.perform_later(url, ref, CurrentUser.user)
-
- strategy = Sources::Strategies.find(url, ref)
- remote_size = strategy.remote_size
-
- return [upload, remote_size]
- end
-
- if file
- # this gets called via XHR so we can process sync
- Preprocessor.new(file: file).delayed_start(CurrentUser.user)
- end
-
- [upload]
- end
- end
-end
diff --git a/app/logical/upload_service/preprocessor.rb b/app/logical/upload_service/preprocessor.rb
deleted file mode 100644
index c38e31fef..000000000
--- a/app/logical/upload_service/preprocessor.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-class UploadService
- class Preprocessor
- extend Memoist
-
- attr_reader :params
-
- def initialize(params)
- @params = params
- end
-
- def source
- params[:source]
- end
-
- def md5
- params[:md5_confirmation]
- end
-
- def referer_url
- params[:referer_url]
- end
-
- def in_progress?
- if md5.present?
- Upload.exists?(status: "preprocessing", md5: md5)
- elsif Utils.is_downloadable?(source)
- Upload.exists?(status: "preprocessing", source: source)
- else
- false
- end
- end
-
- def predecessor
- if md5.present?
- Upload.where(status: ["preprocessed", "preprocessing"], md5: md5).first
- elsif Utils.is_downloadable?(source)
- Upload.where(status: ["preprocessed", "preprocessing"], source: source).first
- end
- end
-
- def completed?
- predecessor.present?
- end
-
- def delayed_start(uploader)
- CurrentUser.scoped(uploader) do
- start!
- end
- rescue ActiveRecord::RecordNotUnique
- end
-
- def start!
- params[:rating] ||= "q"
- params[:tag_string] ||= "tagme"
- upload = Upload.create!(params)
-
- begin
- upload.update(status: "preprocessing")
-
- file = Utils.get_file_for_upload(upload.source_url, upload.referer_url, params[:file]&.tempfile)
- Utils.process_file(upload, file)
-
- upload.rating = params[:rating]
- upload.tag_string = params[:tag_string]
- upload.status = "preprocessed"
- upload.save!
- rescue Exception => e
- upload.update(file_ext: nil, status: "error: #{e.class} - #{e.message}", backtrace: e.backtrace.join("\n"))
- end
-
- upload
- end
-
- def finish!(upload = nil)
- pred = upload || predecessor
-
- # regardless of who initialized the upload, credit should
- # goto whoever submitted the form
- pred.initialize_attributes
-
- pred.attributes = params
-
- # if a file was uploaded after the preprocessing occurred,
- # then process the file and overwrite whatever the preprocessor
- # did
- Utils.process_file(pred, pred.file.tempfile) if pred.file.present?
-
- pred.status = "completed"
- pred.save
- pred
- end
- end
-end
diff --git a/app/logical/upload_service/replacer.rb b/app/logical/upload_service/replacer.rb
index 19cee82fa..35416961d 100644
--- a/app/logical/upload_service/replacer.rb
+++ b/app/logical/upload_service/replacer.rb
@@ -26,7 +26,7 @@ class UploadService
end
def process!
- media_file = Utils::get_file_for_upload(replacement.replacement_url, nil, replacement.replacement_file&.tempfile)
+ media_file = get_file_for_upload(replacement.replacement_url, nil, replacement.replacement_file&.tempfile)
if Post.where.not(id: post.id).exists?(md5: media_file.md5)
raise Error, "Duplicate: post with md5 #{media_file.md5} already exists"
@@ -69,5 +69,15 @@ class UploadService
note.rescale!(x_scale, y_scale)
end
end
+
+ def get_file_for_upload(source_url, referer_url, file)
+ return MediaFile.open(file) if file.present?
+ raise "No file or source URL provided" if source_url.blank?
+
+ strategy = Sources::Strategies.find(source_url, referer_url)
+ raise NotImplementedError, "No login credentials configured for #{strategy.site_name}." unless strategy.class.enabled?
+
+ strategy.download_file!
+ end
end
end
diff --git a/app/logical/upload_service/utils.rb b/app/logical/upload_service/utils.rb
deleted file mode 100644
index 97ddce849..000000000
--- a/app/logical/upload_service/utils.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-class UploadService
- module Utils
- module_function
-
- def is_downloadable?(source)
- source =~ %r{\Ahttps?://}
- end
-
- def process_file(upload, file)
- media_file = MediaFile.open(file)
-
- upload.file = media_file
- upload.file_ext = media_file.file_ext.to_s
- upload.file_size = media_file.file_size
- upload.md5 = media_file.md5
- upload.image_width = media_file.width
- upload.image_height = media_file.height
-
- upload.validate!(:file)
-
- MediaAsset.upload!(media_file)
- end
-
- def get_file_for_upload(source_url, referer_url, file)
- return MediaFile.open(file) if file.present?
- raise "No file or source URL provided" if source_url.blank?
-
- strategy = Sources::Strategies.find(source_url, referer_url)
- raise NotImplementedError, "No login credentials configured for #{strategy.site_name}." unless strategy.class.enabled?
-
- strategy.download_file!
- end
- end
-end
diff --git a/app/models/media_asset.rb b/app/models/media_asset.rb
index 6e7b43e86..694c20dee 100644
--- a/app/models/media_asset.rb
+++ b/app/models/media_asset.rb
@@ -4,6 +4,10 @@ class MediaAsset < ApplicationRecord
class Error < StandardError; end
VARIANTS = %i[preview 180x180 360x360 720x720 sample original]
+ MAX_VIDEO_DURATION = 140 # 2:20
+ MAX_IMAGE_RESOLUTION = Danbooru.config.max_image_resolution
+ MAX_IMAGE_WIDTH = Danbooru.config.max_image_width
+ MAX_IMAGE_HEIGHT = Danbooru.config.max_image_height
ENABLE_SEO_POST_URLS = Danbooru.config.enable_seo_post_urls
LARGE_IMAGE_WIDTH = Danbooru.config.large_image_width
STORAGE_SERVICE = Danbooru.config.storage_manager
@@ -30,6 +34,9 @@ class MediaAsset < ApplicationRecord
}
validates :md5, uniqueness: { conditions: -> { where(status: [:processing, :active]) } }
+ validates :file_ext, inclusion: { in: %w[jpg png gif mp4 webm swf zip], message: "Not an image or video" }
+ validates :duration, numericality: { less_than_or_equal_to: MAX_VIDEO_DURATION, message: "must be less than #{MAX_VIDEO_DURATION} seconds", allow_nil: true }, on: :create # XXX should allow admins to bypass
+ validate :validate_resolution, on: :create
class Variant
extend Memoist
@@ -194,6 +201,8 @@ class MediaAsset < ApplicationRecord
# This can't be called inside a transaction because the transaction will
# fail if there's a RecordNotUnique error when the asset already exists.
def upload!(media_file)
+ raise Error, "File is corrupt" if media_file.is_corrupt?
+
media_asset = create!(file: media_file, status: :processing)
media_asset.distribute_files!(media_file)
media_asset.update!(status: :active)
@@ -318,4 +327,18 @@ class MediaAsset < ApplicationRecord
is_animated? && file_ext == "png"
end
end
+
+ concerning :ValidationMethods do
+ def validate_resolution
+ resolution = image_width * image_height
+
+ if resolution > MAX_IMAGE_RESOLUTION
+ errors.add(:base, "Image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{image_width}x#{image_height}); max: #{MAX_IMAGE_RESOLUTION / 1_000_000} megapixels)")
+ elsif image_width > MAX_IMAGE_WIDTH
+ errors.add(:image_width, "is too large (width: #{image_width}; max width: #{MAX_IMAGE_WIDTH})")
+ elsif image_height > MAX_IMAGE_HEIGHT
+ errors.add(:image_height, "is too large (height: #{image_height}; max height: #{MAX_IMAGE_HEIGHT})")
+ end
+ end
+ end
end
diff --git a/app/models/post.rb b/app/models/post.rb
index ede050c04..b82aa67aa 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -18,8 +18,9 @@ class Post < ApplicationRecord
before_validation :parse_pixiv_id
before_validation :blank_out_nonexistent_parents
before_validation :remove_parent_loops
- validates :md5, uniqueness: { message: ->(post, _data) { "duplicate: #{Post.find_by_md5(post.md5).id}" }}, on: :create
- validates :rating, inclusion: { in: %w[s q e], message: "rating must be s, q, or e" }
+ validates :md5, uniqueness: { message: ->(post, _data) { "Duplicate of post ##{Post.find_by_md5(post.md5).id}" }}, on: :create
+ validates :rating, presence: { message: "not selected" }
+ validates :rating, inclusion: { in: %w[s q e], message: "must be S, Q, or E" }, if: -> { rating.present? }
validates :source, length: { maximum: 1200 }
validate :added_tags_are_valid
validate :removed_tags_are_valid
@@ -34,6 +35,7 @@ class Post < ApplicationRecord
after_save :create_version
after_save :update_parent_on_save
after_save :apply_post_metatags
+ after_create_commit :update_iqdb
belongs_to :approver, class_name: "User", optional: true
belongs_to :uploader, :class_name => "User", :counter_cache => "post_upload_count"
@@ -73,6 +75,36 @@ class Post < ApplicationRecord
has_many :versions, -> { Rails.env.test? ? order("post_versions.updated_at ASC, post_versions.id ASC") : order("post_versions.updated_at ASC") }, class_name: "PostVersion", dependent: :destroy
end
+ def self.new_from_upload(params)
+ upload_media_asset = UploadMediaAsset.find(params[:upload_media_asset_id])
+ media_asset = upload_media_asset.media_asset
+ upload = upload_media_asset.upload
+
+ # XXX depends on CurrentUser
+ commentary = ArtistCommentary.new(
+ original_title: params[:artist_commentary_title],
+ original_description: params[:artist_commentary_desc],
+ translated_title: params[:translated_commentary_title],
+ translated_description: params[:translated_commentary_desc],
+ )
+
+ post = Post.new(
+ uploader: upload.uploader,
+ uploader_ip_addr: upload.uploader_ip_addr,
+ md5: media_asset.md5,
+ file_ext: media_asset.file_ext,
+ file_size: media_asset.file_size,
+ image_width: media_asset.image_width,
+ image_height: media_asset.image_height,
+ source: Sources::Strategies.find(upload.source, upload.referer_url).canonical_url || upload.source,
+ tag_string: params[:tag_string],
+ rating: params[:rating],
+ parent_id: params[:parent_id],
+ is_pending: !upload.uploader.can_upload_free? || params[:is_pending].to_s.truthy?,
+ artist_commentary: (commentary if commentary.any_field_present?),
+ )
+ end
+
module FileMethods
extend ActiveSupport::Concern
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 3c7d4b4ae..654f37ac5 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -1,100 +1,28 @@
# frozen_string_literal: true
class Upload < ApplicationRecord
- class Error < StandardError; end
-
MAX_VIDEO_DURATION = 140
- class FileValidator < ActiveModel::Validator
- def validate(record)
- validate_file_ext(record)
- validate_integrity(record)
- validate_md5_uniqueness(record)
- validate_video_duration(record)
- validate_resolution(record)
- end
+ attr_accessor :file
- def validate_file_ext(record)
- if record.file_ext.in?(["bin", "swf"])
- record.errors.add(:file_ext, "is invalid (only JPEG, PNG, GIF, MP4, and WebM files are allowed")
- end
- end
+ belongs_to :uploader, class_name: "User"
+ has_many :upload_media_assets, dependent: :destroy
+ has_many :media_assets, through: :upload_media_assets
- def validate_integrity(record)
- if record.file.is_corrupt?
- record.errors.add(:file, "is corrupted")
- end
- end
+ validates :source, format: { with: %r{\Ahttps?://}i, message: "is not a valid URL" }, if: -> { source.present? }
+ validates :referer_url, format: { with: %r{\Ahttps?://}i, message: "is not a valid URL" }, if: -> { referer_url.present? }
- def validate_md5_uniqueness(record)
- if record.md5.nil?
- return
- end
-
- md5_post = Post.find_by_md5(record.md5)
-
- if md5_post.nil?
- return
- end
-
- if record.replaced_post && record.replaced_post == md5_post
- return
- end
-
- record.errors.add(:md5, "duplicate: #{md5_post.id}")
- end
-
- def validate_resolution(record)
- resolution = record.image_width.to_i * record.image_height.to_i
-
- if resolution > Danbooru.config.max_image_resolution
- record.errors.add(:base, "image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{record.image_width}x#{record.image_height}); max: #{Danbooru.config.max_image_resolution / 1_000_000} megapixels)")
- elsif record.image_width > Danbooru.config.max_image_width
- record.errors.add(:image_width, "is too large (width: #{record.image_width}; max width: #{Danbooru.config.max_image_width})")
- elsif record.image_height > Danbooru.config.max_image_height
- record.errors.add(:image_height, "is too large (height: #{record.image_height}; max height: #{Danbooru.config.max_image_height})")
- end
- end
-
- def validate_video_duration(record)
- if !record.uploader.is_admin? && record.file.is_video? && record.file.duration.to_i > MAX_VIDEO_DURATION
- record.errors.add(:base, "video must not be longer than #{MAX_VIDEO_DURATION.seconds.inspect}")
- end
- end
- end
-
- attr_accessor :as_pending, :replaced_post, :file
-
- belongs_to :uploader, :class_name => "User"
- belongs_to :post, optional: true
- has_one :media_asset, foreign_key: :md5, primary_key: :md5
-
- before_validation :initialize_attributes, on: :create
- before_validation :assign_rating_from_tags
- # validates :source, format: { with: /\Ahttps?/ }, if: ->(record) {record.file.blank?}, on: :create
- validates :rating, inclusion: { in: %w[q e s] }, allow_nil: true
- validates :md5, confirmation: true, if: ->(rec) { rec.md5_confirmation.present? }
- validates_with FileValidator, on: :file
- serialize :context, JSON
+ after_create :async_process_upload!
scope :pending, -> { where(status: "pending") }
scope :preprocessed, -> { where(status: "preprocessed") }
scope :completed, -> { where(status: "completed") }
- scope :uploaded_by, ->(user_id) { where(uploader_id: user_id) }
-
- def initialize_attributes
- self.uploader_id = CurrentUser.id
- self.uploader_ip_addr = CurrentUser.ip_addr
- self.server = Socket.gethostname
- end
def self.visible(user)
if user.is_admin?
all
- elsif user.is_anonymous?
- completed
else
- completed.or(where(uploader: user))
+ where(uploader: user)
end
end
@@ -111,98 +39,44 @@ class Upload < ApplicationRecord
status == "completed"
end
- def is_preprocessed?
- status == "preprocessed"
- end
-
- def is_preprocessing?
- status == "preprocessing"
- end
-
- def is_duplicate?
- status.match?(/duplicate: \d+/)
- end
-
def is_errored?
status.match?(/error:/)
end
-
- def sanitized_status
- if is_errored?
- status.sub(/DETAIL:.+/m, "...")
- else
- status
- end
- end
-
- def duplicate_post_id
- @duplicate_post_id ||= status[/duplicate: (\d+)/, 1]
- end
- end
-
- concerning :SourceMethods do
- def source=(source)
- source = source.unicode_normalize(:nfc)
-
- # percent encode unicode characters in urls
- if source =~ %r{\Ahttps?://}i
- source = Addressable::URI.normalized_encode(source) rescue source
- end
-
- super(source)
- end
-
- def source_url
- return nil unless source =~ %r{\Ahttps?://}i
- Addressable::URI.heuristic_parse(source) rescue nil
- end
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url, :uploader, :post)
-
- if params[:source_matches].present?
- q = q.where_like(:source, params[:source_matches])
- end
-
- if params[:has_post].to_s.truthy?
- q = q.where.not(post_id: nil)
- elsif params[:has_post].to_s.falsy?
- q = q.where(post_id: nil)
- end
-
- if params[:status].present?
- q = q.where_like(:status, params[:status])
- end
-
- if params[:backtrace].present?
- q = q.where_like(:backtrace, params[:backtrace])
- end
-
- if params[:tag_string].present?
- q = q.where_like(:tag_string, params[:tag_string])
- end
-
+ q = search_attributes(params, :id, :created_at, :updated_at, :source, :referer_url, :uploader, :status, :backtrace, :upload_media_assets, :media_assets)
q.apply_default_order(params)
end
- def assign_rating_from_tags
- rating = PostQueryBuilder.new(tag_string).find_metatag(:rating)
-
- if rating.present?
- self.rating = rating.downcase.first
+ def async_process_upload!
+ if file.present?
+ ProcessUploadJob.perform_now(self)
+ else
+ ProcessUploadJob.perform_later(self)
end
end
- def upload_as_pending?
- as_pending.to_s.truthy?
- end
+ def process_upload!
+ update!(status: "processing")
- def has_commentary?
- artist_commentary_title.present? || artist_commentary_desc.present? || translated_commentary_title.present? || translated_commentary_desc.present?
+ if file.present?
+ media_file = MediaFile.open(file.tempfile)
+ elsif source.present?
+ strategy = Sources::Strategies.find(source, referer_url)
+ media_file = strategy.download_file!(strategy.image_url)
+ else
+ raise "No file or source provided"
+ end
+
+ media_asset = MediaAsset.upload!(media_file)
+ update!(media_assets: [media_asset], status: "completed")
+ rescue Exception => e
+ update!(status: "error: #{e.message}", backtrace: e.backtrace.join("\n"))
+ raise
end
def self.available_includes
- [:uploader, :post]
+ [:uploader, :upload_media_assets, :media_assets]
end
end
diff --git a/app/policies/post_policy.rb b/app/policies/post_policy.rb
index 9141e993f..84f596771 100644
--- a/app/policies/post_policy.rb
+++ b/app/policies/post_policy.rb
@@ -13,6 +13,10 @@ class PostPolicy < ApplicationPolicy
unbanned? && record.visible?
end
+ def create?
+ unbanned? && record.uploader == user
+ end
+
def revert?
update?
end
@@ -66,11 +70,14 @@ class PostPolicy < ApplicationPolicy
user.is_gold?
end
- def permitted_attributes
- [
- :tag_string, :old_tag_string, :parent_id, :old_parent_id,
- :source, :old_source, :rating, :old_rating, :has_embedded_notes,
- ].compact
+ def permitted_attributes_for_create
+ %i[upload_media_asset_id tag_string rating parent_id source is_pending
+ artist_commentary_desc artist_commentary_title translated_commentary_desc
+ translated_commentary_title]
+ end
+
+ def permitted_attributes_for_update
+ %i[tag_string old_tag_string parent_id old_parent_id source old_source rating old_rating has_embedded_notes]
end
def api_attributes
diff --git a/app/policies/upload_policy.rb b/app/policies/upload_policy.rb
index e0c065647..43810e888 100644
--- a/app/policies/upload_policy.rb
+++ b/app/policies/upload_policy.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class UploadPolicy < ApplicationPolicy
+ def create?
+ unbanned?
+ end
+
def show?
- record.is_completed? || user.is_admin? || record.uploader_id == user.id
+ user.is_admin? || record.uploader_id == user.id
end
def batch?
@@ -13,23 +17,9 @@ class UploadPolicy < ApplicationPolicy
unbanned?
end
- def preprocess?
- unbanned?
- end
-
- def can_view_tags?
- user.is_admin? || record.uploader_id == user.id
- end
-
def permitted_attributes
%i[file source tag_string rating status parent_id artist_commentary_title
artist_commentary_desc referer_url
md5_confirmation as_pending translated_commentary_title translated_commentary_desc]
end
-
- def api_attributes
- attributes = super
- attributes -= [:tag_string] unless can_view_tags?
- attributes
- end
end
diff --git a/app/views/sources/show.js.erb b/app/views/sources/show.js.erb
index 18eace131..b461f6991 100644
--- a/app/views/sources/show.js.erb
+++ b/app/views/sources/show.js.erb
@@ -5,8 +5,8 @@ $(document).trigger("danbooru:update-source-data", {
related_tags_html: "<%= j render "related_tags/source_tags", source: @source %>",
});
-if ($("#c-uploads #a-new").length) {
- $("#upload_artist_commentary_title").val(<%= raw @source.dtext_artist_commentary_title.to_json %>);
- $("#upload_artist_commentary_desc").val(<%= raw @source.dtext_artist_commentary_desc.to_json %>);
+if ($("#c-uploads #a-show").length) {
+ $("#post_artist_commentary_title").val(<%= raw @source.dtext_artist_commentary_title.to_json %>);
+ $("#post_artist_commentary_desc").val(<%= raw @source.dtext_artist_commentary_desc.to_json %>);
Danbooru.Upload.toggle_commentary();
}
diff --git a/app/views/uploads/_dropzone_preview.html.erb b/app/views/uploads/_dropzone_preview.html.erb
deleted file mode 100644
index 4e6c39e81..000000000
--- a/app/views/uploads/_dropzone_preview.html.erb
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
![]()
-
-
-
-
-
-
-
-
diff --git a/app/views/uploads/_image.html.erb b/app/views/uploads/_image.html.erb
deleted file mode 100644
index 4ef2a8d6d..000000000
--- a/app/views/uploads/_image.html.erb
+++ /dev/null
@@ -1,22 +0,0 @@
-<% if params[:url] %>
-
- Size
- <% if @remote_size.present? %>
- <%= number_to_human_size(@remote_size) %>
- <% end %>
-
-
- (small | large | full)
-
-
-
-
- <% if ImageProxy.needs_proxy?(@source.image_url) %>
- <%= tag.img src: image_proxy_uploads_path(url: @source.image_url), title: "Preview", id: "image", class: "fit-width fit-height", onerror: "Danbooru.Upload.no_image_available()", "data-shortcut": "z" %>
- <% elsif @source.image_url.present? %>
- <%= tag.img src: @source.image_url, title: "Preview", id: "image", referrerpolicy: "no-referrer", class: "fit-width fit-height", onerror: "Danbooru.Upload.no_image_available()", "data-shortcut": "z" %>
- <% end %>
-
-
No image preview available
-
-<% end %>
diff --git a/app/views/uploads/_secondary_links.html.erb b/app/views/uploads/_secondary_links.html.erb
index 03df67107..34d793277 100644
--- a/app/views/uploads/_secondary_links.html.erb
+++ b/app/views/uploads/_secondary_links.html.erb
@@ -1,7 +1,7 @@
<% content_for(:secondary_links) do %>
- <%= subnav_link_to "Listing", uploads_path %>
- <%= subnav_link_to "New", new_upload_path %>
+ <%= subnav_link_to "My Uploads", uploads_path %>
+ <%= subnav_link_to "New Upload", new_upload_path %>
<%= subnav_link_to "Batch Upload", batch_uploads_path %>
- <%= subnav_link_to "Similar Images Search", iqdb_queries_path %>
+ <%= subnav_link_to "Reverse Image Search", iqdb_queries_path %>
<%= subnav_link_to "Help", wiki_page_path("help:upload") %>
<% end %>
diff --git a/app/views/uploads/index.html.erb b/app/views/uploads/index.html.erb
index 449b3a1f7..086559a05 100644
--- a/app/views/uploads/index.html.erb
+++ b/app/views/uploads/index.html.erb
@@ -1,61 +1,70 @@
- <%= render "uploads/search" %>
- <%= render "posts/partials/common/inline_blacklist" %>
+ <%= search_form_for(uploads_path) do |f| %>
+ <%= f.input :uploader_name, label: "Uploader", input_html: { value: params[:search][:uploader_name], data: { autocomplete: "user" } } %>
+ <%= f.input :source_like, label: "Source", input_html: { value: params[:search][:source_like] } %>
+ <%= f.input :status_like, label: "Status", collection: [%w[Completed completed], %w[Processing processing], %w[Pending pending], %w[Error error*]], include_blank: true, selected: params[:search][:status_like] %>
+
+ <%= f.submit "Search" %>
+ <% end %>
<%= table_for @uploads, class: "striped autofit", width: "100%" do |t| %>
- <% t.column "Upload" do |upload| %>
- <%= post_preview(upload.post, tags: "user:#{upload.uploader.name}", show_deleted: true) %>
- <% end %>
- <% t.column "Info", td: {class: "col-expand upload-info"} do |upload| %>
-
- Upload
- <%= link_to "##{upload.id}", upload %>
-
-
-
- Rating
- <%= upload.rating %>
-
-
- <% if upload.post.present? %>
-
- Size
- <%= link_to "#{upload.post.file_size.to_formatted_s(:human_size, precision: 4)} #{upload.post.file_ext}", upload.post.file_url %>
- (<%= upload.post.image_width %>x<%= upload.post.image_height %>)
-
+ <% t.column "File" do |upload| %>
+ <% upload.media_assets.first.tap do |media_asset| %>
+ <% if media_asset.present? %>
+ <%= link_to upload, class: "inline-block" do %>
+ <%= tag.img src: media_asset.variant("180x180").file_url %>
+ <% end %>
+ <% end %>
<% end %>
-
+ <% end %>
-
+ <% t.column "Info", td: {class: "col-expand upload-info"} do |upload| %>
+
+ Upload
+ <%= link_to "##{upload.id}", upload %>
+
+
+
Source
- <%= link_to_if (upload.source =~ %r!\Ahttps?://!i), (upload.source.presence.try(:truncate, 50) || content_tag(:em, "none")), upload.source %>
- <%= link_to "»", uploads_path(search: params[:search].merge(source_matches: upload.source)) %>
-
-
+
+ <% if upload.source.present? %>
+ <%= external_link_to upload.source %>
+ <%= link_to "»", uploads_path(search: params[:search].merge(source_like: upload.source)) %>
+ <% else %>
+ none
+ <% end %>
+
+
<% if upload.referer_url.present? %>
-
- Referer
- <%= URI.parse(upload.referer_url).host rescue nil %>
-
-
+
+ Referrer
+
+ <%= external_link_to upload.referer_url %>
+ <%= link_to "»", uploads_path(search: params[:search].merge(referer_url: upload.referer_url)) %>
+
+
<% end %>
- <% if policy(upload).can_view_tags? %>
-
- Tags
- <%= render_inline_tag_list_from_names(upload.tag_string.split) %>
-
+ <% if upload.is_errored? %>
+
+ Error
+
+ <%= upload.status.delete_prefix("error: ") %>
+
+
<% end %>
<% end %>
+
<% t.column "Uploader" do |upload| %>
<%= link_to_user upload.uploader %>
<%= link_to "»", uploads_path(search: params[:search].merge(uploader_name: upload.uploader.name)) %>
-
<%= time_ago_in_words_tagged upload.created_at %>
+ <%= time_ago_in_words_tagged upload.created_at %>
<% end %>
- <% t.column "Status", td: {class: "col-normal"} do |upload| %>
- <%= render_status(upload) %>
+
+ <% t.column :status do |upload| %>
+ <%= upload.is_errored? ? "error" : upload.status %>
<% end %>
<% end %>
diff --git a/app/views/uploads/new.html.erb b/app/views/uploads/new.html.erb
index eea9099eb..81c042983 100644
--- a/app/views/uploads/new.html.erb
+++ b/app/views/uploads/new.html.erb
@@ -1,100 +1,11 @@
-<%= content_for :html_header do %>
-
-<% end %>
-
-
Upload
+
Upload
- <% if CurrentUser.user.upload_limit.limited? %>
-
You have reached your upload limit
- <% else %>
- <%= embed_wiki("help:upload_notice", id: "upload-guide-notice") %>
-
- <% unless CurrentUser.can_upload_free? %>
-
- Upload Limit: <%= render "users/upload_limit", user: CurrentUser.user %>
-
- <% end %>
-
- <%= render "image" %>
- <%= render "related_posts", source: @source %>
- <%= render_source_data(nil) %>
-
-
-
- <%= edit_form_for(@upload, html: { id: "form" }) do |f| %>
- <%= f.input :md5_confirmation, as: :hidden %>
- <%= f.input :referer_url, as: :hidden, input_html: { value: params[:ref] } %>
-
- <% if CurrentUser.can_upload_free? %>
- <%= f.input :as_pending, as: :boolean, label: "Upload for approval", input_html: { checked: params[:as_pending].present? } %>
- <% end %>
-
- <%= f.input :file, as: :file, size: 50, wrapper_html: { class: "fallback" } %>
-
-
-
-
- <%= f.label :source %>
- <%= f.input_field :source, as: :string, placeholder: "Enter the URL to upload here", value: params[:url] %>
- <%= tag.button "Similar", id: "similar-button" %>
-
-
- <%= f.input :rating, collection: [["Explicit", "e"], ["Questionable", "q"], ["Safe", "s"]], as: :radio_buttons, selected: params[:rating] %>
-
- <%= f.input :parent_id, label: "Parent ID", as: :string, input_html: { value: params[:parent_id] } %>
-
-
-
-
-
-
-
-
- <%= f.input :tag_string, label: false, hint: "Ctrl+Enter to submit", input_html: { "data-autocomplete": "tag-edit", "data-shortcut": "e", value: params[:tag_string] } %>
- <%= render "related_tags/buttons" %>
-
-
- <%= f.submit "Submit", id: "submit-button", data: { disable_with: false } %>
-
- <%= render "related_tags/container" %>
- <% end %>
- <% end %>
+
+ <%= render FileUploadComponent.new(url: @upload.source, referer_url: @upload.referer_url, drop_target: "body") %>
+
-
- <%= render "dropzone_preview" %>
-
-
<%= render "uploads/secondary_links" %>
diff --git a/app/views/uploads/show.html.erb b/app/views/uploads/show.html.erb
index e5392dbc0..9608d3813 100644
--- a/app/views/uploads/show.html.erb
+++ b/app/views/uploads/show.html.erb
@@ -1,51 +1,110 @@
-
-
Upload #<%= @upload.id %>
+
+
Upload
-
- - Date: <%= @upload.created_at %>
- - Source: <%= @upload.source %>
- <% if policy(@upload).can_view_tags? %>
- - Tags: <%= @upload.tag_string %>
- <% end %>
- <% if @upload.md5.present? %>
- - MD5: <%= @upload.md5 %>
- <% end %>
- <% if @upload.file_size.present? %>
-
-
- Size: <%= number_to_human_size(@upload.file_size) %>
- <% if @upload.image_width.present? %>
- (<%= @upload.image_width %>x<%= @upload.image_height %>)
- <% end %>
-
- <% end %>
-
-
- <% if @upload.is_completed? %>
-
This upload has finished processing. <%= link_to "View the post", post_path(@upload.post_id) %>.
- <% elsif @upload.is_pending? %>
-
This upload is waiting to be processed. Please wait a few seconds.
- <% elsif @upload.is_processing? || @upload.is_preprocessing? || @upload.is_preprocessed? %>
-
This upload is being processed. Please wait a few seconds.
- <% elsif @upload.is_duplicate? %>
-
This upload is a duplicate: <%= link_to "post ##{@upload.duplicate_post_id}", post_path(@upload.duplicate_post_id) %>
- <% else %>
-
An error occurred: <%= render_status(@upload) %>
- <% if CurrentUser.user.is_builder? %>
- <%= render "static/backtrace", backtrace: @upload.backtrace.to_s.split(/\n/) %>
+ <% if @upload.is_pending? || @upload.is_processing? %>
+ <% content_for(:html_header) do %>
+
<% end %>
<% end %>
-
- You can <%= link_to "upload another file", new_upload_path %> or <%= link_to "view your current uploads", uploads_path(:search => {:uploader_id => CurrentUser.id}) %>.
-
+ <% if @upload.is_errored? %>
+
<%= @upload.status %>
+ <% elsif @upload.is_pending? && @upload.source.present? %>
+
Preparing to upload <%= external_link_to @upload.source %>...
+ <% elsif @upload.is_processing? && @upload.source.present? %>
+
Processing <%= external_link_to @upload.source %>...
+ <% elsif !@upload.is_completed? %>
+
Processing upload...
+ <% elsif CurrentUser.user.upload_limit.limited? %>
+
You have reached your upload limit. Please wait for your pending uploads to be approved before uploading more.
+
+
Upload Limit: <%= render "users/upload_limit", user: CurrentUser.user %>
+ <% else %>
+ <%= embed_wiki("help:upload_notice", id: "upload-guide-notice") %>
+
+ <% unless CurrentUser.can_upload_free? %>
+
Upload Limit: <%= render "users/upload_limit", user: CurrentUser.user %>
+ <% end %>
+
+
+
+ <% @upload_media_asset = @upload.upload_media_assets.first %>
+ <% @media_asset = @upload_media_asset.media_asset %>
+
+
+ Size
+ <%= number_to_human_size(@media_asset.file_size) %>
+ <%= @media_asset.image_width %>x<%= @media_asset.image_height %>
+
+ (small | large | full)
+
+
+
+
+ <%= tag.img src: @media_asset.variant("original").file_url, title: "Preview", id: "image", class: "fit-width fit-height", "data-shortcut": "z" %>
+
+
+ <% if @upload.source.present? %>
+ <% @source = ::Sources::Strategies.find(@upload.source, @upload.referer_url) %>
+ <%= render "uploads/related_posts", source: @source %>
+ <%= render_source_data(@source) %>
+ <% end %>
+
+ <%= edit_form_for(@post, html: { id: "form" }) do |f| %>
+ <%= f.input :upload_media_asset_id, as: :hidden, input_html: { value: @upload_media_asset.id } %>
+
+ <%= f.input :source, as: :string %>
+ <%= f.input :rating, collection: [["Explicit", "e"], ["Questionable", "q"], ["Safe", "s"]], as: :radio_buttons, selected: @post.rating %>
+ <%= f.input :parent_id, label: "Parent ID", as: :string, input_html: { value: @post.parent_id } %>
+
+
+
+
+
+
+
+
+ <%= f.input :tag_string, label: false, hint: "Ctrl+Enter to submit", input_html: { "data-autocomplete": "tag-edit", "data-shortcut": "e", value: @post.tag_string } %>
+ <%= render "related_tags/buttons" %>
+
+
+ <%= f.submit "Upload", id: "submit-button", data: { disable_with: false } %>
+
+ <% if CurrentUser.can_upload_free? %>
+ <%= f.input :is_pending, as: :boolean, label: "Upload for approval", wrapper_html: { class: "inline-block" }, input_html: { checked: @post.is_pending? } %>
+ <% end %>
+
+ <%= render "related_tags/container" %>
+ <% end %>
+ <% end %>
<%= render "uploads/secondary_links" %>
-
-<% if @upload.is_pending? || @upload.is_processing? || @upload.is_preprocessing? || @upload.is_preprocessed? %>
- <% content_for(:html_header) do %>
-
- <% end %>
-<% end %>
diff --git a/app/views/uploads/update.js.erb b/app/views/uploads/update.js.erb
deleted file mode 100644
index 345366b9b..000000000
--- a/app/views/uploads/update.js.erb
+++ /dev/null
@@ -1 +0,0 @@
-location.reload();
diff --git a/config/locales/en.yml b/config/locales/en.yml
index bf885fde2..2fd5b0a30 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -40,6 +40,8 @@ en:
user_id: "You"
forum_post_vote:
creator_id: "Your vote"
+ media_asset:
+ file_ext: ""
moderation_report:
creator: "You"
post:
@@ -48,6 +50,7 @@ en:
updater_id: "You"
uploader: "You"
uploader_id: "You"
+ md5: ""
post_flag:
creator: "You"
creator_id: "You"
@@ -72,6 +75,8 @@ en:
user: "You"
user_id: "You"
errors:
+ messages:
+ record_invalid: "%{errors}"
models:
tag_implication:
attributes:
diff --git a/config/routes.rb b/config/routes.rb
index 0f19afcd5..1c3e0da8c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,7 +8,7 @@
# @see https://guides.rubyonrails.org/routing.html
# @see http://localhost:3000/rails/info/routes
Rails.application.routes.draw do
- resources :posts, only: [:index, :show, :update, :destroy] do
+ resources :posts, only: [:index, :show, :update, :destroy, :new, :create] do
get :random, on: :collection
end
diff --git a/package.json b/package.json
index 13e51d37d..1e5c6ce2a 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,6 @@
"ruffle-mirror": "^2022.1.13",
"sass": "^1.48.0",
"sass-loader": "^12.4.0",
- "spark-md5": "^3.0.2",
"tippy.js": "^6.3.7",
"typopro-web": "^4.2.6",
"webpack": "^5.66.0",
diff --git a/test/functional/uploads_controller_test.rb b/test/functional/uploads_controller_test.rb
index 1284c472b..84dd4eaa0 100644
--- a/test/functional/uploads_controller_test.rb
+++ b/test/functional/uploads_controller_test.rb
@@ -42,16 +42,6 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
end
end
- context "preprocess action" do
- should "prefer the file over the source when preprocessing" do
- file = Rack::Test::UploadedFile.new("#{Rails.root}/test/files/test.jpg", "image/jpeg")
- post_auth preprocess_uploads_path, @user, params: {:upload => {:source => "https://cdn.donmai.us/original/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg", :file => file}}
- assert_response :success
- perform_enqueued_jobs
- assert_equal("ecef68c44edb8a0d6a3070b5f8e8ee76", Upload.last.md5)
- end
- end
-
context "new action" do
should "render" do
get_auth new_upload_path, @user
@@ -66,17 +56,6 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
end
-
- should "prefer the file" do
- get_auth new_upload_path, @user, params: {url: "https://cdn.donmai.us/original/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg"}
- perform_enqueued_jobs
- file = Rack::Test::UploadedFile.new("#{Rails.root}/test/files/test.jpg", "image/jpeg")
- assert_difference(-> { Post.count }) do
- post_auth uploads_path, @user, params: {upload: {file: file, tag_string: "aaa", rating: "q", source: "https://cdn.donmai.us/original/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg"}}
- end
- post = Post.last
- assert_equal("ecef68c44edb8a0d6a3070b5f8e8ee76", post.md5)
- end
end
context "for a direct link twitter post" do
diff --git a/test/unit/sources/moebooru_test.rb b/test/unit/sources/moebooru_test.rb
index 868375295..cffb05f52 100644
--- a/test/unit/sources/moebooru_test.rb
+++ b/test/unit/sources/moebooru_test.rb
@@ -14,7 +14,6 @@ module Sources
assert_equal(page_url, site.page_url) if page_url.present?
assert_equal(tags.sort, site.tags.map(&:first).sort)
assert_equal(profile_url.to_s, site.profile_url.to_s)
- assert_equal(size, site.remote_size)
assert_nothing_raised { site.to_h }
end
diff --git a/test/unit/sources/pixiv_test.rb b/test/unit/sources/pixiv_test.rb
index f49cb7a69..017ad8060 100644
--- a/test/unit/sources/pixiv_test.rb
+++ b/test/unit/sources/pixiv_test.rb
@@ -99,10 +99,6 @@ module Sources
assert_equal("uroobnad", @site.artist_name)
end
- should "get the remote image size" do
- assert_equal(863_758, @site.remote_size)
- end
-
should "get the full size image url" do
assert_equal("https://i.pximg.net/img-original/img/2017/11/21/05/12/37/65981735_p0.jpg", @site.image_url)
end
diff --git a/yarn.lock b/yarn.lock
index 7fb82c02f..992cc9fcc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6983,7 +6983,6 @@ fsevents@~2.3.2:
ruffle-mirror: ^2022.1.13
sass: ^1.48.0
sass-loader: ^12.4.0
- spark-md5: ^3.0.2
stylelint: ^14.2.0
stylelint-config-standard: ^24.0.0
stylelint-config-standard-scss: ^3.0.0
@@ -7416,13 +7415,6 @@ fsevents@~2.3.2:
languageName: node
linkType: hard
-"spark-md5@npm:^3.0.2":
- version: 3.0.2
- resolution: "spark-md5@npm:3.0.2"
- checksum: f36020b068e82800b240a4aed779a11e4a31ff21a0da14a75ca37ec856b6238e5c397c07d4ce99cd23a2113fea2855cf177b024ae2612cb47003e786768680b0
- languageName: node
- linkType: hard
-
"spdx-correct@npm:^3.0.0":
version: 3.1.1
resolution: "spdx-correct@npm:3.1.1"