| <!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> |