|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>CloudSync - S3 File Uploader</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.dropzone { |
|
border: 2px dashed #3b82f6; |
|
transition: all 0.3s ease; |
|
} |
|
.dropzone.active { |
|
border-color: #10b981; |
|
background-color: #f0f9ff; |
|
} |
|
.progress-bar { |
|
transition: width 0.3s ease; |
|
} |
|
.file-item:hover { |
|
background-color: #f8fafc; |
|
} |
|
.rotate { |
|
animation: spin 1s linear infinite; |
|
} |
|
@keyframes spin { |
|
from { transform: rotate(0deg); } |
|
to { transform: rotate(360deg); } |
|
} |
|
#folderInput, #fileInput { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8 max-w-4xl"> |
|
|
|
<header class="mb-8 text-center"> |
|
<h1 class="text-4xl font-bold text-blue-600 mb-2">CloudSync</h1> |
|
<p class="text-gray-600">Efficient S3 File Uploader with Real-time Monitoring</p> |
|
</header> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
|
<i class="fas fa-cloud-upload-alt text-blue-500 mr-2"></i> |
|
S3 Connection Settings |
|
</h2> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Endpoint URL</label> |
|
<input type="text" id="endpointUrl" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" value="https://75f5ed467c14e39214f3a6f2a169f3d0.r2.cloudflarestorage.com/scrapydocs"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Bucket Name</label> |
|
<input type="text" id="bucketName" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" value="scrapydocs"> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Access Key</label> |
|
<input type="password" id="accessKey" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter your access key"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Secret Key</label> |
|
<input type="password" id="secretKey" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter your secret key"> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-6 flex justify-end"> |
|
<button id="testConnectionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center"> |
|
<i class="fas fa-plug mr-2"></i> Test Connection |
|
</button> |
|
</div> |
|
|
|
<div id="connectionStatus" class="mt-4 hidden"> |
|
<div class="flex items-center p-3 rounded-md"> |
|
<i class="fas fa-circle mr-2 text-gray-400"></i> |
|
<span class="text-sm">Connection status will appear here</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
|
<i class="fas fa-folder-open text-blue-500 mr-2"></i> |
|
Folder Upload |
|
</h2> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Folder Path</label> |
|
<div class="flex"> |
|
<input type="text" id="folderPath" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter folder path or drag & drop" readonly> |
|
<button id="browseBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-r-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> |
|
<i class="fas fa-folder-open mr-1"></i> Browse |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div id="dropzone" class="dropzone p-8 rounded-md mb-4 text-center cursor-pointer"> |
|
<i class="fas fa-cloud-upload-alt text-4xl text-blue-400 mb-3"></i> |
|
<p class="text-gray-600 mb-2">Drag & drop files here or click to select</p> |
|
<p class="text-sm text-gray-500">Supports multiple files and folders</p> |
|
</div> |
|
|
|
|
|
<input type="file" id="fileInput" multiple> |
|
<input type="file" id="folderInput" webkitdirectory directory multiple> |
|
|
|
<div class="flex justify-between"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Prefix (optional)</label> |
|
<input type="text" id="prefix" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., docs/2023"> |
|
</div> |
|
<div class="flex items-end"> |
|
<button id="uploadBtn" class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 flex items-center"> |
|
<i class="fas fa-upload mr-2"></i> Upload Files |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="uploadProgress" class="bg-white rounded-lg shadow-md p-6 mb-6 hidden"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
|
<i class="fas fa-tasks text-blue-500 mr-2"></i> |
|
Upload Progress |
|
</h2> |
|
|
|
<div class="mb-4"> |
|
<div class="flex justify-between mb-1"> |
|
<span class="text-sm font-medium text-gray-700">Overall Progress</span> |
|
<span id="overallProgressText" class="text-sm font-medium text-gray-700">0%</span> |
|
</div> |
|
<div class="w-full bg-gray-200 rounded-full h-2.5"> |
|
<div id="overallProgressBar" class="bg-blue-600 h-2.5 rounded-full progress-bar" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-2 flex justify-between"> |
|
<span class="text-sm font-medium text-gray-700">Files Processed: <span id="filesProcessed">0</span>/<span id="totalFiles">0</span></span> |
|
<span class="text-sm font-medium text-gray-700">Success: <span id="successCount" class="text-green-600">0</span> | Failed: <span id="failedCount" class="text-red-600">0</span></span> |
|
</div> |
|
|
|
<div id="fileList" class="border border-gray-200 rounded-md divide-y divide-gray-200 max-h-64 overflow-y-auto"> |
|
|
|
<div class="text-center py-8 text-gray-500"> |
|
<i class="fas fa-file-upload text-3xl mb-2"></i> |
|
<p>No files selected</p> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-4 flex justify-end"> |
|
<button id="cancelUploadBtn" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center"> |
|
<i class="fas fa-stop-circle mr-2"></i> Cancel Upload |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
|
<i class="fas fa-terminal text-blue-500 mr-2"></i> |
|
Activity Log |
|
</h2> |
|
|
|
<div id="logConsole" class="bg-gray-900 text-green-400 font-mono text-sm p-4 rounded-md h-48 overflow-y-auto"> |
|
<div>> Welcome to CloudSync S3 Uploader</div> |
|
<div>> Ready to upload files to your S3 bucket</div> |
|
</div> |
|
|
|
<div class="mt-4 flex justify-between"> |
|
<button id="clearLogBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 flex items-center"> |
|
<i class="fas fa-trash-alt mr-2"></i> Clear Log |
|
</button> |
|
<div class="flex space-x-2"> |
|
<button id="copyLogBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 flex items-center"> |
|
<i class="fas fa-copy mr-2"></i> Copy Log |
|
</button> |
|
<button id="exportLogBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 flex items-center"> |
|
<i class="fas fa-file-export mr-2"></i> Export Log |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const dropzone = document.getElementById('dropzone'); |
|
const folderPathInput = document.getElementById('folderPath'); |
|
const browseBtn = document.getElementById('browseBtn'); |
|
const uploadBtn = document.getElementById('uploadBtn'); |
|
const uploadProgress = document.getElementById('uploadProgress'); |
|
const fileList = document.getElementById('fileList'); |
|
const logConsole = document.getElementById('logConsole'); |
|
const testConnectionBtn = document.getElementById('testConnectionBtn'); |
|
const connectionStatus = document.getElementById('connectionStatus'); |
|
const fileInput = document.getElementById('fileInput'); |
|
const folderInput = document.getElementById('folderInput'); |
|
|
|
|
|
let filesToUpload = []; |
|
let isUploading = false; |
|
let uploadCancelled = false; |
|
|
|
|
|
dropzone.addEventListener('click', () => { |
|
fileInput.click(); |
|
}); |
|
|
|
dropzone.addEventListener('dragover', (e) => { |
|
e.preventDefault(); |
|
dropzone.classList.add('active'); |
|
}); |
|
|
|
dropzone.addEventListener('dragleave', () => { |
|
dropzone.classList.remove('active'); |
|
}); |
|
|
|
dropzone.addEventListener('drop', (e) => { |
|
e.preventDefault(); |
|
dropzone.classList.remove('active'); |
|
|
|
const files = Array.from(e.dataTransfer.files); |
|
if (files.length > 0) { |
|
filesToUpload = files; |
|
folderPathInput.value = ''; |
|
updateFileList(); |
|
addLog(`Added ${files.length} file(s) from drag & drop`); |
|
} |
|
}); |
|
|
|
browseBtn.addEventListener('click', () => { |
|
folderInput.click(); |
|
}); |
|
|
|
fileInput.addEventListener('change', (e) => { |
|
const files = Array.from(e.target.files); |
|
if (files.length > 0) { |
|
filesToUpload = files; |
|
folderPathInput.value = ''; |
|
updateFileList(); |
|
addLog(`Added ${files.length} file(s) from file selection`); |
|
} |
|
fileInput.value = ''; |
|
}); |
|
|
|
folderInput.addEventListener('change', (e) => { |
|
const files = Array.from(e.target.files); |
|
if (files.length > 0) { |
|
filesToUpload = files; |
|
const path = e.target.files[0].webkitRelativePath.split('/')[0]; |
|
folderPathInput.value = path; |
|
updateFileList(); |
|
addLog(`Added folder "${path}" containing ${files.length} files`); |
|
} |
|
folderInput.value = ''; |
|
}); |
|
|
|
uploadBtn.addEventListener('click', () => { |
|
if (filesToUpload.length === 0 && !folderPathInput.value) { |
|
addLog("✖ Error: No files or folder selected for upload", "error"); |
|
return; |
|
} |
|
|
|
startUpload(); |
|
}); |
|
|
|
testConnectionBtn.addEventListener('click', () => { |
|
testConnection(); |
|
}); |
|
|
|
document.getElementById('cancelUploadBtn').addEventListener('click', () => { |
|
if (isUploading) { |
|
uploadCancelled = true; |
|
isUploading = false; |
|
addLog("Upload cancelled by user", "warning"); |
|
} |
|
}); |
|
|
|
document.getElementById('clearLogBtn').addEventListener('click', () => { |
|
logConsole.innerHTML = ''; |
|
addLog("Log cleared"); |
|
}); |
|
|
|
document.getElementById('copyLogBtn').addEventListener('click', () => { |
|
navigator.clipboard.writeText(logConsole.textContent); |
|
addLog("Log content copied to clipboard"); |
|
}); |
|
|
|
document.getElementById('exportLogBtn').addEventListener('click', () => { |
|
const blob = new Blob([logConsole.textContent], { type: 'text/plain' }); |
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
a.href = url; |
|
a.download = 'cloudsync-log.txt'; |
|
document.body.appendChild(a); |
|
a.click(); |
|
document.body.removeChild(a); |
|
URL.revokeObjectURL(url); |
|
addLog("Log exported as text file"); |
|
}); |
|
|
|
|
|
function updateFileList() { |
|
if (filesToUpload.length === 0 && !folderPathInput.value) { |
|
fileList.innerHTML = ` |
|
<div class="text-center py-8 text-gray-500"> |
|
<i class="fas fa-file-upload text-3xl mb-2"></i> |
|
<p>No files selected</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
let html = ''; |
|
|
|
if (folderPathInput.value) { |
|
html += ` |
|
<div class="file-item p-3 flex items-center bg-blue-50"> |
|
<i class="fas fa-folder text-blue-500 mr-3"></i> |
|
<div class="flex-1"> |
|
<div class="font-medium">${folderPathInput.value.split('/').pop() || folderPathInput.value.split('\\').pop()}</div> |
|
<div class="text-xs text-gray-500">Folder (${filesToUpload.length} files)</div> |
|
</div> |
|
<div class="text-sm text-gray-500">Pending</div> |
|
</div> |
|
`; |
|
} |
|
|
|
filesToUpload.forEach((file, index) => { |
|
html += ` |
|
<div class="file-item p-3 flex items-center" data-index="${index}"> |
|
<i class="fas ${file.type.startsWith('image/') ? 'fa-image' : 'fa-file'} text-gray-500 mr-3"></i> |
|
<div class="flex-1 truncate"> |
|
<div class="font-medium truncate">${file.name}</div> |
|
<div class="text-xs text-gray-500">${formatFileSize(file.size)}</div> |
|
</div> |
|
<div class="text-sm text-gray-500">Pending</div> |
|
</div> |
|
`; |
|
}); |
|
|
|
fileList.innerHTML = html; |
|
uploadProgress.classList.remove('hidden'); |
|
} |
|
|
|
function formatFileSize(bytes) { |
|
if (bytes === 0) return '0 Bytes'; |
|
const k = 1024; |
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; |
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
} |
|
|
|
function addLog(message, type = "info") { |
|
const now = new Date(); |
|
const timestamp = now.toLocaleTimeString(); |
|
let icon = '>'; |
|
|
|
if (type === "error") { |
|
icon = '<i class="fas fa-times-circle text-red-500"></i>'; |
|
} else if (type === "success") { |
|
icon = '<i class="fas fa-check-circle text-green-500"></i>'; |
|
} else if (type === "warning") { |
|
icon = '<i class="fas fa-exclamation-triangle text-yellow-500"></i>'; |
|
} |
|
|
|
const logEntry = document.createElement('div'); |
|
logEntry.innerHTML = `<span class="text-gray-500">${timestamp}</span> ${icon} ${message}`; |
|
logConsole.appendChild(logEntry); |
|
logConsole.scrollTop = logConsole.scrollHeight; |
|
} |
|
|
|
function testConnection() { |
|
const endpointUrl = document.getElementById('endpointUrl').value; |
|
const accessKey = document.getElementById('accessKey').value; |
|
const secretKey = document.getElementById('secretKey').value; |
|
|
|
if (!endpointUrl || !accessKey || !secretKey) { |
|
addLog("✖ Error: Please fill all connection fields", "error"); |
|
return; |
|
} |
|
|
|
testConnectionBtn.innerHTML = '<i class="fas fa-spinner rotate mr-2"></i> Testing...'; |
|
testConnectionBtn.disabled = true; |
|
|
|
|
|
setTimeout(() => { |
|
connectionStatus.classList.remove('hidden'); |
|
connectionStatus.innerHTML = ` |
|
<div class="flex items-center p-3 rounded-md bg-green-50"> |
|
<i class="fas fa-check-circle mr-2 text-green-500"></i> |
|
<span class="text-sm text-green-800">Successfully connected to S3 bucket</span> |
|
</div> |
|
`; |
|
testConnectionBtn.innerHTML = '<i class="fas fa-plug mr-2"></i> Test Connection'; |
|
testConnectionBtn.disabled = false; |
|
addLog("✔ Successfully connected to S3 bucket", "success"); |
|
}, 1500); |
|
} |
|
|
|
function startUpload() { |
|
if (isUploading) return; |
|
|
|
isUploading = true; |
|
uploadCancelled = false; |
|
const prefix = document.getElementById('prefix').value; |
|
|
|
|
|
document.getElementById('filesProcessed').textContent = '0'; |
|
document.getElementById('totalFiles').textContent = filesToUpload.length; |
|
document.getElementById('successCount').textContent = '0'; |
|
document.getElementById('failedCount').textContent = '0'; |
|
document.getElementById('overallProgressBar').style.width = '0%'; |
|
document.getElementById('overallProgressText').textContent = '0%'; |
|
|
|
|
|
uploadBtn.innerHTML = '<i class="fas fa-spinner rotate mr-2"></i> Uploading...'; |
|
uploadBtn.disabled = true; |
|
|
|
addLog(`Starting upload to bucket: ${document.getElementById('bucketName').value}`); |
|
if (prefix) { |
|
addLog(`Using prefix: ${prefix}`); |
|
} |
|
|
|
if (folderPathInput.value) { |
|
addLog(`Uploading folder: ${folderPathInput.value}`); |
|
} |
|
|
|
|
|
simulateFileUpload(filesToUpload, prefix); |
|
} |
|
|
|
function simulateFileUpload(files, prefix) { |
|
const totalFiles = files.length; |
|
let processed = 0; |
|
let success = 0; |
|
let failed = 0; |
|
|
|
const uploadInterval = setInterval(() => { |
|
if (uploadCancelled || processed >= totalFiles) { |
|
clearInterval(uploadInterval); |
|
finishUpload(processed, success, failed); |
|
return; |
|
} |
|
|
|
processed++; |
|
const isSuccess = Math.random() > 0.1; |
|
|
|
if (isSuccess) { |
|
success++; |
|
document.getElementById('successCount').textContent = success; |
|
addLog(`✔ Successfully uploaded ${prefix ? prefix + '/' : ''}${files[processed-1].name}`, "success"); |
|
|
|
|
|
const fileItem = fileList.querySelector(`.file-item[data-index="${processed-1}"]`); |
|
if (fileItem) { |
|
const statusDiv = fileItem.querySelector('div:last-child'); |
|
statusDiv.innerHTML = '<span class="text-green-600"><i class="fas fa-check mr-1"></i>Done</span>'; |
|
} |
|
} else { |
|
failed++; |
|
document.getElementById('failedCount').textContent = failed; |
|
addLog(`✖ Failed to upload ${prefix ? prefix + '/' : ''}${files[processed-1].name}`, "error"); |
|
} |
|
|
|
document.getElementById('filesProcessed').textContent = processed; |
|
const progress = Math.floor((processed / totalFiles) * 100); |
|
document.getElementById('overallProgressBar').style.width = `${progress}%`; |
|
document.getElementById('overallProgressText').textContent = `${progress}%`; |
|
|
|
}, 500); |
|
} |
|
|
|
function finishUpload(processed, success, failed) { |
|
isUploading = false; |
|
|
|
uploadBtn.innerHTML = '<i class="fas fa-upload mr-2"></i> Upload Files'; |
|
uploadBtn.disabled = false; |
|
|
|
if (uploadCancelled) { |
|
addLog(`Upload cancelled. Processed ${processed} files (${success} success, ${failed} failed)`, "warning"); |
|
} else { |
|
addLog(`Upload complete. Processed ${processed} files (${success} success, ${failed} failed)`); |
|
|
|
if (failed > 0) { |
|
addLog("Some files failed to upload. You can try uploading them again.", "warning"); |
|
} |
|
} |
|
} |
|
|
|
|
|
updateFileList(); |
|
addLog("Application initialized"); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=gewei20/upload-r2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |