PHP 文件上传共享存储

wen PHP项目 4

本文目录导读:

PHP 文件上传共享存储

  1. 数据库结构
  2. 上传配置文件
  3. 文件上传类
  4. 数据库连接类
  5. 文件管理类
  6. 上传页面
  7. 上传处理脚本
  8. 下载处理脚本
  9. 辅助函数
  10. 页面提示功能
  11. 安全注意事项

我将为您设计一个PHP文件上传共享存储系统,这个系统支持多用户文件上传、浏览和下载功能。

数据库结构

-- 创建文件表
CREATE TABLE IF NOT EXISTS files (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    original_name VARCHAR(255) NOT NULL,
    stored_name VARCHAR(255) NOT NULL,
    file_path VARCHAR(500) NOT NULL,
    file_size INT NOT NULL,
    mime_type VARCHAR(100),
    category VARCHAR(50),
    description TEXT,
    downloads_count INT DEFAULT 0,
    is_public TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_category (category),
    INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户表(简化版,实际项目中通常已有)
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

上传配置文件

<?php
// config.php
define('UPLOAD_DIR', __DIR__ . '/uploads/');  // 上传目录
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar']);
define('MAX_FILE_SIZE', 50 * 1024 * 1024);  // 50MB
define('MAX_FILES_PER_UPLOAD', 10);
define('DATE_FORMAT', 'Y-m-d H:i:s');
// 创建上传目录(如果不存在)
if (!file_exists(UPLOAD_DIR)) {
    mkdir(UPLOAD_DIR, 0755, true);
    mkdir(UPLOAD_DIR . 'images/', 0755, true);
    mkdir(UPLOAD_DIR . 'documents/', 0755, true);
    mkdir(UPLOAD_DIR . 'archives/', 0755, true);
}

文件上传类

<?php
// FileUploader.php
class FileUploader {
    private $uploadDir;
    private $allowedExts;
    private $maxSize;
    public function __construct() {
        $this->uploadDir = UPLOAD_DIR;
        $this->allowedExts = ALLOWED_EXTENSIONS;
        $this->maxSize = MAX_FILE_SIZE;
    }
    /**
     * 上传单个文件
     */
    public function uploadFile($file, $userId, $category = 'general', $isPublic = true) {
        $result = [
            'success' => false,
            'message' => '',
            'file' => null
        ];
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $result['message'] = '上传失败,错误码: ' . $file['error'];
            return $result;
        }
        // 检查文件大小
        if ($file['size'] > $this->maxSize) {
            $result['message'] = '文件大小超过限制';
            return $result;
        }
        // 检查文件类型
        $fileInfo = pathinfo($file['name']);
        $extension = strtolower($fileInfo['extension']);
        if (!in_array($extension, $this->allowedExts)) {
            $result['message'] = '不支持的文件类型';
            return $result;
        }
        // 生成存储文件名
        $storedName = $this->generateStoredName($fileInfo['filename'], $extension);
        // 根据文件类型分配到子目录
        $subDir = $this->getSubDirectory($extension);
        $targetPath = $this->uploadDir . $subDir;
        // 确保目录存在
        if (!file_exists($targetPath)) {
            mkdir($targetPath, 0755, true);
        }
        $targetFile = $targetPath . '/' . $storedName;
        // 移动文件
        if (move_uploaded_file($file['tmp_name'], $targetFile)) {
            // 保存文件信息到数据库
            $fileData = [
                'user_id' => $userId,
                'original_name' => $file['name'],
                'stored_name' => $storedName,
                'file_path' => $subDir . '/' . $storedName,
                'file_size' => $file['size'],
                'mime_type' => $file['type'],
                'category' => $category,
                'is_public' => $isPublic ? 1 : 0
            ];
            $fileId = $this->saveFileInfo($fileData);
            if ($fileId) {
                $fileData['id'] = $fileId;
                $result['success'] = true;
                $result['file'] = $fileData;
                $result['message'] = '文件上传成功';
            } else {
                unlink($targetFile); // 删除已上传的文件
                $result['message'] = '保存文件信息失败';
            }
        } else {
            $result['message'] = '文件移动失败';
        }
        return $result;
    }
    /**
     * 批量上传文件
     */
    public function uploadMultipleFiles($files, $userId, $category = 'general') {
        $results = [];
        $count = count($files['name']);
        for ($i = 0; $i < $count; $i++) {
            $file = [
                'name' => $files['name'][$i],
                'type' => $files['type'][$i],
                'tmp_name' => $files['tmp_name'][$i],
                'error' => $files['error'][$i],
                'size' => $files['size'][$i]
            ];
            $results[] = $this->uploadFile($file, $userId, $category);
        }
        return $results;
    }
    /**
     * 生成存储文件名
     */
    private function generateStoredName($originalName, $extension) {
        $unique = uniqid() . '_' . substr(md5(uniqid(mt_rand(), true)), 0, 8);
        return $unique . '.' . $extension;
    }
    /**
     * 根据文件类型返回子目录
     */
    private function getSubDirectory($extension) {
        $imageExts = ['jpg', 'jpeg', 'png', 'gif'];
        $docExts = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
        $archiveExts = ['zip', 'rar'];
        if (in_array($extension, $imageExts)) {
            return 'images';
        } elseif (in_array($extension, $docExts)) {
            return 'documents';
        } elseif (in_array($extension, $archiveExts)) {
            return 'archives';
        } else {
            return 'others';
        }
    }
    /**
     * 保存文件信息到数据库
     */
    private function saveFileInfo($fileData) {
        $db = Database::getInstance()->getConnection();
        $sql = "INSERT INTO files (user_id, original_name, stored_name, file_path, file_size, mime_type, category, is_public) 
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
        $stmt = $db->prepare($sql);
        $stmt->bind_param("isssissi",
            $fileData['user_id'],
            $fileData['original_name'],
            $fileData['stored_name'],
            $fileData['file_path'],
            $fileData['file_size'],
            $fileData['mime_type'],
            $fileData['category'],
            $fileData['is_public']
        );
        if ($stmt->execute()) {
            return $db->insert_id;
        }
        return false;
    }
}

数据库连接类

<?php
// Database.php
class Database {
    private static $instance = null;
    private $connection;
    private $host = 'localhost';
    private $username = 'root';
    private $password = '';
    private $database = 'file_sharing';
    private function __construct() {
        $this->connection = new mysqli($this->host, $this->username, $this->password, $this->database);
        if ($this->connection->connect_error) {
            die("连接失败: " . $this->connection->connect_error);
        }
        $this->connection->set_charset("utf8mb4");
    }
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new Database();
        }
        return self::$instance;
    }
    public function getConnection() {
        return $this->connection;
    }
}

文件管理类

<?php
// FileManager.php
class FileManager {
    private $db;
    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }
    /**
     * 获取文件列表
     */
    public function getFiles($userId = null, $category = null, $search = '', $page = 1, $perPage = 20) {
        $offset = ($page - 1) * $perPage;
        $where = [];
        $params = [];
        $types = "";
        if ($userId !== null) {
            $where[] = "user_id = ?";
            $params[] = $userId;
            $types .= "i";
        }
        if ($category && $category !== 'all') {
            $where[] = "category = ?";
            $params[] = $category;
            $types .= "s";
        }
        if (!empty($search)) {
            $where[] = "original_name LIKE ?";
            $params[] = "%$search%";
            $types .= "s";
        }
        // 只显示公开文件或自己的文件
        $where[] = "(is_public = 1 OR user_id = ?)";
        $params[] = $_SESSION['user_id'] ?? 0;
        $types .= "i";
        $whereSQL = $where ? "WHERE " . implode(" AND ", $where) : "";
        $sql = "SELECT * FROM files $whereSQL ORDER BY created_at DESC LIMIT ? OFFSET ?";
        $params[] = $perPage;
        $params[] = $offset;
        $types .= "ii";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param($types, ...$params);
        $stmt->execute();
        $result = $stmt->get_result();
        $files = $result->fetch_all(MYSQLI_ASSOC);
        // 获取总数
        $countSQL = "SELECT COUNT(*) as total FROM files $whereSQL";
        $stmt = $this->db->prepare($countSQL);
        $countParams = array_slice($params, 0, count($params) - 2);
        $countTypes = substr($types, 0, -2);
        if ($countTypes) {
            $stmt->bind_param($countTypes, ...$countParams);
        }
        $stmt->execute();
        $countResult = $stmt->get_result()->fetch_assoc();
        return [
            'files' => $files,
            'total' => $countResult['total'],
            'page' => $page,
            'perPage' => $perPage,
            'totalPages' => ceil($countResult['total'] / $perPage)
        ];
    }
    /**
     * 获取单个文件信息
     */
    public function getFileById($fileId) {
        $sql = "SELECT * FROM files WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("i", $fileId);
        $stmt->execute();
        return $stmt->get_result()->fetch_assoc();
    }
    /**
     * 下载文件
     */
    public function downloadFile($fileId) {
        $file = $this->getFileById($fileId);
        if (!$file) {
            return ['success' => false, 'message' => '文件不存在'];
        }
        $filePath = UPLOAD_DIR . $file['file_path'];
        if (!file_exists($filePath)) {
            return ['success' => false, 'message' => '文件物理文件不存在'];
        }
        // 更新下载次数
        $this->incrementDownloadCount($fileId);
        // 配置下载响应
        header('Content-Description: File Transfer');
        header('Content-Type: ' . $file['mime_type']);
        header('Content-Disposition: attachment; filename="' . $file['original_name'] . '"');
        header('Content-Length: ' . filesize($filePath));
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        header('Expires: 0');
        readfile($filePath);
        return ['success' => true];
    }
    /**
     * 删除文件
     */
    public function deleteFile($fileId, $userId) {
        $file = $this->getFileById($fileId);
        if (!$file) {
            return ['success' => false, 'message' => '文件不存在'];
        }
        // 检查权限(只有文件所有者可以删除)
        if ($file['user_id'] != $userId) {
            return ['success' => false, 'message' => '没有删除权限'];
        }
        // 删除物理文件
        $filePath = UPLOAD_DIR . $file['file_path'];
        if (file_exists($filePath)) {
            unlink($filePath);
        }
        // 删除数据库记录
        $sql = "DELETE FROM files WHERE id = ? AND user_id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("ii", $fileId, $userId);
        if ($stmt->execute()) {
            return ['success' => true, 'message' => '文件删除成功'];
        }
        return ['success' => false, 'message' => '删除失败'];
    }
    /**
     * 更新文件信息
     */
    public function updateFileInfo($fileId, $userId, $data) {
        $allowedFields = ['category', 'description', 'is_public'];
        $updates = [];
        $params = [];
        $types = "";
        foreach ($data as $key => $value) {
            if (in_array($key, $allowedFields)) {
                $updates[] = "$key = ?";
                $params[] = $value;
                $types .= "s";
            }
        }
        if (empty($updates)) {
            return ['success' => false, 'message' => '没有要更新的字段'];
        }
        $params[] = $fileId;
        $params[] = $userId;
        $types .= "ii";
        $sql = "UPDATE files SET " . implode(", ", $updates) . " WHERE id = ? AND user_id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param($types, ...$params);
        if ($stmt->execute()) {
            return ['success' => true, 'message' => '更新成功'];
        }
        return ['success' => false, 'message' => '更新失败'];
    }
    /**
     * 增加下载次数
     */
    private function incrementDownloadCount($fileId) {
        $sql = "UPDATE files SET downloads_count = downloads_count + 1 WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("i", $fileId);
        $stmt->execute();
    }
    /**
     * 获取文件分类统计
     */
    public function getCategoryStats() {
        $sql = "SELECT category, COUNT(*) as count FROM files GROUP BY category";
        $result = $this->db->query($sql);
        return $result->fetch_all(MYSQLI_ASSOC);
    }
}

上传页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">文件上传中心</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background-color: white;
            padding: 20px;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        .upload-form {
            background: #f9f9f9;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .form-group input,
        .form-group select,
        .form-group textarea {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        .btn {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        .btn:hover {
            background-color: #45a049;
        }
        .file-list {
            margin-top: 20px;
        }
        .file-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 15px;
            margin-top: 20px;
        }
        .file-card {
            background-color: white;
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
        }
        .file-card h4 {
            margin: 0 0 10px 0;
            color: #333;
        }
        .file-card .meta {
            color: #666;
            font-size: 12px;
        }
        .file-card .actions {
            margin-top: 10px;
        }
        .file-card .actions a {
            margin-right: 10px;
            text-decoration: none;
            color: #2196F3;
        }
        .file-card .actions a:hover {
            text-decoration: underline;
        }
        .success {
            background-color: #d4edda;
            color: #155724;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
        .error {
            background-color: #f8d7da;
            color: #721c24;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>文件上传共享系统</h1>
        <?php if (!isset($_SESSION['user_id'])): ?>
            <div class="error">请先登录</div>
        <?php else: ?>
            <div class="upload-form">
                <h2>上传文件</h2>
                <form action="upload_handler.php" method="post" enctype="multipart/form-data">
                    <div class="form-group">
                        <label for="files">选择文件(可多选)</label>
                        <input type="file" id="files" name="files[]" multiple required>
                        <input type="hidden" name="MAX_FILE_SIZE" value="<?php echo MAX_FILE_SIZE; ?>">
                    </div>
                    <div class="form-group">
                        <label for="category">文件分类</label>
                        <select id="category" name="category">
                            <option value="general">综合</option>
                            <option value="images">图片</option>
                            <option value="documents">文档</option>
                            <option value="archives">压缩包</option>
                            <option value="videos">视频</option>
                            <option value="audio">音频</option>
                            <option value="other">其他</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="description">文件描述</label>
                        <textarea id="description" name="description" rows="3"></textarea>
                    </div>
                    <div class="form-group">
                        <label>
                            <input type="checkbox" name="is_public" value="1" checked>
                            公开共享
                        </label>
                    </div>
                    <button type="submit" class="btn">上传文件</button>
                </form>
            </div>
            <div class="file-list">
                <h2>文件列表</h2>
                <div class="file-grid">
                    <?php
                    $fileManager = new FileManager();
                    $files = $fileManager->getFiles(null, null, '', 1, 100);
                    foreach ($files['files'] as $file):
                    ?>
                    <div class="file-card">
                        <h4><?php echo htmlspecialchars($file['original_name']); ?></h4>
                        <div class="meta">
                            <p>大小: <?php echo formatFileSize($file['file_size']); ?></p>
                            <p>类型: <?php echo htmlspecialchars($file['mime_type']); ?></p>
                            <p>分类: <?php echo htmlspecialchars($file['category']); ?></p>
                            <p>上传时间: <?php echo date('Y-m-d H:i', strtotime($file['created_at'])); ?></p>
                            <p>下载次数: <?php echo $file['downloads_count']; ?></p>
                        </div>
                        <div class="actions">
                            <a href="download.php?id=<?php echo $file['id']; ?>">下载</a>
                            <?php if ($_SESSION['user_id'] == $file['user_id']): ?>
                                <a href="delete.php?id=<?php echo $file['id']; ?>" onclick="return confirm('确定要删除这个文件吗?')">删除</a>
                            <?php endif; ?>
                        </div>
                    </div>
                    <?php endforeach; ?>
                </div>
            </div>
        <?php endif; ?>
    </div>
</body>
</html>

上传处理脚本

<?php
// upload_handler.php
session_start();
require_once 'config.php';
require_once 'Database.php';
require_once 'FileUploader.php';
// 检查用户是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}
// 验证上传请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['files'])) {
    $uploader = new FileUploader();
    $category = $_POST['category'] ?? 'general';
    $isPublic = isset($_POST['is_public']) ? true : false;
    $results = $uploader->uploadMultipleFiles(
        $_FILES['files'],
        $_SESSION['user_id'],
        $category,
        $isPublic
    );
    // 处理结果
    $successCount = 0;
    $errorMessages = [];
    foreach ($results as $result) {
        if ($result['success']) {
            $successCount++;
        } else {
            $errorMessages[] = $result['message'];
        }
    }
    // 保存上传结果到会话
    $_SESSION['upload_messages'] = [
        'success_count' => $successCount,
        'errors' => $errorMessages
    ];
    header('Location: index.php');
    exit;
}

下载处理脚本

<?php
// download.php
session_start();
require_once 'config.php';
require_once 'Database.php';
require_once 'FileManager.php';
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}
$fileId = $_GET['id'] ?? null;
if ($fileId) {
    $fileManager = new FileManager();
    $result = $fileManager->downloadFile($fileId);
    // 如果返回了结果,说明文件不存在
    if (!$result['success']) {
        $_SESSION['error_message'] = $result['message'];
        header('Location: index.php');
        exit;
    }
} else {
    header('Location: index.php');
    exit;
}

辅助函数

<?php
// helpers.php
/**
 * 格式化文件大小
 */
function formatFileSize($size) {
    if ($size >= 1073741824) {
        return round($size / 1073741824, 2) . ' GB';
    } elseif ($size >= 1048576) {
        return round($size / 1048576, 2) . ' MB';
    } elseif ($size >= 1024) {
        return round($size / 1024, 2) . ' KB';
    } else {
        return $size . ' bytes';
    }
}
/**
 * 检查文件路径安全性
 */
function isSafePath($path) {
    $realPath = realpath(UPLOAD_DIR . $path);
    if (!$realPath) {
        return false;
    }
    $uploadDir = realpath(UPLOAD_DIR);
    return strpos($realPath, $uploadDir) === 0;
}
/**
 * 生成安全的文件名
 */
function generateSafeFileName($name) {
    // 移除危险字符
    $name = preg_replace('/[^\w\.\-]/', '', $name);
    // 限制长度
    return substr($name, 0, 255);
}

页面提示功能

<?php
// 在index.php的合适位置添加
if (isset($_SESSION['upload_messages'])) {
    $messages = $_SESSION['upload_messages'];
    if ($messages['success_count'] > 0) {
        echo '<div class="success">';
        echo $messages['success_count'] . ' 个文件上传成功';
        echo '</div>';
    }
    foreach ($messages['errors'] as $error) {
        echo '<div class="error">' . htmlspecialchars($error) . '</div>';
    }
    unset($_SESSION['upload_messages']);
}
if (isset($_SESSION['error_message'])) {
    echo '<div class="error">' . htmlspecialchars($_SESSION['error_message']) . '</div>';
    unset($_SESSION['error_message']);
}

安全注意事项

  1. 文件类型验证:始终验证文件的实际MIME类型,而不仅仅是文件扩展名。

  2. 文件名安全:不要直接使用用户提供的文件名,生成唯一的新文件名。

  3. 路径安全:确保文件路径不包含../等路径穿越字符。

  4. 访问控制:确保只有文件所有者或公开文件可以被访问。

  5. 文件大小限制:在服务器端PHP配置和应用程序层面都设置上传大小限制。

  6. 使用HTTPS:在生产环境中使用HTTPS保护文件传输。

这个系统提供了一个完整的文件上传共享解决方案,支持多文件上传、分类管理、搜索、下载统计等功能,您可以根据实际需求进行扩展和修改。

抱歉,评论功能暂时关闭!