PHP unlink() 函数

定义和用法

unlink() 函数用于删除文件。这个函数对应于Unix/Linux系统中的unlink命令和Windows系统中的del命令。

rmdir()函数不同,unlink()只能删除文件,不能删除目录。要删除目录,需要使用rmdir()函数。

注意: unlink() 函数删除的文件无法恢复(除非有备份)。请谨慎使用此函数。

语法

unlink ( string $filename [, resource $context ] ) : bool

参数说明:

  • $filename:要删除的文件的路径(必需)
  • $context:上下文资源,用于指定流的上下文参数(可选)

参数详解

参数 描述
filename

要删除的文件的路径。必需参数。

  • 可以是相对路径或绝对路径
  • 必须指向一个存在的文件
  • 不能是目录(使用rmdir()删除目录)
  • PHP进程必须有该文件的删除权限
context

上下文资源。可选参数。

  • 用于指定流的上下文参数
  • 通常通过stream_context_create()创建
  • 在大多数情况下不需要使用

返回值

  • 成功时:返回 true
  • 失败时:返回 false,通常是由于以下原因:
    • 文件不存在
    • 没有删除权限
    • 文件正在被其他进程使用
    • 文件是只读的
    • 路径是一个目录而不是文件

示例

示例 1:删除单个文件

删除一个文件并检查结果:

<?php
$filename = 'old_file.txt';

if (file_exists($filename)) {
    if (unlink($filename)) {
        echo "文件删除成功: $filename";
    } else {
        echo "文件删除失败: $filename";

        // 获取错误信息
        $error = error_get_last();
        if ($error) {
            echo " - " . $error['message'];
        }
    }
} else {
    echo "文件不存在: $filename";
}
?>
示例 2:安全删除文件

安全地删除文件,包含更多验证:

<?php
function safeUnlink($filename) {
    // 验证输入
    if (!is_string($filename) || empty($filename)) {
        return ['success' => false, 'error' => '无效的文件名'];
    }

    // 检查文件是否存在
    if (!file_exists($filename)) {
        return ['success' => false, 'error' => '文件不存在'];
    }

    // 检查是否是文件(不是目录)
    if (!is_file($filename)) {
        return ['success' => false, 'error' => '路径不是文件'];
    }

    // 检查文件是否可写
    if (!is_writable($filename)) {
        return ['success' => false, 'error' => '文件不可写'];
    }

    // 尝试删除文件
    if (unlink($filename)) {
        // 验证文件是否真的被删除
        clearstatcache(true, $filename);
        if (!file_exists($filename)) {
            return ['success' => true, 'error' => null];
        } else {
            return ['success' => false, 'error' => '文件删除失败(可能仍在缓存中)'];
        }
    } else {
        $error = error_get_last();
        return ['success' => false, 'error' => $error['message'] ?? '未知错误'];
    }
}

// 使用示例
$result = safeUnlink('data.txt');
if ($result['success']) {
    echo "文件安全删除成功";
} else {
    echo "删除失败: " . $result['error'];
}
?>
示例 3:批量删除文件

批量删除匹配特定模式的文件:

<?php
function deleteFilesByPattern($directory, $pattern = '*') {
    if (!is_dir($directory) || !is_readable($directory)) {
        return ['success' => false, 'error' => '目录不可访问'];
    }

    $files = glob($directory . '/' . $pattern);
    $results = [
        'total' => count($files),
        'deleted' => 0,
        'failed' => 0,
        'errors' => []
    ];

    foreach ($files as $file) {
        // 跳过目录
        if (is_dir($file)) {
            continue;
        }

        if (unlink($file)) {
            $results['deleted']++;
        } else {
            $results['failed']++;
            $error = error_get_last();
            $results['errors'][$file] = $error['message'] ?? '未知错误';
        }
    }

    return $results;
}

// 使用示例:删除所有.txt文件
$result = deleteFilesByPattern('/tmp', '*.txt');
echo "批量删除结果:<br>";
echo "找到文件: {$result['total']} 个<br>";
echo "成功删除: {$result['deleted']} 个<br>";
echo "删除失败: {$result['failed']} 个<br>";

if (!empty($result['errors'])) {
    echo "<br>错误详情:<br>";
    foreach ($result['errors'] as $file => $error) {
        echo "- {$file}: {$error}<br>";
    }
}
?>
示例 4:删除旧文件

删除超过指定时间的旧文件:

<?php
function deleteOldFiles($directory, $maxAgeDays = 30) {
    if (!is_dir($directory)) {
        return false;
    }

    $maxAge = $maxAgeDays * 24 * 60 * 60; // 转换为秒
    $now = time();
    $deleted = 0;

    $files = scandir($directory);

    foreach ($files as $file) {
        if ($file === '.' || $file === '..') {
            continue;
        }

        $filepath = $directory . '/' . $file;

        // 只处理文件
        if (!is_file($filepath)) {
            continue;
        }

        // 获取文件修改时间
        $filemtime = filemtime($filepath);
        $fileAge = $now - $filemtime;

        // 如果文件太旧,删除它
        if ($fileAge > $maxAge) {
            if (unlink($filepath)) {
                $deleted++;
                echo "删除旧文件: {$file} (年龄: " . round($fileAge / 86400, 1) . " 天)<br>";
            }
        }
    }

    return $deleted;
}

// 使用示例:删除超过7天的文件
$deletedCount = deleteOldFiles('/var/log/myapp', 7);
echo "共删除了 {$deletedCount} 个旧文件";
?>
示例 5:使用上下文删除文件

使用上下文资源删除文件(虽然不常用):

<?php
// 创建上下文(虽然unlink()通常不需要特殊的上下文)
$context = stream_context_create([
    'http' => [
        'method' => 'GET',
        'header' => "User-Agent: PHP\r\n"
    ]
]);

$filename = 'temp_file.txt';

// 先创建测试文件
file_put_contents($filename, '测试内容');

// 使用上下文删除文件
if (unlink($filename, $context)) {
    echo "使用上下文删除文件成功";
} else {
    echo "删除失败";

    // 如果失败,尝试不使用上下文删除
    if (file_exists($filename)) {
        unlink($filename);
    }
}
?>

安全考虑

安全警告

unlink() 函数可能被滥用,特别是当文件名来自用户输入时。需要特别注意以下安全问题:

1. 路径遍历攻击(Directory Traversal)

防止用户输入导致删除系统文件:

<?php
// 不安全的做法
$user_input = $_GET['file']; // 用户可能输入 '../../etc/passwd'
unlink($user_input); // 危险!可能删除系统文件

// 安全的做法
function safeDeleteUserFile($user_input, $allowed_dir) {
    // 获取规范化路径
    $real_path = realpath($allowed_dir . '/' . basename($user_input));

    // 验证路径是否在允许的目录内
    if (strpos($real_path, realpath($allowed_dir)) !== 0) {
        return false; // 路径不在允许的目录内
    }

    // 验证是否是文件且存在
    if (!is_file($real_path)) {
        return false;
    }

    return unlink($real_path);
}

// 使用示例
$result = safeDeleteUserFile($_GET['file'], '/var/www/uploads');
?>
2. 符号链接攻击

防止通过符号链接删除敏感文件:

<?php
function safeUnlinkWithSymlinkCheck($filename) {
    // 检查是否是符号链接
    if (is_link($filename)) {
        // 如果是符号链接,获取实际路径
        $realpath = realpath($filename);

        // 验证实际路径是否在安全范围内
        $safe_dirs = ['/var/www/uploads', '/tmp/myapp'];
        $is_safe = false;

        foreach ($safe_dirs as $safe_dir) {
            if (strpos($realpath, $safe_dir) === 0) {
                $is_safe = true;
                break;
            }
        }

        if (!$is_safe) {
            return false;
        }
    }

    return unlink($filename);
}
?>
3. 权限检查

在执行删除前检查适当权限:

<?php
function checkDeletePermission($filename) {
    // 检查文件是否存在
    if (!file_exists($filename)) {
        return false;
    }

    // 检查是否是文件
    if (!is_file($filename)) {
        return false;
    }

    // 检查当前用户是否有权限
    $perms = fileperms($filename);

    // 如果是当前用户拥有的文件
    if (fileowner($filename) === getmyuid()) {
        // 检查所有者写权限
        return ($perms & 0x0080) !== 0; // 所有者写权限位
    }

    // 如果是当前用户所在组的文件
    if (filegroup($filename) === getmygid()) {
        // 检查组写权限
        return ($perms & 0x0010) !== 0; // 组写权限位
    }

    // 其他情况:检查其他用户写权限
    return ($perms & 0x0002) !== 0; // 其他用户写权限位
}
?>

错误处理

1. 基本的错误处理
<?php
$filename = 'test.txt';

// 方法1:使用错误控制运算符和错误获取函数
if (@unlink($filename)) {
    echo "删除成功";
} else {
    $error = error_get_last();
    if ($error) {
        echo "删除失败: " . $error['message'];
    }
}

// 方法2:使用try-catch(PHP 7.0+)
try {
    if (!unlink($filename)) {
        throw new Exception("无法删除文件: $filename");
    }
    echo "删除成功";
} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
}
?>
2. 常见的错误和解决方案
错误情况 可能的原因 解决方案
权限被拒绝 文件只读、没有写权限、被其他进程锁定 检查文件权限,确保文件未被占用
文件不存在 文件已被删除、路径错误、符号链接断裂 使用file_exists()检查文件是否存在
路径是目录 尝试使用unlink()删除目录 使用is_file()验证,使用rmdir()删除目录
磁盘I/O错误 磁盘故障、文件系统错误、磁盘满 检查磁盘状态和可用空间
3. 完整的错误处理函数
<?php
function unlinkWithDetailedError($filename) {
    // 清除状态缓存
    clearstatcache(true, $filename);

    // 检查文件是否存在
    if (!file_exists($filename)) {
        return ['success' => false, 'error' => '文件不存在', 'code' => 'ENOENT'];
    }

    // 检查是否是文件
    if (!is_file($filename)) {
        if (is_dir($filename)) {
            return ['success' => false, 'error' => '路径是目录,请使用rmdir()', 'code' => 'EISDIR'];
        }
        return ['success' => false, 'error' => '路径不是普通文件', 'code' => 'ENOTFILE'];
    }

    // 尝试删除
    if (unlink($filename)) {
        // 验证删除成功
        clearstatcache(true, $filename);
        if (!file_exists($filename)) {
            return ['success' => true, 'error' => null, 'code' => null];
        } else {
            return ['success' => false, 'error' => '文件删除后仍然存在(可能是缓存问题)', 'code' => 'ESTALE'];
        }
    } else {
        // 获取系统错误
        $error = error_get_last();
        $error_msg = $error['message'] ?? '未知错误';

        // 根据错误消息判断错误类型
        $error_code = 'EUNKNOWN';
        if (strpos($error_msg, 'Permission denied') !== false) {
            $error_code = 'EACCES';
        } elseif (strpos($error_msg, 'No such file') !== false) {
            $error_code = 'ENOENT';
        }

        return ['success' => false, 'error' => $error_msg, 'code' => $error_code];
    }
}

// 使用示例
$result = unlinkWithDetailedError('/path/to/file.txt');
if ($result['success']) {
    echo "删除成功";
} else {
    echo "删除失败 [{$result['code']}]: {$result['error']}";
}
?>

实际应用场景

场景1:临时文件清理

自动清理应用程序生成的临时文件:

<?php
class TempFileCleaner {
    private $tempDir;
    private $patterns;

    public function __construct($tempDir = null) {
        $this->tempDir = $tempDir ?: sys_get_temp_dir();
        $this->patterns = ['*.tmp', '*.temp', 'cache_*', 'temp_*'];
    }

    public function cleanup($maxAgeHours = 24) {
        if (!is_dir($this->tempDir) || !is_readable($this->tempDir)) {
            return ['success' => false, 'error' => '临时目录不可访问'];
        }

        $maxAge = $maxAgeHours * 3600;
        $now = time();
        $stats = ['deleted' => 0, 'failed' => 0, 'skipped' => 0];

        foreach ($this->patterns as $pattern) {
            $files = glob($this->tempDir . '/' . $pattern);

            foreach ($files as $file) {
                // 跳过目录
                if (is_dir($file)) {
                    $stats['skipped']++;
                    continue;
                }

                // 检查文件年龄
                $fileAge = $now - filemtime($file);

                if ($fileAge > $maxAge) {
                    if (unlink($file)) {
                        $stats['deleted']++;
                    } else {
                        $stats['failed']++;
                    }
                } else {
                    $stats['skipped']++;
                }
            }
        }

        return ['success' => true, 'stats' => $stats];
    }

    public function getDiskUsage() {
        $totalSize = 0;
        $fileCount = 0;

        foreach ($this->patterns as $pattern) {
            $files = glob($this->tempDir . '/' . $pattern);

            foreach ($files as $file) {
                if (is_file($file)) {
                    $totalSize += filesize($file);
                    $fileCount++;
                }
            }
        }

        return [
            'total_size' => $totalSize,
            'file_count' => $fileCount,
            'total_size_mb' => round($totalSize / 1024 / 1024, 2)
        ];
    }
}

// 使用示例
$cleaner = new TempFileCleaner();
$usage = $cleaner->getDiskUsage();
echo "临时文件使用情况: {$usage['file_count']} 个文件,{$usage['total_size_mb']} MB<br>";

$result = $cleaner->cleanup(12); // 清理超过12小时的文件
if ($result['success']) {
    $stats = $result['stats'];
    echo "清理完成: 删除 {$stats['deleted']} 个,失败 {$stats['failed']} 个,跳过 {$stats['skipped']} 个";
}
?>
场景2:文件上传处理

处理文件上传失败或完成后的清理:

<?php
class UploadHandler {
    private $uploadDir;
    private $maxFileSize;
    private $allowedTypes;

    public function __construct($uploadDir, $maxFileSize = 10485760, $allowedTypes = []) {
        $this->uploadDir = $uploadDir;
        $this->maxFileSize = $maxFileSize;
        $this->allowedTypes = $allowedTypes;

        if (!is_dir($uploadDir)) {
            mkdir($uploadDir, 0755, true);
        }
    }

    public function handleUpload($fileField) {
        if (!isset($_FILES[$fileField])) {
            return ['success' => false, 'error' => '没有上传文件'];
        }

        $file = $_FILES[$fileField];
        $tempPath = $file['tmp_name'];

        // 检查上传错误
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->cleanupTempFile($tempPath);
            return ['success' => false, 'error' => $this->getUploadError($file['error'])];
        }

        // 检查文件大小
        if ($file['size'] > $this->maxFileSize) {
            $this->cleanupTempFile($tempPath);
            return ['success' => false, 'error' => '文件太大'];
        }

        // 检查文件类型
        if (!empty($this->allowedTypes)) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $mimeType = finfo_file($finfo, $tempPath);
            finfo_close($finfo);

            if (!in_array($mimeType, $this->allowedTypes)) {
                $this->cleanupTempFile($tempPath);
                return ['success' => false, 'error' => '不允许的文件类型'];
            }
        }

        // 生成安全文件名
        $filename = $this->generateSafeFilename($file['name']);
        $destination = $this->uploadDir . '/' . $filename;

        // 移动文件
        if (move_uploaded_file($tempPath, $destination)) {
            return ['success' => true, 'filename' => $filename, 'path' => $destination];
        } else {
            $this->cleanupTempFile($tempPath);
            return ['success' => false, 'error' => '文件移动失败'];
        }
    }

    private function cleanupTempFile($tempPath) {
        if (file_exists($tempPath) && is_file($tempPath)) {
            @unlink($tempPath);
        }
    }

    private function getUploadError($errorCode) {
        $errors = [
            UPLOAD_ERR_INI_SIZE => '文件超过php.ini中upload_max_filesize限制',
            UPLOAD_ERR_FORM_SIZE => '文件超过表单MAX_FILE_SIZE限制',
            UPLOAD_ERR_PARTIAL => '文件只有部分被上传',
            UPLOAD_ERR_NO_FILE => '没有文件被上传',
            UPLOAD_ERR_NO_TMP_DIR => '找不到临时文件夹',
            UPLOAD_ERR_CANT_WRITE => '文件写入失败',
            UPLOAD_ERR_EXTENSION => 'PHP扩展阻止了文件上传'
        ];

        return $errors[$errorCode] ?? '未知上传错误';
    }

    private function generateSafeFilename($originalName) {
        $extension = pathinfo($originalName, PATHINFO_EXTENSION);
        $basename = pathinfo($originalName, PATHINFO_FILENAME);

        // 清理文件名
        $safeBasename = preg_replace('/[^a-zA-Z0-9_-]/', '_', $basename);
        $safeBasename = substr($safeBasename, 0, 100); // 限制长度

        return $safeBasename . '_' . uniqid() . '.' . $extension;
    }

    public function deleteUploadedFile($filename) {
        $filepath = $this->uploadDir . '/' . basename($filename);

        if (file_exists($filepath) && is_file($filepath)) {
            return unlink($filepath);
        }

        return false;
    }
}

// 使用示例
$uploader = new UploadHandler('/var/www/uploads', 5 * 1024 * 1024, ['image/jpeg', 'image/png']);
$result = $uploader->handleUpload('userfile');

if ($result['success']) {
    echo "上传成功: " . $result['filename'];
    // ... 处理上传的文件

    // 如果需要删除
    // $uploader->deleteUploadedFile($result['filename']);
} else {
    echo "上传失败: " . $result['error'];
}
?>