|
|
@@ -5,6 +5,8 @@
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>AWS S3 文件夹批量下载工具</title>
|
|
|
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.938.0.min.js"></script>
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
|
|
|
<style>
|
|
|
* {
|
|
|
box-sizing: border-box;
|
|
|
@@ -116,6 +118,14 @@
|
|
|
background-color: #27ae60;
|
|
|
}
|
|
|
|
|
|
+ .download-zip {
|
|
|
+ background-color: #9b59b6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .download-zip:hover {
|
|
|
+ background-color: #8e44ad;
|
|
|
+ }
|
|
|
+
|
|
|
.clear {
|
|
|
background-color: #e74c3c;
|
|
|
}
|
|
|
@@ -293,7 +303,7 @@
|
|
|
<body>
|
|
|
<div class="container">
|
|
|
<h1>AWS S3 文件夹批量下载工具</h1>
|
|
|
- <p class="description">批量下载指定目录中的文件</p>
|
|
|
+ <p class="description">批量下载指定目录中的文件并打包为ZIP</p>
|
|
|
|
|
|
<div class="stats">
|
|
|
<div class="stat-item">
|
|
|
@@ -316,7 +326,7 @@
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
<button id="loadFolders" class="load-btn">加载所有文件夹</button>
|
|
|
- <button id="downloadAll" class="download-all" disabled>全部下载</button>
|
|
|
+ <button id="downloadZip" class="download-zip" disabled>打包下载ZIP</button>
|
|
|
<button id="clearAll" class="clear">清空列表</button>
|
|
|
</div>
|
|
|
|
|
|
@@ -355,7 +365,7 @@
|
|
|
|
|
|
// DOM元素
|
|
|
const loadFoldersBtn = document.getElementById('loadFolders');
|
|
|
- const downloadAllBtn = document.getElementById('downloadAll');
|
|
|
+ const downloadZipBtn = document.getElementById('downloadZip');
|
|
|
const clearAllBtn = document.getElementById('clearAll');
|
|
|
const foldersContainer = document.getElementById('foldersContainer');
|
|
|
const summary = document.getElementById('summary');
|
|
|
@@ -442,106 +452,114 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 下载单个文件
|
|
|
- async function downloadFile(file, folder) {
|
|
|
- file.status = 'downloading';
|
|
|
- updateFolderUI(folder);
|
|
|
+ // 下载所有文件并打包为ZIP
|
|
|
+ async function downloadAllAsZip() {
|
|
|
+ summary.style.display = 'block';
|
|
|
+ summaryText.textContent = '正在准备下载...';
|
|
|
+ overallProgress.style.width = '0%';
|
|
|
|
|
|
- try {
|
|
|
- const params = {
|
|
|
- Bucket: bucketName,
|
|
|
- Key: file.key
|
|
|
- };
|
|
|
+ // 收集所有需要下载的文件
|
|
|
+ const allFiles = [];
|
|
|
+ folderData.forEach(folder => {
|
|
|
+ if (folder.status === 'ready' || folder.status === 'partial') {
|
|
|
+ folder.files.forEach(file => {
|
|
|
+ if (file.status !== 'completed') {
|
|
|
+ allFiles.push({
|
|
|
+ folder: folder.path,
|
|
|
+ key: file.key,
|
|
|
+ name: file.name || file.key.split('/').pop()
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (allFiles.length === 0) {
|
|
|
+ alert('没有可下载的文件');
|
|
|
+ summary.style.display = 'none';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ summaryText.textContent = `正在下载 ${allFiles.length} 个文件...`;
|
|
|
+
|
|
|
+ // 创建ZIP文件
|
|
|
+ const zip = new JSZip();
|
|
|
+ let downloadedCount = 0;
|
|
|
+
|
|
|
+ // 分批下载文件,避免同时发起太多请求
|
|
|
+ const batchSize = 5;
|
|
|
+ for (let i = 0; i < allFiles.length; i += batchSize) {
|
|
|
+ const batch = allFiles.slice(i, i + batchSize);
|
|
|
|
|
|
- // 获取预签名URL
|
|
|
- const url = await s3.getSignedUrlPromise('getObject', params);
|
|
|
- file.url = url;
|
|
|
+ // 下载当前批次的所有文件
|
|
|
+ await Promise.allSettled(batch.map(file => downloadFileForZip(file, zip)));
|
|
|
|
|
|
- // 创建隐藏的下载链接并触发点击
|
|
|
- const a = document.createElement('a');
|
|
|
- a.href = url;
|
|
|
- a.download = file.name || file.key.split('/').pop();
|
|
|
- a.style.display = 'none';
|
|
|
- document.body.appendChild(a);
|
|
|
- a.click();
|
|
|
- document.body.removeChild(a);
|
|
|
+ downloadedCount += batch.length;
|
|
|
|
|
|
- file.status = 'completed';
|
|
|
- file.progress = 100;
|
|
|
- folder.completedFiles++;
|
|
|
- updateFolderUI(folder);
|
|
|
- updateStats();
|
|
|
+ // 更新进度
|
|
|
+ const progress = (downloadedCount / allFiles.length) * 100;
|
|
|
+ overallProgress.style.width = `${progress}%`;
|
|
|
+ summaryText.textContent = `已下载 ${downloadedCount}/${allFiles.length} 个文件 (${Math.round(progress)}%)`;
|
|
|
|
|
|
- // 检查文件夹是否全部完成
|
|
|
- checkFolderCompletion(folder);
|
|
|
- } catch (error) {
|
|
|
- console.error('下载文件失败:', error);
|
|
|
- file.status = 'error';
|
|
|
- file.error = error.message;
|
|
|
- updateFolderUI(folder);
|
|
|
- updateStats();
|
|
|
+ // 添加延迟以避免请求过于频繁
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- // 下载文件夹中的所有文件
|
|
|
- async function downloadFolder(folder) {
|
|
|
- folder.status = 'downloading';
|
|
|
- updateFolderUI(folder);
|
|
|
|
|
|
- for (const file of folder.files) {
|
|
|
- if (file.status !== 'completed') {
|
|
|
- await downloadFile(file, folder);
|
|
|
- // 添加延迟以避免请求过于频繁
|
|
|
- await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 下载所有文件夹
|
|
|
- async function downloadAllFolders() {
|
|
|
- summary.style.display = 'block';
|
|
|
- const totalFiles = folderData.reduce((sum, folder) => sum + folder.files.length, 0);
|
|
|
- let completedFiles = 0;
|
|
|
+ summaryText.textContent = '正在生成ZIP文件...';
|
|
|
|
|
|
- summaryText.textContent = `正在下载 ${totalFiles} 个文件...`;
|
|
|
+ // 生成ZIP文件
|
|
|
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
|
|
|
|
- for (const folder of folderData) {
|
|
|
+ // 下载ZIP文件
|
|
|
+ saveAs(zipBlob, 'folders_download.zip');
|
|
|
+ summaryText.textContent = `下载完成!共打包 ${allFiles.length} 个文件`;
|
|
|
+
|
|
|
+ // 更新文件夹状态
|
|
|
+ folderData.forEach(folder => {
|
|
|
if (folder.status === 'ready' || folder.status === 'partial') {
|
|
|
- folder.status = 'downloading';
|
|
|
+ folder.status = 'completed';
|
|
|
+ folder.completedFiles = folder.fileCount;
|
|
|
updateFolderUI(folder);
|
|
|
-
|
|
|
- for (const file of folder.files) {
|
|
|
- if (file.status !== 'completed') {
|
|
|
- await downloadFile(file, folder);
|
|
|
- completedFiles++;
|
|
|
-
|
|
|
- // 更新总进度
|
|
|
- const progress = (completedFiles / totalFiles) * 100;
|
|
|
- overallProgress.style.width = `${progress}%`;
|
|
|
- summaryText.textContent = `已下载 ${completedFiles}/${totalFiles} 个文件 (${Math.round(progress)}%)`;
|
|
|
-
|
|
|
- // 添加延迟以避免请求过于频繁
|
|
|
- await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
|
|
|
- summaryText.textContent = `下载完成!共下载 ${completedFiles} 个文件`;
|
|
|
- downloadAllBtn.disabled = true;
|
|
|
+ updateStats();
|
|
|
+ updateDownloadButtonState();
|
|
|
}
|
|
|
|
|
|
- // 检查文件夹是否全部完成
|
|
|
- function checkFolderCompletion(folder) {
|
|
|
- if (folder.completedFiles === folder.fileCount) {
|
|
|
- folder.status = 'completed';
|
|
|
- } else if (folder.completedFiles > 0) {
|
|
|
- folder.status = 'partial';
|
|
|
+ // 下载单个文件并添加到ZIP
|
|
|
+ async function downloadFileForZip(file, zip) {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ Bucket: bucketName,
|
|
|
+ Key: file.key
|
|
|
+ };
|
|
|
+
|
|
|
+ const data = await s3.getObject(params).promise();
|
|
|
+
|
|
|
+ // 将文件添加到ZIP中,保持文件夹结构
|
|
|
+ const folderPath = file.folder + '/';
|
|
|
+ const filePath = folderPath + file.name;
|
|
|
+
|
|
|
+ zip.file(filePath, data.Body);
|
|
|
+
|
|
|
+ // 更新对应文件夹的完成状态
|
|
|
+ const folder = folderData.find(f => f.path === file.folder);
|
|
|
+ if (folder) {
|
|
|
+ const fileObj = folder.files.find(f => f.key === file.key);
|
|
|
+ if (fileObj) {
|
|
|
+ fileObj.status = 'completed';
|
|
|
+ fileObj.progress = 100;
|
|
|
+ folder.completedFiles++;
|
|
|
+ updateFolderUI(folder);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`下载文件 ${file.key} 失败:`, error);
|
|
|
+ return false;
|
|
|
}
|
|
|
-
|
|
|
- updateFolderUI(folder);
|
|
|
- updateDownloadButtonState();
|
|
|
- updateStats();
|
|
|
}
|
|
|
|
|
|
// 更新文件夹UI
|
|
|
@@ -552,7 +570,6 @@
|
|
|
const statusElement = folderElement.querySelector('.folder-status');
|
|
|
const progressBar = folderElement.querySelector('.progress');
|
|
|
const fileCountElement = folderElement.querySelector('.file-count');
|
|
|
- const downloadBtn = folderElement.querySelector('.download-btn');
|
|
|
const fileList = folderElement.querySelector('.file-list');
|
|
|
|
|
|
// 更新状态
|
|
|
@@ -571,7 +588,6 @@
|
|
|
case 'ready':
|
|
|
statusText = '准备下载';
|
|
|
statusClass = 'status-ready';
|
|
|
- downloadBtn.disabled = false;
|
|
|
break;
|
|
|
case 'empty':
|
|
|
statusText = '空文件夹';
|
|
|
@@ -580,17 +596,14 @@
|
|
|
case 'downloading':
|
|
|
statusText = '下载中';
|
|
|
statusClass = 'status-downloading';
|
|
|
- downloadBtn.disabled = true;
|
|
|
break;
|
|
|
case 'completed':
|
|
|
statusText = '已完成';
|
|
|
statusClass = 'status-completed';
|
|
|
- downloadBtn.disabled = true;
|
|
|
break;
|
|
|
case 'partial':
|
|
|
statusText = '部分完成';
|
|
|
statusClass = 'status-ready';
|
|
|
- downloadBtn.disabled = false;
|
|
|
break;
|
|
|
case 'error':
|
|
|
statusText = '错误';
|
|
|
@@ -675,12 +688,10 @@
|
|
|
</div>
|
|
|
<button class="expand-btn">显示文件</button>
|
|
|
<div class="file-list"></div>
|
|
|
- <button class="download-btn" disabled style="margin-top: 10px; width: 100%;">下载文件夹</button>
|
|
|
`;
|
|
|
|
|
|
// 添加事件监听器
|
|
|
const expandBtn = folderElement.querySelector('.expand-btn');
|
|
|
- const downloadBtn = folderElement.querySelector('.download-btn');
|
|
|
const fileList = folderElement.querySelector('.file-list');
|
|
|
|
|
|
expandBtn.addEventListener('click', () => {
|
|
|
@@ -695,10 +706,6 @@
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- downloadBtn.addEventListener('click', () => {
|
|
|
- downloadFolder(folder);
|
|
|
- });
|
|
|
-
|
|
|
foldersContainer.appendChild(folderElement);
|
|
|
updateFolderUI(folder);
|
|
|
});
|
|
|
@@ -717,7 +724,7 @@
|
|
|
const hasReadyFolders = folderData.some(f =>
|
|
|
f.status === 'ready' || f.status === 'partial'
|
|
|
);
|
|
|
- downloadAllBtn.disabled = !hasReadyFolders;
|
|
|
+ downloadZipBtn.disabled = !hasReadyFolders;
|
|
|
}
|
|
|
|
|
|
// 格式化文件大小
|
|
|
@@ -733,7 +740,7 @@
|
|
|
|
|
|
// 事件监听器
|
|
|
loadFoldersBtn.addEventListener('click', loadAllFolders);
|
|
|
- downloadAllBtn.addEventListener('click', downloadAllFolders);
|
|
|
+ downloadZipBtn.addEventListener('click', downloadAllAsZip);
|
|
|
|
|
|
clearAllBtn.addEventListener('click', () => {
|
|
|
if (confirm('确定要清空所有文件夹吗?')) {
|