@@ -1201,8 +1233,18 @@
-
-
If the target bucket does not exist, it will be created automatically.
+
+
+
+
If the target bucket does not exist, it will be created automatically.
+
+
+ Rules: 3-63 characters, lowercase letters, numbers, hyphens, and periods only. Must start/end with letter or number.
+
+
@@ -2043,12 +2085,64 @@
updateLoadMoreButton();
}
+ // Track the count of items in current folder before re-rendering
+ let prevFolderItemCount = 0;
+ if (currentPrefix && append) {
+ const prevState = getFoldersAtPrefix(currentPrefix);
+ prevFolderItemCount = prevState.folders.length + prevState.files.length;
+ }
+
if (typeof initFolderNavigation === 'function') {
initFolderNavigation();
}
attachRowHandlers();
+ // If we're in a nested folder and loaded more objects, scroll to show newly loaded content
+ if (currentPrefix && append) {
+ const newState = getFoldersAtPrefix(currentPrefix);
+ const newFolderItemCount = newState.folders.length + newState.files.length;
+ const addedCount = newFolderItemCount - prevFolderItemCount;
+
+ if (addedCount > 0) {
+ // Show a brief notification about newly loaded items in the current folder
+ const folderViewStatusEl = document.getElementById('folder-view-status');
+ if (folderViewStatusEl) {
+ folderViewStatusEl.innerHTML = `+${addedCount} new item${addedCount !== 1 ? 's' : ''} loaded in this folder`;
+ folderViewStatusEl.classList.remove('d-none');
+ // Reset to normal status after 3 seconds
+ setTimeout(() => {
+ if (typeof updateFolderViewStatus === 'function') {
+ updateFolderViewStatus();
+ }
+ }, 3000);
+ }
+
+ // Scroll to show the first newly added item
+ const allRows = objectsTableBody.querySelectorAll('tr:not([style*="display: none"])');
+ if (allRows.length > prevFolderItemCount) {
+ const firstNewRow = allRows[prevFolderItemCount];
+ if (firstNewRow) {
+ firstNewRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ // Briefly highlight the new rows
+ for (let i = prevFolderItemCount; i < allRows.length; i++) {
+ allRows[i].classList.add('table-info');
+ setTimeout(() => {
+ allRows[i].classList.remove('table-info');
+ }, 2000);
+ }
+ }
+ }
+ } else if (hasMoreObjects) {
+ // Objects were loaded but none were in the current folder - show a hint
+ const folderViewStatusEl = document.getElementById('folder-view-status');
+ if (folderViewStatusEl) {
+ folderViewStatusEl.innerHTML = `Loaded more objects (not in this folder). to see all.`;
+ folderViewStatusEl.classList.remove('d-none');
+ }
+ }
+ }
+
} catch (error) {
console.error('Failed to load objects:', error);
if (!append) {
@@ -3755,5 +3849,106 @@
}
}
});
+
+ // Bucket name validation for replication setup
+ const targetBucketInput = document.getElementById('target_bucket');
+ const targetBucketFeedback = document.getElementById('target_bucket_feedback');
+
+ const validateBucketName = (name) => {
+ if (!name) return { valid: false, error: 'Bucket name is required' };
+ if (name.length < 3) return { valid: false, error: 'Bucket name must be at least 3 characters' };
+ if (name.length > 63) return { valid: false, error: 'Bucket name must be 63 characters or less' };
+ if (!/^[a-z0-9]/.test(name)) return { valid: false, error: 'Bucket name must start with a lowercase letter or number' };
+ if (!/[a-z0-9]$/.test(name)) return { valid: false, error: 'Bucket name must end with a lowercase letter or number' };
+ if (/[A-Z]/.test(name)) return { valid: false, error: 'Bucket name must not contain uppercase letters' };
+ if (/_/.test(name)) return { valid: false, error: 'Bucket name must not contain underscores' };
+ if (/\.\.|--/.test(name)) return { valid: false, error: 'Bucket name must not contain consecutive periods or hyphens' };
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return { valid: false, error: 'Bucket name must not be formatted as an IP address' };
+ if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(name) && name.length > 2) return { valid: false, error: 'Bucket name contains invalid characters. Use only lowercase letters, numbers, hyphens, and periods.' };
+ return { valid: true, error: null };
+ };
+
+ const updateBucketNameValidation = () => {
+ if (!targetBucketInput || !targetBucketFeedback) return;
+ const name = targetBucketInput.value.trim();
+ if (!name) {
+ targetBucketInput.classList.remove('is-valid', 'is-invalid');
+ targetBucketFeedback.textContent = '';
+ return;
+ }
+ const result = validateBucketName(name);
+ targetBucketInput.classList.toggle('is-valid', result.valid);
+ targetBucketInput.classList.toggle('is-invalid', !result.valid);
+ targetBucketFeedback.textContent = result.error || '';
+ };
+
+ targetBucketInput?.addEventListener('input', updateBucketNameValidation);
+ targetBucketInput?.addEventListener('blur', updateBucketNameValidation);
+
+ // Prevent form submission if bucket name is invalid
+ const replicationForm = targetBucketInput?.closest('form');
+ replicationForm?.addEventListener('submit', (e) => {
+ const name = targetBucketInput.value.trim();
+ const result = validateBucketName(name);
+ if (!result.valid) {
+ e.preventDefault();
+ updateBucketNameValidation();
+ targetBucketInput.focus();
+ return false;
+ }
+ });
+
+ // Policy JSON validation and formatting
+ const formatPolicyBtn = document.getElementById('formatPolicyBtn');
+ const policyValidationStatus = document.getElementById('policyValidationStatus');
+ const policyValidBadge = document.getElementById('policyValidBadge');
+ const policyInvalidBadge = document.getElementById('policyInvalidBadge');
+ const policyErrorDetail = document.getElementById('policyErrorDetail');
+
+ const validatePolicyJson = () => {
+ if (!policyTextarea || !policyValidationStatus) return;
+ const value = policyTextarea.value.trim();
+ if (!value) {
+ policyValidationStatus.classList.add('d-none');
+ policyErrorDetail?.classList.add('d-none');
+ return;
+ }
+ policyValidationStatus.classList.remove('d-none');
+ try {
+ JSON.parse(value);
+ policyValidBadge?.classList.remove('d-none');
+ policyInvalidBadge?.classList.add('d-none');
+ policyErrorDetail?.classList.add('d-none');
+ } catch (err) {
+ policyValidBadge?.classList.add('d-none');
+ policyInvalidBadge?.classList.remove('d-none');
+ if (policyErrorDetail) {
+ policyErrorDetail.textContent = err.message;
+ policyErrorDetail.classList.remove('d-none');
+ }
+ }
+ };
+
+ policyTextarea?.addEventListener('input', validatePolicyJson);
+ policyTextarea?.addEventListener('blur', validatePolicyJson);
+
+ formatPolicyBtn?.addEventListener('click', () => {
+ if (!policyTextarea) return;
+ const value = policyTextarea.value.trim();
+ if (!value) return;
+ try {
+ const parsed = JSON.parse(value);
+ policyTextarea.value = JSON.stringify(parsed, null, 2);
+ validatePolicyJson();
+ } catch (err) {
+ // Show error in validation
+ validatePolicyJson();
+ }
+ });
+
+ // Initialize policy validation on page load
+ if (policyTextarea && policyPreset?.value === 'custom') {
+ validatePolicyJson();
+ }
{% endblock %}