Files
peptiderepo-r2-manager/assets/r2-manager.js
peptiderepo 3cb8cd7c7d feat: initial release v1.1.0
Cloudflare R2 media offload manager for peptiderepo.com.
Built on WP Offload Media Lite. Supersedes the one-off migration script
and mu-plugin peptiderepo-r2-endpoint.php.

Features:
- Admin page under Peptide Repo > R2 Media Manager (shared umbrella menu)
- ensure_umbrella_menu() guard — safe for multi-plugin umbrella sharing
- Batch AJAX migration to R2 with pause/resume and progress bar
- Remove-local-copies batch with physical disk presence check
- Activity log table (wp_pr_r2_log) with self-healing on plugins_loaded
- Test R2 connection button
- R2 endpoint injection via as3cf_aws_s3_client_args filter
2026-04-25 08:48:01 +00:00

190 lines
6.8 KiB
JavaScript
Executable File

/* global jQuery, prR2 */
/**
* R2 Media Manager — admin JS.
*
* Drives batch loops for migrate-to-R2 and remove-local-files operations.
* Each loop fires one AJAX request, waits for the response, updates the
* progress bar, then fires the next request — until the server signals done=true
* or the user clicks Pause.
*
* Also handles: Test R2 Connection button, Clear Log button.
*/
(function ($) {
'use strict';
// ── Batch runner ──────────────────────────────────────────────────────────
/**
* Run one batch of an AJAX operation and schedule the next.
*
* @param {string} action WP AJAX action name.
* @param {string} nonce Nonce string from wp_localize_script.
* @param {Object} state Mutable state: { processed, succeeded, failed, paused }.
* @param {Object} ui jQuery element refs: $btn, $pauseBtn, $bar, $status.
* @param {string} countKey 'succeeded' | 'removed' — key to read from response.
* @param {string} failKey 'failed' | 'skipped'.
*/
function runBatch(action, nonce, state, ui, countKey, failKey) {
if (state.paused) {
ui.$status.text('Paused. Click the button again to resume.');
ui.$btn.prop('disabled', false).text(ui.$btn.data('resume-label'));
ui.$pauseBtn.hide();
return;
}
$.post(prR2.ajaxUrl, {
action: action,
nonce: nonce,
batch_size: prR2.batchSize,
})
.done(function (res) {
if (!res.success) {
showNotice('error', 'Error: ' + (res.data ? res.data.message : 'Unknown error.'));
ui.$btn.prop('disabled', false);
ui.$pauseBtn.hide();
return;
}
var d = res.data;
state.processed += d.processed;
state.succeeded += (d[countKey] || 0);
state.failed += (d[failKey] || 0);
// remaining is the count left AFTER this batch.
var total = state.processed + d.remaining;
var pct = total > 0 ? Math.round((state.processed / total) * 100) : 100;
ui.$bar.val(pct);
ui.$status.text(
'Processed ' + state.processed + ' of ' + total +
' — success: ' + state.succeeded +
(state.failed ? ', failed/skipped: ' + state.failed : '')
);
if (d.done) {
ui.$status.text(
'Done! ✓ Succeeded: ' + state.succeeded +
(state.failed ? ' Failed/skipped: ' + state.failed : '')
);
ui.$btn.prop('disabled', true);
ui.$pauseBtn.hide();
// Reload after 1.5 s so the status table refreshes.
setTimeout(function () { window.location.reload(); }, 1500);
return;
}
runBatch(action, nonce, state, ui, countKey, failKey);
})
.fail(function () {
ui.$status.text('Network error — operation paused. Retry by clicking the button.');
ui.$btn.prop('disabled', false).text(ui.$btn.data('resume-label'));
ui.$pauseBtn.hide();
});
}
// ── Wire up Migrate button ────────────────────────────────────────────────
$('#pr-r2-btn-migrate').on('click', function () {
var $btn = $(this);
var state = { processed: 0, succeeded: 0, failed: 0, paused: false };
var ui = {
$btn: $btn,
$pauseBtn: $('#pr-r2-btn-migrate-pause'),
$bar: $('#pr-r2-migrate-bar'),
$status: $('#pr-r2-migrate-status'),
};
$btn.data('resume-label', 'Resume migration').prop('disabled', true);
ui.$pauseBtn.show();
$('#pr-r2-migrate-progress').show();
ui.$bar.val(0);
ui.$status.text('Starting migration…');
runBatch('pr_r2_migrate_batch', prR2.nonceMigrate, state, ui, 'succeeded', 'failed');
});
$('#pr-r2-btn-migrate-pause').on('click', function () {
// Signal the batch runner to stop after the current in-flight request.
var state = $('#pr-r2-btn-migrate').data('batchState');
if (state) { state.paused = true; }
});
// Re-wire so the button can resume (state persists in data).
// Simpler: just reload the page — already-offloaded items are excluded by the query.
// ── Wire up Remove Local button ───────────────────────────────────────────
$('#pr-r2-btn-remove').on('click', function () {
var $btn = $(this);
var state = { processed: 0, succeeded: 0, failed: 0, paused: false };
var ui = {
$btn: $btn,
$pauseBtn: $('#pr-r2-btn-remove-pause'),
$bar: $('#pr-r2-remove-bar'),
$status: $('#pr-r2-remove-status'),
};
$btn.data('resume-label', 'Resume removal').prop('disabled', true);
ui.$pauseBtn.show();
$('#pr-r2-remove-progress').show();
ui.$bar.val(0);
ui.$status.text('Starting local-file removal…');
runBatch('pr_r2_remove_local_batch', prR2.nonceRemove, state, ui, 'removed', 'skipped');
});
$('#pr-r2-btn-remove-pause').on('click', function () {
var state = $('#pr-r2-btn-remove').data('batchState');
if (state) { state.paused = true; }
});
// ── Test Connection ───────────────────────────────────────────────────────
$('#pr-r2-btn-test').on('click', function () {
var $btn = $(this);
var $result = $('#pr-r2-test-result');
$btn.prop('disabled', true);
$result.css('color', '#555').text('Testing…');
$.post(prR2.ajaxUrl, { action: 'pr_r2_test_connection', nonce: prR2.nonceTest })
.done(function (res) {
if (res.success) {
$result.css('color', '#090').text('✓ ' + res.data.message);
} else {
$result.css('color', '#c00').text('✗ ' + (res.data ? res.data.message : 'Failed.'));
}
})
.fail(function () {
$result.css('color', '#c00').text('✗ Network error.');
})
.always(function () { $btn.prop('disabled', false); });
});
// ── Clear Log ─────────────────────────────────────────────────────────────
$('#pr-r2-btn-clear-log').on('click', function () {
if (!window.confirm('Clear all log entries?')) { return; }
var $btn = $(this);
$btn.prop('disabled', true);
$.post(prR2.ajaxUrl, { action: 'pr_r2_clear_log', nonce: prR2.nonceClearLog })
.done(function (res) {
if (res.success) {
$('#pr-r2-log-tbody').closest('table').replaceWith('<p>Log cleared.</p>');
} else {
showNotice('error', 'Could not clear log.');
}
})
.always(function () { $btn.prop('disabled', false); });
});
// ── Utility ───────────────────────────────────────────────────────────────
function showNotice(type, msg) {
var cls = type === 'error' ? 'notice-error' : 'notice-success';
$('#pr-r2-notice').html(
'<div class="notice ' + cls + ' is-dismissible"><p>' + msg + '</p></div>'
);
}
}(jQuery));