引言
作为前端开发者,你可能经常需要与服务器文件系统交互。本文将详细介绍如何通过PHP配合Apache实现服务器端文件管理功能。即使你没有任何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
),目录结构应包含:bashphp.exe php8apache2_4.dll php.ini-development ext/ (扩展目录)
Apache配置PHP
编辑conf/httpd.conf
文件:
bash
# 加载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>
验证配置
bash
cd D:\Apache24\bin
httpd -t
看到"Syntax OK"表示配置正确,重启Apache服务:
bash
httpd -k restart
文件管理API实现
创建file_manager.php
文件:
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
:
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);
}
// 调用API
async 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
<?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);
?>