文件夾拖放上傳系統(保持文件結構)
下面是一個完整的HTML5+CSS3+AJAX+PHP實現,支持拖放文件夾上傳并保持原有文件結構的解決方案。
前端部分 (index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>文件夾拖放上傳</title><style>* {box-sizing: border-box;font-family: Arial, sans-serif;}body {margin: 0;padding: 20px;background-color: #f5f5f5;}.container {max-width: 800px;margin: 0 auto;background: white;padding: 20px;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);}h1 {text-align: center;color: #333;}.upload-area {border: 2px dashed #ccc;border-radius: 8px;padding: 40px;text-align: center;margin: 20px 0;transition: all 0.3s;background-color: #fafafa;}.upload-area.highlight {border-color: #4CAF50;background-color: #e8f5e9;}.upload-area p {margin: 0;font-size: 18px;color: #666;}.upload-area .icon {font-size: 48px;color: #4CAF50;margin-bottom: 15px;}.progress-container {margin-top: 20px;display: none;}.progress-bar {height: 20px;background-color: #e0e0e0;border-radius: 4px;margin-bottom: 10px;overflow: hidden;}.progress {height: 100%;background-color: #4CAF50;width: 0%;transition: width 0.3s;}.status {font-size: 14px;color: #666;}.file-list {margin-top: 20px;border-top: 1px solid #eee;padding-top: 20px;}.file-list h3 {margin-top: 0;color: #333;}.file-list ul {list-style: none;padding: 0;max-height: 300px;overflow-y: auto;}.file-list li {padding: 8px 0;border-bottom: 1px solid #eee;color: #555;font-family: monospace;}.btn {background-color: #4CAF50;color: white;border: none;padding: 10px 20px;text-align: center;text-decoration: none;display: inline-block;font-size: 16px;margin: 10px 2px;cursor: pointer;border-radius: 4px;transition: background-color 0.3s;}.btn:hover {background-color: #45a049;}.btn:disabled {background-color: #cccccc;cursor: not-allowed;}</style>
</head>
<body><div class="container"><h1>文件夾拖放上傳</h1><div class="upload-area" id="dropArea"><div class="icon">📁</div><p>拖放文件夾到此處</p><p><small>或</small></p><button id="browseBtn" class="btn">選擇文件夾</button><input type="file" id="folderInput" webkitdirectory directory multiple style="display: none;"></div><div class="progress-container" id="progressContainer"><div class="progress-bar"><div class="progress" id="progressBar"></div></div><div class="status" id="statusText">準備上傳...</div></div><div class="file-list"><h3>文件結構預覽</h3><ul id="fileList"></ul></div><button id="uploadBtn" class="btn" disabled>開始上傳</button></div><script>document.addEventListener('DOMContentLoaded', function() {const dropArea = document.getElementById('dropArea');const folderInput = document.getElementById('folderInput');const browseBtn = document.getElementById('browseBtn');const uploadBtn = document.getElementById('uploadBtn');const progressContainer = document.getElementById('progressContainer');const progressBar = document.getElementById('progressBar');const statusText = document.getElementById('statusText');const fileList = document.getElementById('fileList');let files = [];let fileStructure = {};// 阻止默認拖放行為['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {dropArea.addEventListener(eventName, preventDefaults, false);document.body.addEventListener(eventName, preventDefaults, false);});// 高亮顯示拖放區域['dragenter', 'dragover'].forEach(eventName => {dropArea.addEventListener(eventName, highlight, false);});['dragleave', 'drop'].forEach(eventName => {dropArea.addEventListener(eventName, unhighlight, false);});// 處理文件放置dropArea.addEventListener('drop', handleDrop, false);// 瀏覽文件夾按鈕browseBtn.addEventListener('click', () => folderInput.click());// 文件夾選擇變化folderInput.addEventListener('change', handleFolderSelect, false);// 上傳按鈕uploadBtn.addEventListener('click', startUpload);function preventDefaults(e) {e.preventDefault();e.stopPropagation();}function highlight() {dropArea.classList.add('highlight');}function unhighlight() {dropArea.classList.remove('highlight');}function handleDrop(e) {const dt = e.dataTransfer;const items = dt.items;files = [];fileStructure = {};fileList.innerHTML = '';// 檢查是否支持目錄上傳if (items && items.length && 'webkitGetAsEntry' in items[0]) {processItems(items);} else {statusText.textContent = '您的瀏覽器不支持文件夾上傳,請使用選擇文件夾按鈕';}}function handleFolderSelect(e) {files = [];fileStructure = {};fileList.innerHTML = '';if (e.target.files.length) {processFileList(e.target.files);}}function processItems(items) {let remaining = items.length;for (let i = 0; i < items.length; i++) {const item = items[i].webkitGetAsEntry();if (item) {scanEntry(item);} else {remaining--;if (remaining === 0) {updateUI();}}}}function scanEntry(entry, path = '') {if (entry.isFile) {entry.file(file => {file.relativePath = path + file.name;files.push(file);// 構建文件結構const pathParts = file.relativePath.split('/');let currentLevel = fileStructure;for (let i = 0; i < pathParts.length - 1; i++) {const part = pathParts[i];if (!currentLevel[part]) {currentLevel[part] = {};}currentLevel = currentLevel[part];}currentLevel[pathParts[pathParts.length - 1]] = 'file';if (--remainingFiles === 0) {updateUI();}});} else if (entry.isDirectory) {const dirReader = entry.createReader();let entries = [];const readEntries = () => {dirReader.readEntries(results => {if (results.length) {entries = entries.concat(Array.from(results));readEntries();} else {// 構建目錄結構const pathParts = (path + entry.name).split('/');let currentLevel = fileStructure;for (let i = 0; i < pathParts.length; i++) {const part = pathParts[i];if (!currentLevel[part]) {currentLevel[part] = {};}currentLevel = currentLevel[part];}remainingDirs += entries.length;remainingFiles += entries.length;for (let i = 0; i < entries.length; i++) {const newPath = path + entry.name + '/';scanEntry(entries[i], newPath);}if (--remainingDirs === 0 && remainingFiles === 0) {updateUI();}}});};readEntries();}}let remainingFiles = 0;let remainingDirs = 0;function processFileList(fileList) {files = Array.from(fileList);fileStructure = {};for (const file of files) {const pathParts = file.webkitRelativePath.split('/');let currentLevel = fileStructure;for (let i = 0; i < pathParts.length - 1; i++) {const part = pathParts[i];if (!currentLevel[part]) {currentLevel[part] = {};}currentLevel = currentLevel[part];}currentLevel[pathParts[pathParts.length - 1]] = 'file';}updateUI();}function updateUI() {// 顯示文件結構renderFileStructure(fileStructure, '');if (files.length > 0) {uploadBtn.disabled = false;statusText.textContent = `準備上傳 ${files.length} 個文件`;} else {uploadBtn.disabled = true;statusText.textContent = '沒有可上傳的文件';}}function renderFileStructure(structure, path, indent = 0) {for (const key in structure) {const li = document.createElement('li');li.style.paddingLeft = `${indent * 20}px`;if (structure[key] === 'file') {li.textContent = `📄 ${key}`;fileList.appendChild(li);} else {li.textContent = `📁 ${key}`;fileList.appendChild(li);renderFileStructure(structure[key], path + key + '/', indent + 1);}}}function startUpload() {if (files.length === 0) return;progressContainer.style.display = 'block';uploadBtn.disabled = true;const totalFiles = files.length;let uploaded = 0;statusText.textContent = `上傳中: 0/${totalFiles}`;function uploadNext() {if (uploaded >= totalFiles) {statusText.textContent = `上傳完成! 共上傳 ${totalFiles} 個文件`;uploadBtn.disabled = false;return;}const file = files[uploaded];const formData = new FormData();formData.append('file', file);formData.append('path', file.webkitRelativePath || file.relativePath);const xhr = new XMLHttpRequest();xhr.open('POST', 'upload.php', true);xhr.upload.onprogress = function(e) {if (e.lengthComputable) {const fileProgress = (e.loaded / e.total) * (100 / totalFiles);const totalProgress = (uploaded / totalFiles) * 100 + fileProgress;progressBar.style.width = `${totalProgress}%`;}};xhr.onload = function() {if (xhr.status === 200) {uploaded++;statusText.textContent = `上傳中: ${uploaded}/${totalFiles}`;uploadNext();} else {statusText.textContent = `上傳失敗: ${xhr.responseText}`;uploadBtn.disabled = false;}};xhr.onerror = function() {statusText.textContent = '上傳出錯,請重試';uploadBtn.disabled = false;};xhr.send(formData);}uploadNext();}});</script>
</body>
</html>
后端部分 (upload.php)
<?php
header('Content-Type: application/json');// 配置上傳目錄
$uploadDir = __DIR__ . '/uploads/';// 確保上傳目錄存在
if (!file_exists($uploadDir)) {mkdir($uploadDir, 0777, true);
}// 檢查是否有文件上傳
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {http_response_code(400);echo json_encode(['error' => '沒有文件被上傳或上傳出錯']);exit;
}// 獲取文件路徑信息
$relativePath = isset($_POST['path']) ? $_POST['path'] : '';
$relativePath = ltrim($relativePath, '/');// 防止目錄遍歷攻擊
if (strpos($relativePath, '..') !== false) {http_response_code(400);echo json_encode(['error' => '非法路徑']);exit;
}// 創建完整的目錄結構
$fullPath = $uploadDir . $relativePath;
$directory = dirname($fullPath);if (!file_exists($directory)) {if (!mkdir($directory, 0777, true)) {http_response_code(500);echo json_encode(['error' => '無法創建目錄']);exit;}
}// 移動上傳的文件
if (move_uploaded_file($_FILES['file']['tmp_name'], $fullPath)) {echo json_encode(['success' => true, 'path' => $relativePath]);
} else {http_response_code(500);echo json_encode(['error' => '文件移動失敗']);
}
功能說明
-
前端功能:
- 支持拖放文件夾上傳
- 支持通過按鈕選擇文件夾
- 實時顯示文件結構預覽
- 顯示上傳進度條
- 保持原始文件目錄結構
-
后端功能:
- 接收上傳的文件
- 根據相對路徑創建目錄結構
- 防止目錄遍歷攻擊
- 返回上傳狀態
部署說明
- 將這兩個文件放在同一目錄下
- 確保PHP有寫入權限(需要創建
uploads
目錄) - 確保服務器支持PHP文件上傳
- 現代瀏覽器訪問index.html即可使用
注意事項
- 文件夾上傳需要現代瀏覽器支持(Chrome、Edge、Firefox等)
- 大文件上傳可能需要調整PHP配置(upload_max_filesize, post_max_size等)
- 生產環境應考慮添加更多安全措施,如文件類型檢查、用戶認證等
- 對于超大文件或大量文件,可能需要分塊上傳實現
這個實現完全使用原生技術,不依賴任何第三方庫,保持了原始文件目錄結構。