參考 elFinder 架構,封裝一個輕量級的檔案操作工具,支援多檔案批量複製/移動、目錄遞迴複製、存在性檢查、自我保護 (防止移動到自身子目錄) 與日誌記錄。適合整合至後台管理系統的檔案管理模組。
class FileOperator {
private $baseDir; // 專案根目錄 (預設為腳本所在目錄)
private $logFile; // 日誌檔案路徑
public function __construct($baseDir = null, $logFile = 'file_operator.log') {
$this->baseDir = $baseDir ?: dirname(__FILE__);
$this->logFile = $this->baseDir . DIRECTORY_SEPARATOR . $logFile;
}
/** 公開介面:批量複製或移動
* @param array $sources 來源檔案/目錄相對路徑陣列
* @param string $destDir 目的目錄相對路徑
* @param string $op 'copy' 或 'move'
* @return array ['success' => bool, 'message' => string, 'details' => array]
*/
public function copyMove(array $sources, $destDir, $op = 'move') {
$destAbs = $this->resolvePath($destDir);
if (!is_dir($destAbs)) {
if (!mkdir($destAbs, 0755, true)) {
return $this->fail("無法建立目的目錄: $destDir");
}
}
$results = [];
foreach ($sources as $srcRel) {
$srcAbs = $this->resolvePath($srcRel);
if (!file_exists($srcAbs)) {
$results[$srcRel] = '來源不存在';
continue;
}
// 防止移動到自身或子目錄內
if ($op === 'move' && strpos($destAbs, $srcAbs) === 0) {
$results[$srcRel] = '不可移動到自身或子目錄';
continue;
}
$targetAbs = $destAbs . DIRECTORY_SEPARATOR . basename($srcAbs);
if (file_exists($targetAbs)) {
$results[$srcRel] = '目標已存在同名檔案/目錄';
continue;
}
$ok = ($op === 'move')
? @rename($srcAbs, $targetAbs)
: $this->deepCopy($srcAbs, $targetAbs);
$results[$srcRel] = $ok ? '成功' : '操作失敗 (權限或磁碟空間不足?)';
}
$this->log("Batch $op", $results);
return ['success' => true, 'message' => '批次處理完成', 'details' => $results];
}
/** 遞迴深度複製 */
private function deepCopy($src, $dst) {
if (is_file($src)) {
return @copy($src, $dst);
}
if (!@mkdir($dst, 0755, true)) return false;
$items = scandir($src);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
if (!$this->deepCopy("$src/$item", "$dst/$item")) return false;
}
return true;
}
/** 解析相對路徑為絕對路徑,防止目錄遍歷攻擊 */
private function resolvePath($rel) {
$abs = realpath($this->baseDir . DIRECTORY_SEPARATOR . $rel);
if ($abs === false || strpos($abs, realpath($this->baseDir)) !== 0) {
throw new InvalidArgumentException("非法路徑: $rel");
}
return $abs;
}
private function log($action, $details) {
$msg = date('Y-m-d H:i:s') . " [$action] " . json_encode($details, JSON_UNESCAPED_UNICODE) . PHP_EOL;
file_put_contents($this->logFile, $msg, FILE_APPEND | LOCK_EX);
}
private function fail($msg) {
$this->log('ERROR', $msg);
return ['success' => false, 'message' => $msg, 'details' => []];
}
}
$operator = new FileOperator('/var/www/project');
// 複製多個檔案/目錄到 backup 資料夾
$src = ['public/uploads/avatar.jpg', 'storage/logs', 'config/app.php'];
$res = $operator->copyMove($src, 'backup/2024-01-15', 'copy');
print_r($res);
// 移動單一檔案
$res = $operator->copyMove(['tmp/old_report.pdf'], 'archive/2023', 'move');
print_r($res);
resolvePath() 使用 realpath() 確保操作限制在專案根目錄下,防止 ../../etc/passwd 類攻擊。symlink 或系統指令 cp -r / rsync (透過 exec()),速度快數量級。