引言
作為前端開發者,你可能經常需要與服務器文件系統交互。本文將詳細介紹如何通過PHP配合Apache實現服務器端文件管理功能。即使你沒有任何PHP經驗,也能按照本教程實現完整解決方案!
系統準備
PHP下載與安裝
-
訪問PHP官網下載頁面
-
選擇與Apache匹配的版本:
-
Apache版本:2.4.x
-
PHP版本:8.4.x TS (Thread Safe)(對應64位Apache的文件名應該類似:VS17 x64 Thread Safe,對應32位Apache的文件名應該類似:VS17 x86?Thread Safe)
-
架構:x64(64位系統)或x86(32位系統)
-
-
解壓到目錄(如
D:\php
),目錄結構應包含:php.exe php8apache2_4.dll php.ini-development ext/ (擴展目錄)
Apache配置PHP
編輯conf/httpd.conf
文件:
# 加載PHP模塊
LoadModule php_module "D:/php/php8apache2_4.dll"# 指定php.ini目錄
PHPIniDir "D:/php"# 將.php文件交給PHP處理
AddHandler application/x-httpd-php .php# 關聯文件擴展名
<FilesMatch \.php$>SetHandler application/x-httpd-php
</FilesMatch>
驗證配置
cd D:\Apache24\bin
httpd -t
看到"Syntax OK"表示配置正確,重啟Apache服務:
httpd -k restart
文件管理API實現
創建file_manager.php
文件:
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); // 測試用,生產環境應移除// 安全配置
$BASE_DIR = realpath(__DIR__ . '/Resources');
$ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'docx'];// 路徑驗證函數
function validatePath($path) {global $BASE_DIR;$realPath = realpath($BASE_DIR . '/' . $path);return ($realPath && strpos($realPath, $BASE_DIR) === 0) ? $realPath : false;
}// 獲取目錄結構
function listDirectory($path) {$realPath = validatePath($path);if (!$realPath || !is_dir($realPath)) {return ['error' => '無效目錄路徑'];}$result = [];$items = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realPath, FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::SELF_FIRST);foreach ($items as $item) {$relativePath = substr($item->getPathname(), strlen($realPath)) ?: '/';$relativePath = ltrim(str_replace('\\', '/', $relativePath), '/');$result[] = ['name' => $item->getFilename(),'path' => $relativePath,'type' => $item->isDir() ? 'directory' : 'file','size' => $item->isFile() ? $item->getSize() : 0,'modified' => date('Y-m-d H:i:s', $item->getMTime())];}return $result;
}// 主請求處理
$response = ['status' => 'error', 'message' => '無效請求'];
$request = json_decode(file_get_contents('php://input'), true) ?? $_REQUEST;try {if (!isset($request['action'])) {throw new Exception('未指定操作');}$action = $request['action'];switch ($action) {case 'list':$path = $request['path'] ?? '';$data = listDirectory($path);$response = ['status' => 'success', 'data' => $data];break;case 'create-folder':$path = $request['path'] ?? '';$name = $request['name'] ?? '';if (empty($name)) throw new Exception('文件夾名稱不能為空');$realPath = validatePath($path);if (!$realPath) throw new Exception('無效路徑');// 清理文件夾名稱$cleanName = preg_replace('/[^a-zA-Z0-9_-]/', '', $name);$newFolder = $realPath . DIRECTORY_SEPARATOR . $cleanName;if (file_exists($newFolder)) {throw new Exception('文件夾已存在');}if (!mkdir($newFolder, 0755)) {throw new Exception('創建文件夾失敗');}$response = ['status' => 'success', 'message' => '文件夾創建成功'];break;case 'delete-folder':$path = $request['path'] ?? '';$realPath = validatePath($path);if (!$realPath || !is_dir($realPath)) {throw new Exception('無效目錄路徑');}// 安全措施:防止刪除根目錄if ($realPath === $BASE_DIR) {throw new Exception('不能刪除根目錄');}// 遞歸刪除$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realPath, FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST);foreach ($files as $file) {if ($file->isDir()) {rmdir($file->getRealPath());} else {unlink($file->getRealPath());}}if (!rmdir($realPath)) {throw new Exception('刪除文件夾失敗');}$response = ['status' => 'success', 'message' => '文件夾已刪除'];break;case 'rename':$type = $request['type'] ?? 'file';$path = $request['path'] ?? '';$newName = $request['newName'] ?? '';if (empty($newName)) throw new Exception('新名稱不能為空');$realPath = validatePath($path);if (!$realPath) throw new Exception('無效路徑');// 清理新名稱$cleanName = preg_replace('/[^a-zA-Z0-9_.-]/', '', $newName);$newPath = dirname($realPath) . DIRECTORY_SEPARATOR . $cleanName;if (file_exists($newPath)) {throw new Exception('目標名稱已存在');}if (!rename($realPath, $newPath)) {throw new Exception('重命名失敗');}$response = ['status' => 'success', 'message' => '重命名成功'];break;case 'upload-file':$targetPath = $request['path'] ?? '';$realPath = validatePath($targetPath);if (!$realPath || !is_dir($realPath)) {throw new Exception('無效目標目錄');}if (empty($_FILES['file'])) {throw new Exception('未選擇上傳文件');}$file = $_FILES['file'];$filename = preg_replace('/[^a-zA-Z0-9_.-]/', '', basename($file['name']));$targetFile = $realPath . DIRECTORY_SEPARATOR . $filename;$ext = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));// 文件類型驗證if (!in_array($ext, $ALLOWED_EXTENSIONS)) {throw new Exception('不允許的文件類型: ' . $ext);}// 防止文件覆蓋if (file_exists($targetFile)) {$filename = time() . '_' . $filename;$targetFile = $realPath . DIRECTORY_SEPARATOR . $filename;}if (!move_uploaded_file($file['tmp_name'], $targetFile)) {throw new Exception('文件上傳失敗');}$response = ['status' => 'success', 'filename' => $filename, 'message' => '文件上傳成功'];break;case 'delete-file':$path = $request['path'] ?? '';$realPath = validatePath($path);if (!$realPath || !is_file($realPath)) {throw new Exception('無效文件路徑');}if (!unlink($realPath)) {throw new Exception('文件刪除失敗');}$response = ['status' => 'success', 'message' => '文件已刪除'];break;default:throw new Exception('未知操作: ' . $action);}} catch (Exception $e) {$response = ['status' => 'error', 'message' => $e->getMessage()];
}echo json_encode($response);
?>
測試頁面
創建file_manager_test.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>:root {--primary: #3498db;--success: #2ecc71;--danger: #e74c3c;--dark: #34495e;--light: #f8f9fa;}* {box-sizing: border-box;margin: 0;padding: 0;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;}body {background-color: #f5f7fa;color: #333;line-height: 1.6;padding: 20px;}.container {max-width: 1200px;margin: 0 auto;background: white;border-radius: 10px;box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);padding: 30px;}header {text-align: center;margin-bottom: 30px;padding-bottom: 20px;border-bottom: 1px solid #eee;}h1 {color: var(--dark);margin-bottom: 10px;}.subtitle {color: #7f8c8d;font-weight: 400;}.section {margin-bottom: 30px;padding: 25px;border: 1px solid #e1e4e8;border-radius: 8px;background: var(--light);transition: all 0.3s ease;}.section:hover {box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);border-color: #d1d9e0;}.section h2 {color: var(--dark);margin-top: 0;margin-bottom: 20px;padding-bottom: 10px;border-bottom: 1px dashed #ddd;display: flex;align-items: center;}.section h2 i {margin-right: 10px;color: var(--primary);}.form-group {margin-bottom: 20px;display: flex;flex-wrap: wrap;align-items: center;}label {display: inline-block;width: 150px;font-weight: 600;color: #555;}input[type="text"],input[type="file"] {width: calc(100% - 160px);padding: 12px 15px;border: 1px solid #ddd;border-radius: 4px;font-size: 16px;transition: border 0.3s;}input[type="text"]:focus,input[type="file"]:focus {border-color: var(--primary);outline: none;box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);}button {padding: 12px 25px;background: var(--primary);color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 16px;font-weight: 600;transition: all 0.3s;margin-top: 10px;}button:hover {background: #2980b9;transform: translateY(-2px);}.delete-btn {background: var(--danger);}.delete-btn:hover {background: #c0392b;}.radio-group {display: flex;gap: 20px;margin-left: 150px;width: calc(100% - 150px);}.radio-group label {width: auto;display: flex;align-items: center;gap: 5px;font-weight: normal;}.response {margin-top: 20px;padding: 15px;border-radius: 4px;display: none;}.success {background-color: rgba(46, 204, 113, 0.1);border: 1px solid var(--success);color: #27ae60;display: block;}.error {background-color: rgba(231, 76, 60, 0.1);border: 1px solid var(--danger);color: #c0392b;display: block;}pre {background: #2d2d2d;color: #f8f8f2;padding: 15px;border-radius: 4px;max-height: 400px;overflow: auto;margin-top: 15px;font-family: 'Consolas', monospace;}@media (max-width: 768px) {.form-group {flex-direction: column;align-items: flex-start;}label {width: 100%;margin-bottom: 8px;}input[type="text"],input[type="file"] {width: 100%;}.radio-group {margin-left: 0;width: 100%;}}</style>
</head>
<body><div class="container"><header><h1>服務器文件管理器測試</h1><p class="subtitle">通過PHP與Apache實現安全的服務器端文件操作</p></header><!-- 列出目錄內容 --><div class="section"><h2><i class="fas fa-folder-open"></i> 列出目錄內容</h2><div class="form-group"><label for="listPath">目錄路徑:</label><input type="text" id="listPath" placeholder="例如: docs/images (留空顯示根目錄)"></div><button onclick="listDirectory()">列出目錄</button><div class="response"><pre id="listResult">目錄內容將顯示在這里...</pre></div></div><!-- 創建文件夾 --><div class="section"><h2><i class="fas fa-folder-plus"></i> 創建文件夾</h2><div class="form-group"><label for="createPath">父目錄路徑:</label><input type="text" id="createPath" placeholder="例如: docs"></div><div class="form-group"><label for="folderName">文件夾名稱:</label><input type="text" id="folderName" placeholder="例如: new_folder"></div><button onclick="createFolder()">創建文件夾</button><div class="response" id="createResponse"></div></div><!-- 刪除文件夾 --><div class="section"><h2><i class="fas fa-trash-alt"></i> 刪除文件夾</h2><div class="form-group"><label for="deleteFolderPath">文件夾路徑:</label><input type="text" id="deleteFolderPath" placeholder="例如: docs/old_folder"></div><button class="delete-btn" onclick="deleteFolder()">刪除文件夾</button><div class="response" id="deleteFolderResponse"></div></div><!-- 重命名文件/文件夾 --><div class="section"><h2><i class="fas fa-i-cursor"></i> 重命名項目</h2><div class="form-group"><label for="renamePath">當前路徑:</label><input type="text" id="renamePath" placeholder="例如: docs/image.jpg"></div><div class="form-group"><label for="newName">新名稱:</label><input type="text" id="newName" placeholder="例如: new_image.jpg"></div><div class="form-group"><label>類型:</label><div class="radio-group"><label><input type="radio" name="renameType" value="file" checked> 文件</label><label><input type="radio" name="renameType" value="folder"> 文件夾</label></div></div><button onclick="renameItem()">重命名</button><div class="response" id="renameResponse"></div></div><!-- 上傳文件 --><div class="section"><h2><i class="fas fa-upload"></i> 上傳文件</h2><div class="form-group"><label for="uploadPath">目標目錄:</label><input type="text" id="uploadPath" placeholder="例如: docs/uploads"></div><div class="form-group"><label for="fileInput">選擇文件:</label><input type="file" id="fileInput"></div><button onclick="uploadFile()">上傳文件</button><div class="response" id="uploadResponse"></div></div><!-- 刪除文件 --><div class="section"><h2><i class="fas fa-trash-alt"></i> 刪除文件</h2><div class="form-group"><label for="deleteFilePath">文件路徑:</label><input type="text" id="deleteFilePath" placeholder="例如: docs/file.txt"></div><button class="delete-btn" onclick="deleteFile()">刪除文件</button><div class="response" id="deleteFileResponse"></div></div></div><!-- Font Awesome 圖標 --><script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script><script>const API_URL = 'file_manager.php';// 顯示響應消息function showResponse(element, message, isSuccess = true) {element.textContent = message;element.className = 'response ' + (isSuccess ? 'success' : 'error');element.style.display = 'block';// 3秒后淡出setTimeout(() => {element.style.opacity = '1';let opacity = 1;const fadeOut = setInterval(() => {opacity -= 0.05;element.style.opacity = opacity;if (opacity <= 0) {clearInterval(fadeOut);element.style.display = 'none';}}, 50);}, 3000);}// 調用APIasync function callApi(data, files) {const formData = new FormData();// 添加表單數據for (const key in data) {formData.append(key, data[key]);}// 添加文件if (files) {for (const file of files) {formData.append('file', file);}}try {const response = await fetch(API_URL, {method: 'POST',body: formData});return await response.json();} catch (error) {return {status: 'error',message: '網絡錯誤: ' + error.message};}}// 列出目錄內容async function listDirectory() {const path = document.getElementById('listPath').value || '';const result = document.getElementById('listResult');result.textContent = '加載中...';const response = await callApi({action: 'list',path: path});if (response.status === 'success') {result.textContent = JSON.stringify(response.data, null, 2);} else {result.textContent = '錯誤: ' + response.message;}}// 創建文件夾async function createFolder() {const path = document.getElementById('createPath').value || '';const name = document.getElementById('folderName').value;const responseEl = document.getElementById('createResponse');if (!name) {showResponse(responseEl, '文件夾名稱不能為空', false);return;}const response = await callApi({action: 'create-folder',path: path,name: name});showResponse(responseEl, response.status === 'success' ? '? ' + response.message : '? 錯誤: ' + response.message,response.status === 'success');}// 刪除文件夾async function deleteFolder() {const path = document.getElementById('deleteFolderPath').value;const responseEl = document.getElementById('deleteFolderResponse');if (!path) {showResponse(responseEl, '文件夾路徑不能為空', false);return;}if (!confirm(`確定要刪除文件夾 "${path}" 及其所有內容嗎?`)) {return;}const response = await callApi({action: 'delete-folder',path: path});showResponse(responseEl, response.status === 'success' ? '? ' + response.message : '? 錯誤: ' + response.message,response.status === 'success');}// 重命名項目async function renameItem() {const path = document.getElementById('renamePath').value;const newName = document.getElementById('newName').value;const type = document.querySelector('input[name="renameType"]:checked').value;const responseEl = document.getElementById('renameResponse');if (!path || !newName) {showResponse(responseEl, '路徑和新名稱不能為空', false);return;}const response = await callApi({action: 'rename',path: path,newName: newName,type: type});showResponse(responseEl, response.status === 'success' ? '? ' + response.message : '? 錯誤: ' + response.message,response.status === 'success');}// 上傳文件async function uploadFile() {const path = document.getElementById('uploadPath').value || '';const fileInput = document.getElementById('fileInput');const responseEl = document.getElementById('uploadResponse');if (!fileInput.files || fileInput.files.length === 0) {showResponse(responseEl, '請選擇要上傳的文件', false);return;}const response = await callApi({action: 'upload-file',path: path}, [fileInput.files[0]]);showResponse(responseEl, response.status === 'success' ? `? ${response.message} - 文件名: ${response.filename}` : '? 錯誤: ' + response.message,response.status === 'success');}// 刪除文件async function deleteFile() {const path = document.getElementById('deleteFilePath').value;const responseEl = document.getElementById('deleteFileResponse');if (!path) {showResponse(responseEl, '文件路徑不能為空', false);return;}if (!confirm(`確定要刪除文件 "${path}" 嗎?`)) {return;}const response = await callApi({action: 'delete-file',path: path});showResponse(responseEl, response.status === 'success' ? '? ' + response.message : '? 錯誤: ' + response.message,response.status === 'success');}</script>
</body>
</html>
關鍵注意事項
-
安全配置
-
生產環境中移除
Access-Control-Allow-Origin: *
-
添加身份驗證(如Basic Auth或API密鑰)
-
限制允許的文件擴展名
-
設置文件上傳大小限制(在php.ini中配置)
-
-
權限設置
-
Windows:為
Resources
目錄添加Apache用戶(如SYSTEM
)的完全控制權限 -
Linux:
chown -R www-data:www-data Resources && chmod -R 755 Resources
-
-
路徑說明
-
列出Resources根目錄:輸入框留空
-
列出子目錄:直接輸入子目錄名(如
uploads
) -
不要使用前導斜杠(如
/uploads
)
-
測試流程
-
創建Resources目錄并設置權限
-
上傳
file_manager.php
和file_manager_test.html
到Apache文檔根目錄 -
訪問
http://localhost/file_manager_test.html
-
測試各項功能:
-
列出目錄(留空)
-
創建測試文件夾
-
上傳文件
-
重命名/刪除操作
-
補充:
由于前面的php提供的目錄樹形式過于扁平,這里提供一個返回樹形結構的目錄的json的php文件。如果要基于這個php文件進行測試,在測試文件file-manager-test.html里面 action:'list'的地方改成action:'get-tree'就可以了。
改進后的file_manager.php:
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');// 配置安全選項
$BASE_DIR = realpath(__DIR__ . '/Resources');
$ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'glb', 'gltf', 'fbx', 'obj', 'txt', 'md'];// 遞歸獲取目錄結構
function getDirectoryTree($path) {$realPath = realpath($path);if (!$realPath || !is_dir($realPath)) {return null;}$result = ['name' => basename($realPath),'path' => str_replace($GLOBALS['BASE_DIR'], '', $realPath) ?: '/','type' => 'directory','children' => []];$items = scandir($realPath);foreach ($items as $item) {if ($item === '.' || $item === '..') continue;$itemPath = $realPath . DIRECTORY_SEPARATOR . $item;if (is_dir($itemPath)) {$result['children'][] = getDirectoryTree($itemPath);} else {$ext = strtolower(pathinfo($itemPath, PATHINFO_EXTENSION));$result['children'][] = ['name' => $item,'path' => str_replace($GLOBALS['BASE_DIR'], '', $itemPath),'type' => 'file','size' => filesize($itemPath),'modified' => filemtime($itemPath),'extension' => $ext];}}return $result;
}// 處理請求
$response = ['status' => 'error', 'message' => 'Invalid request'];
$request = json_decode(file_get_contents('php://input'), true) ?? $_REQUEST;try {if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {exit(0);}if (!isset($request['action'])) {throw new Exception('No action specified');}$action = $request['action'];switch ($action) {case 'get-tree':$tree = getDirectoryTree($BASE_DIR);if ($tree) {$response = ['status' => 'success', 'data' => $tree];} else {throw new Exception('Failed to load directory tree');}break;case 'get-files':$path = $request['path'] ?? '/';$realPath = realpath($BASE_DIR . $path);if (!$realPath || !is_dir($realPath)) {throw new Exception('Invalid directory path');}$files = [];$items = scandir($realPath);foreach ($items as $item) {if ($item === '.' || $item === '..') continue;$itemPath = $realPath . DIRECTORY_SEPARATOR . $item;if (!is_dir($itemPath)) {$ext = strtolower(pathinfo($itemPath, PATHINFO_EXTENSION));$files[] = ['name' => $item,'path' => $path . '/' . $item,'type' => 'file','size' => filesize($itemPath),'modified' => filemtime($itemPath),'extension' => $ext];}}$response = ['status' => 'success', 'files' => $files];break;case 'create-folder':$path = $request['path'] ?? '';$name = $request['name'] ?? '';if (empty($name)) throw new Exception('Folder name is required');$realPath = validatePath($path);if (!$realPath) throw new Exception('Invalid path');$newFolder = $realPath . DIRECTORY_SEPARATOR . preg_replace('/[^a-zA-Z0-9_-]/', '', $name);if (file_exists($newFolder)) {throw new Exception('Folder already exists');}if (!mkdir($newFolder)) {throw new Exception('Failed to create folder');}$response = ['status' => 'success', 'message' => 'Folder created'];break;case 'delete-folder':$path = $request['path'] ?? '';$realPath = validatePath($path);if (!$realPath || !is_dir($realPath)) {throw new Exception('Invalid directory path');}// 安全措施:防止刪除根目錄if ($realPath === $BASE_DIR) {throw new Exception('Cannot delete base directory');}// 遞歸刪除目錄$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realPath, FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST);foreach ($files as $file) {if ($file->isDir()) {rmdir($file->getRealPath());} else {unlink($file->getRealPath());}}if (!rmdir($realPath)) {throw new Exception('Failed to delete folder');}$response = ['status' => 'success', 'message' => 'Folder deleted'];break;case 'rename':$type = $request['type'] ?? ''; // 'file' or 'folder'$path = $request['path'] ?? '';$newName = $request['newName'] ?? '';if (empty($newName)) throw new Exception('New name is required');$realPath = validatePath($path);if (!$realPath) throw new Exception('Invalid path');$newPath = dirname($realPath) . DIRECTORY_SEPARATOR . preg_replace('/[^a-zA-Z0-9_.-]/', '', $newName);if (file_exists($newPath)) {throw new Exception('Target name already exists');}if (!rename($realPath, $newPath)) {throw new Exception('Rename failed');}$response = ['status' => 'success', 'message' => 'Renamed successfully'];break;case 'upload-file':$targetPath = $request['path'] ?? '';$realPath = validatePath($targetPath);if (!$realPath || !is_dir($realPath)) {throw new Exception('Invalid target directory');}if (empty($_FILES['file'])) {throw new Exception('No file uploaded');}$file = $_FILES['file'];$filename = preg_replace('/[^a-zA-Z0-9_.-]/', '', basename($file['name']));$targetFile = $realPath . DIRECTORY_SEPARATOR . $filename;$ext = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));// 驗證文件類型if (!in_array($ext, $ALLOWED_EXTENSIONS)) {throw new Exception('File type not allowed');}// 防止覆蓋現有文件if (file_exists($targetFile)) {$filename = time() . '_' . $filename;$targetFile = $realPath . DIRECTORY_SEPARATOR . $filename;}if (!move_uploaded_file($file['tmp_name'], $targetFile)) {throw new Exception('File upload failed');}$response = ['status' => 'success', 'filename' => $filename, 'message' => 'File uploaded'];break;case 'delete-file':$path = $request['path'] ?? '';$realPath = validatePath($path);if (!$realPath || !is_file($realPath)) {throw new Exception('Invalid file path');}if (!unlink($realPath)) {throw new Exception('File deletion failed');}$response = ['status' => 'success', 'message' => 'File deleted'];break;default:throw new Exception('Unknown action');}} catch (Exception $e) {$response = ['status' => 'error', 'message' => $e->getMessage()];
}echo json_encode($response);
?>