MediaWiki:Gadget-UploadMultipleFiles.js
MediaWiki interface page
More actions
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* uploadMultipleFiles v2 by westgrass */
/* v2.1: support multiple categories, by Mourten */
/* Improved from https://dev.fandom.com/wiki/UploadMultipleFiles */
/* Adapted for Deepspace Lore */
$(function () {
class apiQueue {
constructor({ api, concurrency=3, rateLimitDelay = 60, maxRetries = 5, handlers = {} }) {
this.api = api;
this.concurrency = concurrency;
this.maxRetries = maxRetries;
this.rateLimitDelay = rateLimitDelay;
this.rateLimitUntil = 0;
this.rateLimitPromise = null;
this.handlers = handlers;
this.queue = [];
this.runningWorkers = 0;
}
add(job) {
if (!job) return;
this.handlers.onAdd && this.handlers.onAdd(job);
this.queue.push({job, retries: 0});
this._ensureWorkers();
}
_ensureWorkers() {
while (this.runningWorkers < this.concurrency && this.queue.length > 0) {
this._worker();
}
}
async _worker() {
this.runningWorkers++;
try {
while (true) {
const item = this.queue.shift();
if (!item) break;
await this._work(item);
}
} finally {
this.runningWorkers--;
this._ensureWorkers();
if (this.runningWorkers === 0 && this.queue.length === 0) {
this.handlers.onDone && this.handlers.onDone();
}
}
}
async _work(item) {
const job = item.job;
this.handlers.onStart && this.handlers.onStart(job);
while (true) {
await this._waitForGlobalRateLimit();
try {
const result = this.handlers.onWork && await this.handlers.onWork(job, this.api);
this.handlers.onSuccess && this.handlers.onSuccess(job, result);
return;
} catch (e) {
if (e.type === 'retriable') {
item.retries++;
if (item.retries > this.maxRetries) {
this.handlers.onFail && this.handlers.onFail(job, e);
return;
}
this._triggerGlobalRateLimit(this.rateLimitDelay);
continue;
}
this.handlers.onFail && this.handlers.onFail(job, e);
return;
}
}
}
async _waitForGlobalRateLimit() {
if (!this.rateLimitPromise) return;
await this.rateLimitPromise;
}
_triggerGlobalRateLimit(seconds) {
const now = Date.now();
const until = now + seconds * 1000;
if (this.rateLimitUntil >= until) {
return;
}
this.rateLimitUntil = until;
this.rateLimitPromise = (async () => {
let remaining;
while ((remaining = this.rateLimitUntil - Date.now()) >= 0) {
const sec = Math.ceil(remaining / 1000);
this.handlers.onProgress && this.handlers.onProgress(sec);
await new Promise(r => setTimeout(r, 1000));
}
})().finally(() => {
this.rateLimitPromise = null;
this.rateLimitUntil = 0;
});
}
}
///////////////////////////////////////////////////////////////////////////////
if (mw.config.get("wgCanonicalSpecialPageName") !== "Upload") {
return;
}
if (window.__wgg_UploadMultipleFiles_IsLoaded) {
return;
}
window.__wgg_UploadMultipleFiles_IsLoaded = true;
mw.messages.set({
'gadget-uploadMultipleFiles-multiupload': 'Multiple upload',
'gadget-uploadMultipleFiles-yes': 'Yes',
'gadget-uploadMultipleFiles-no': 'No',
'gadget-uploadMultipleFiles-sourcefiles': 'Source files',
'gadget-uploadMultipleFiles-categoryname': 'Category',
'gadget-uploadMultipleFiles-categorynamehint': 'Enter one category name per line',
'gadget-uploadMultipleFiles-categoryname-placeholder': 'One category per line',
'gadget-uploadMultipleFiles-watchFiles': 'Watch these files',
'gadget-uploadMultipleFiles-uploadfiles': 'Upload files',
'gadget-uploadMultipleFiles-nofiles': 'No files selected.',
'gadget-uploadMultipleFiles-nolicense': 'No license selected.',
'gadget-uploadMultipleFiles-license': 'Licensing',
'gadget-uploadMultipleFiles-uploading': 'Uploading\u2026',
'gadget-uploadMultipleFiles-uploaded': 'Uploaded',
'gadget-uploadMultipleFiles-uploaded-placeholder': 'No files uploaded yet.',
'gadget-uploadMultipleFiles-failed': 'Failed',
'gadget-uploadMultipleFiles-failed-placeholder': 'No failed uploads.',
'gadget-uploadMultipleFiles-warning': 'Warnings',
'gadget-uploadMultipleFiles-warning-placeholder': 'No warnings.',
'gadget-uploadMultipleFiles-status-uploading': 'Uploading\u2026',
'gadget-uploadMultipleFiles-done': 'Done.',
'gadget-uploadMultipleFiles-done-with-error': 'Done, with some errors or warnings.',
'gadget-uploadMultipleFiles-ratelimited': 'Rate limited. Resuming in $1 seconds\u2026',
'gadget-uploadMultipleFiles-error': 'Upload failed.',
'gadget-uploadMultipleFiles-error-fileexists-no-change': '$1 is identical to the current version and was not uploaded.',
'gadget-uploadMultipleFiles-warning-exists': '$1 already exists.',
'gadget-uploadMultipleFiles-warning-deleted': '$1 was previously deleted.',
'gadget-uploadMultipleFiles-warning-duplicate': 'Duplicate of $1.',
'gadget-uploadMultipleFiles-warning-duplicate-deleted': 'Duplicate of previously deleted file $1.',
'gadget-uploadMultipleFiles-warning-exists-normalized': '$1 already exists (normalized name).',
'gadget-uploadMultipleFiles-warning-page-exists': 'Page $1 already exists.',
'gadget-uploadMultipleFiles-retry': 'Retry',
'gadget-uploadMultipleFiles-ignore': 'Upload anyway',
});
$.when(
mw.loader.using(['site', 'mediawiki.util', 'mediawiki.api', 'mediawiki.jqueryMsg']), $.ready
).then(function () {
enhanceUploadUI();
$("#multiFileSubmit").on('click', function () {
const files = $("#multiupload")[0].files;
// Cancel upload if no files are selected
if (files.length === 0) {
alert(mw.msg("gadget-uploadMultipleFiles-nofiles"));
return false;
}
// get form options
const description = getUploadDescription(); // Description is summary + license + category
const watch = $("#wpWatchthis").is(":checked") ? "watch" : "nochange";
const ignoreWarnings = $("#wpIgnoreWarning").is(":checked");
switchToUploadingUI();
if (ignoreWarnings) {
$('#multiUploadWarning').hide();
}
const $qu = $('#multiUploadQueue ul');
const $done = $('#multiUploadUploaded ul');
const $donePh = $('#multiUploadUploaded p');
const $fail = $('#multiUploadFailed ul');
const $failPh = $('#multiUploadFailed p');
const $warn = $('#multiUploadWarning ul');
const $warnPh = $('#multiUploadWarning p');
const $message = $('#multiUploadMessage');
const $result = $("#firstHeading .result");
//prepare upload queue
const queue = new apiQueue({api: new mw.Api(), handlers: {
onAdd(job) {
$result.empty();
let $li = $('<li data-idx="'+job.idx+'">'+job.file.name+' <span class="status"></span></li>');
job.node = $li;
$qu.append($li);
},
onDone() {
if ($fail.find('li').length || $warn.find('li').length) {
$result.msg('gadget-uploadMultipleFiles-done-with-error');
} else {
$result.msg('gadget-uploadMultipleFiles-done');
}
},
onStart(job) {
job.node.find('.status').msg('gadget-uploadMultipleFiles-status-uploading');
},
onSuccess(job, result) {
job.node.remove();
$donePh.remove();
$done.append('<li><a href="' + result.imageinfo.descriptionurl + '" target="_blank">' + result.filename + '</a></li>');
},
onFail(job, e) {
job.node.remove();
if (e.type === 'warning'){
const warnings = e.result.upload.warnings;
if(warnings["exists"]){
warningOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-warning-exists', job.file.name));
}
else if(warnings["duplicate"]){
warningOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-warning-duplicate', warnings.duplicate));
}
else if(warnings["was-deleted"]){
warningOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-warning-deleted', warnings["was-deleted"]));
}
else if(warnings["duplicate-archive"]){
warningOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-warning-duplicate-deleted', warnings["duplicate-archive"]));
}
else if(warnings["exists-normalized"]){
warningOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-warning-exists-normalized', warnings["exists-normalized"]));
}
else if(warnings["page-exists"]){
warningOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-warning-page-exists', warnings["page-exists"]));
}else{
// generic fallback
warningOutput(job.idx, JSON.stringify(warnings));
}
}
else{ //fail
// specific catch for some types
if(e.code === "fileexists-no-change"){
failedOutput(job.idx, mwMsg('gadget-uploadMultipleFiles-error-fileexists-no-change', job.file.name));
}else{
// generic error fallback
if (e.result && e.result.error) {
failedOutput(job.idx, e.result.error.info + ' (' + e.result.error.code +')', true);
}
else{
failedOutput(job.idx, mw.message('gadget-uploadMultipleFiles-error').parse(), true);
}
}
}
},
onProgress(sec) {
$message.show();
$('#rateLimitCounter').text(sec);
if(sec <= 0){
$message.hide();
}
},
async onWork(job, api) {
const params = {
format: 'json',
filename: job.file.name,
text: job.description,
watchlist: job.watch
};
if (job.ignoreWarnings) {
params.ignorewarnings = 1;
}
return new Promise((resolve, reject) => {
api.upload(job.file, params)
.done((result) => resolve(result.upload))
.fail((code, result) => {
if (result.upload && result.upload.warnings) { // warning
if (result.upload.result === "Success"){ //upload with "ignoreWarnings=1"
resolve(result.upload); //success
}
else if (result.upload.warnings.exists && result.upload.warnings.nochange) {
//an exact duplicate of the current version, treat as failure
reject({ code:'fileexists-no-change', result, type:'fail' });
}
else{
reject({ code, result, type:'warning' });
}
}
else if (code === "ratelimited") { // specific catch for ratelimiting
reject({ code, result, type:'retriable' });
}
else{
reject({ code, result, type:'fail' });
}
});
});
}
}});
const jobMap = new Map();
$fail.on('click', 'a.retry', function(event){
const $li = $(event.target).parents('li');
const idx = $li.data('idx');
const job = jobMap.get(idx);
$li.remove();
if($fail.find('li').length === 0){
$failPh.show();
}
queue.add(job);
return false;
});
$warn.on('click', 'a.retry', function(event){
const $li = $(event.target).parents('li');
const idx = $li.data('idx');
const job = jobMap.get(idx);
job.ignoreWarnings = true;
$li.remove();
if($warn.find('li').length === 0){
$warnPh.show();
}
queue.add(job);
return false;
});
Array.from(files).forEach((file, idx) => {
const job = {
idx,
file,
description,
watch,
ignoreWarnings
};
jobMap.set(idx, job);
queue.add(job);
});
////////////////////////
function failedOutput (fileIdx, msg, retry = false) {
$failPh.hide();
var $node = $('<li data-idx="'+fileIdx+'"></li>').append(files[fileIdx].name + ': ', msg);
if(retry){
$node.append(' ', $('<a class="retry" href="#">'+mw.msg('gadget-uploadMultipleFiles-retry')+'</a>'));
}
$fail.append($node);
}
function warningOutput (fileIdx, msg) {
$warnPh.hide();
var $node = $('<li data-idx="'+fileIdx+'"></li>').append(files[fileIdx].name + ': ', msg);
$node.append(' ', $('<a class="retry ignore" href="#">'+mw.msg('gadget-uploadMultipleFiles-ignore')+'</a>'));
$warn.append($node);
}
});
});
///////////////////////////////////////////////////////////////////////////////
// functions
///////////////////////////////////////////////////////////////////////////////
function getUploadDescription () {
var sections = [];
var categories = [];
var summary = $("#wpUploadDescription").val();
var licenseDisplayName = $("#wpLicense option:selected").val();
var categoryName = $("#multiFileCategory").val();
if(summary !== ""){
sections.push(summary);
}
if(licenseDisplayName !== ""){
var licenseTemplateText = $("#wpLicense option:selected").prop("title");
sections.push("== " + mw.message("gadget-uploadMultipleFiles-license").parse() + " ==\n" + licenseTemplateText);
}
if (categoryName !== "") {
categoryName
.split(/\n+/)
.map(c => c.trim())
.filter(Boolean)
.forEach(cat => {
categories.push("[[" + "Category:" + cat + "]]");
});
}
// categories go at the end, joined WITHOUT blank lines
if (categories.length) {
sections.push(categories.join("\n"));
}
return sections.join("\n\n");
}
function mwMsg (key, filename) {
const getFileLink = function(filename){
return '<a href="'+mw.util.getUrl('File:'+filename)+'" target="_blank">' + filename + '</a>';
};
if(Array.isArray(filename)){
return mw.message(key, $(filename.map((x)=>getFileLink(x)).join(', '))).parse();
}
else{
return mw.message(key, $(getFileLink(filename))).parse();
}
}
function enhanceUploadUI(){
$("#wpUploadFile").parents('.mw-htmlform-field-UploadSourceField').addClass("regularFileSelect") // source file input
.before( // multiple? yes/no
$("<tr></tr>").append(
$('<td class="mw-label"></td>').msg("gadget-uploadMultipleFiles-multiupload"),
$('<td class="mw-input"></td>').append(
$("<label></label>").msg("gadget-uploadMultipleFiles-yes").prepend('<input type="radio" name="multipleFiles" value="yes" /> '),
' ',
$("<label></label>").msg("gadget-uploadMultipleFiles-no").prepend('<input type="radio" name="multipleFiles" value="no" checked/> '),
)
)
)
.after( //source files
$('<tr class="mw-htmlform-field-UploadSourceField multipleFileSelect"></tr>').hide().append(
$('<td class="mw-label"></td>').msg("gadget-uploadMultipleFiles-sourcefiles"),
$('<td class="mw-input"></td>').append('<input type="file" id="multiupload" size="60" multiple />')
)
);
$("#wpDestFile").parents(".mw-htmlform-field-HTMLTextField").addClass("regularFileSelect") //dest filename
.after(// add "category name" textbox to multiupload section
$('<tr class="mw-htmlform-field-HTMLTextField multipleFileSelect"></tr>').hide().append(
$('<td class="mw-label"></td>').append(
$('<label for="multiFileCategory"></label>').append(
$('<abbr></abbr>').msg("gadget-uploadMultipleFiles-categoryname").attr('title', mw.msg("gadget-uploadMultipleFiles-categorynamehint"))
)
),
$('<td class="mw-input"></td>').append('<textarea id="multiFileCategory" name="multiFileCategory" rows="5" placeholder="'+mw.msg("gadget-uploadMultipleFiles-categoryname-placeholder")+'"></textarea>')
)
);
$("label[for=wpWatchthis]").addClass("regularFileSelect") //watch this file
.after(
$('<label for="wpWatchthis" class="multipleFileSelect"></label>').msg("gadget-uploadMultipleFiles-watchFiles").hide() //watch these files
);
$("input[name=wpUpload]").addClass("regularFileSelect") //upload file button
.after(
$('<input type="button" id="multiFileSubmit" class="multipleFileSelect" />').val(mw.msg("gadget-uploadMultipleFiles-uploadfiles")).hide() //upload files
);
$("input[name=multipleFiles]").on('change', function(){
if(this.value === 'yes'){
// also hide thumbnail and warning in regular mode.
$(".regularFileSelect, #mw-upload-thumbnail, .mw-destfile-warning").hide();
$(".multipleFileSelect").show();
}else{
$(".regularFileSelect, #mw-upload-thumbnail, .mw-destfile-warning").show();
$(".multipleFileSelect").hide();
}
});
}
function switchToUploadingUI(){
// init uploading UI
$("#firstHeading").msg("gadget-uploadMultipleFiles-uploading").append('<span class="result"></span>');
$("#mw-content-text").empty().append(
$('<div id="multiUploadMessage"></div>').msg('gadget-uploadMultipleFiles-ratelimited', $('<span id="rateLimitCounter">60</span>')).hide(),
$('<div id="multiUploadQueue"></div>').append($('<ul></ul>')),
$('<div id="multiUploadResult"></div>').append(
$('<div id="multiUploadFailed"></div>').append(
$('<h3></h3>').msg("gadget-uploadMultipleFiles-failed"),
$('<p></p>').msg("gadget-uploadMultipleFiles-failed-placeholder"),
$('<ul></ul>')
),
$('<div id="multiUploadWarning"></div>').append(
$('<h3></h3>').msg("gadget-uploadMultipleFiles-warning"),
$('<p></p>').msg("gadget-uploadMultipleFiles-warning-placeholder"),
$('<ul></ul>')
),
$('<div id="multiUploadUploaded"></div>').append(
$('<h3></h3>').msg("gadget-uploadMultipleFiles-uploaded"),
$('<p></p>').msg("gadget-uploadMultipleFiles-uploaded-placeholder"),
$('<ul></ul>')
)
)
);
}
});