轻量级 PHP 临时文件分享系统源码|提取码 + 自动过期(解压即用)

轻量级 PHP 临时文件分享系统源码|提取码 + 自动过期(解压即用)

前言

临时传个文件给别人,不想注册网盘、又担心长期挂着隐私。今天分享一套原创教学用途的轻量 PHP 文件分享系统:上传生成提取码、可选下载码、按时过期自动删除,解压即可运行。

声明:本文源码为原创教学示例,请勿用于违法违规用途;请遵守当地法律法规与目标服务条款,禁止传播侵权或有害文件。

功能特性

  • 拖拽上传,大小可配置(默认 20MB)
  • 生成 6 位提取码,可选 4 位下载码
  • 有效期 1 小时~7 天,过期自动删文件
  • 下载计数、原文件名安全回传
  • 拦截可执行脚本类扩展名
  • SQLite 存储,深色 UI,手机可用

环境要求

  • PHP 8.0+,需 pdo_sqlite
  • 磁盘可写目录 storage/ 与 data/
  • Nginx/Apache 或 php -S 均可

目录结构

file-share/
├── index.php       # 入口
├── lib.php         # 核心逻辑
├── config.php      # 配置
├── templates/      # 页面
├── storage/        # 上传文件(注意勿公开列目录)
└── data/           # SQLite(自动创建)

完整源码

config.php

<?php
/** 临时文件分享 · 配置 */
return [
    'site_name'      => '轻分享',
    'db_path'        => __DIR__ . '/data/share.db',
    'upload_dir'     => __DIR__ . '/storage',
    'max_size'       => 20 * 1024 * 1024,  // 20MB
    'default_hours'  => 24,                 // 默认有效期(小时)
    'max_hours'      => 168,                // 最长 7 天
    'code_len'       => 6,                  // 提取码长度
    'download_code'  => true,               // 是否启用下载码
];

lib.php

<?php
/**
 * 临时文件分享核心:上传 → 提取码 → 定时过期
 */
function cfg(string $key, $default = null)
{
    static $c = null;
    if ($c === null) {
        $c = require __DIR__ . '/config.php';
    }
    return $c[$key] ?? $default;
}

function e(?string $s): string
{
    return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function db(): PDO
{
    static $pdo = null;
    if ($pdo instanceof PDO) {
        return $pdo;
    }
    $path = cfg('db_path');
    if (!is_dir(dirname($path))) {
        mkdir(dirname($path), 0755, true);
    }
    $upload = (string)cfg('upload_dir');
    if (!is_dir($upload)) {
        mkdir($upload, 0755, true);
    }
    $pdo = new PDO('sqlite:' . $path);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    $pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS shares (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  code TEXT UNIQUE NOT NULL,
  dl_code TEXT DEFAULT '',
  original_name TEXT NOT NULL,
  stored_name TEXT NOT NULL,
  size INTEGER DEFAULT 0,
  hits INTEGER DEFAULT 0,
  expires_at TEXT NOT NULL,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
SQL);
    return $pdo;
}

function random_code(int $len = 6): string
{
    $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $out = '';
    for ($i = 0; $i < $len; $i++) {
        $out .= $chars[random_int(0, strlen($chars) - 1)];
    }
    return $out;
}

function csrf_token(): string
{
    if (empty($_SESSION['csrf'])) {
        $_SESSION['csrf'] = bin2hex(random_bytes(16));
    }
    return $_SESSION['csrf'];
}

function csrf_check(): void
{
    if (!hash_equals(csrf_token(), (string)($_POST['csrf'] ?? ''))) {
        http_response_code(419);
        exit('CSRF 校验失败');
    }
}

function flash(string $key, ?string $msg = null)
{
    if ($msg !== null) {
        $_SESSION['flash'][$key] = $msg;
        return null;
    }
    $v = $_SESSION['flash'][$key] ?? null;
    unset($_SESSION['flash'][$key]);
    return $v;
}

function format_bytes(int $bytes): string
{
    $units = ['B', 'KB', 'MB', 'GB'];
    $i = 0;
    $n = (float)$bytes;
    while ($n >= 1024 && $i < count($units) - 1) {
        $n /= 1024;
        $i++;
    }
    return round($n, $i === 0 ? 0 : 1) . ' ' . $units[$i];
}

function clean_expired(): void
{
    $db = db();
    $rows = $db->query("SELECT stored_name FROM shares WHERE expires_at < datetime('now')")->fetchAll();
    foreach ($rows as $row) {
        $f = cfg('upload_dir') . '/' . $row['stored_name'];
        if (is_file($f)) {
            @unlink($f);
        }
    }
    $db->exec("DELETE FROM shares WHERE expires_at < datetime('now')");
}

function save_upload(array $file, int $hours): array
{
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return ['ok' => false, 'msg' => '上传失败,请重试'];
    }
    if ($file['size'] > (int)cfg('max_size')) {
        return ['ok' => false, 'msg' => '文件不能超过 ' . format_bytes((int)cfg('max_size'))];
    }
    if ($file['size'] <= 0) {
        return ['ok' => false, 'msg' => '空文件'];
    }
    $orig = (string)$file['name'];
    $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
    // 可自行扩充白名单;此处禁止明显可执行类型
    $deny = ['php', 'php3', 'php4', 'php5', 'phtml', 'phar', 'exe', 'sh', 'bat', 'cmd', 'cgi', 'pl'];
    if (in_array($ext, $deny, true)) {
        return ['ok' => false, 'msg' => '不支持该文件类型'];
    }
    $stored = date('Ymd') . '_' . bin2hex(random_bytes(8));
    $dest = cfg('upload_dir') . '/' . $stored;
    if (!move_uploaded_file($file['tmp_name'], $dest)) {
        return ['ok' => false, 'msg' => '保存失败'];
    }
    $hours = max(1, min((int)cfg('max_hours', 168), $hours));
    $code = random_code((int)cfg('code_len', 6));
    $dl = cfg('download_code') ? random_code(4) : '';
    db()->prepare(
        "INSERT INTO shares (code, dl_code, original_name, stored_name, size, expires_at)
         VALUES (?,?,?,?,?, datetime('now', ?))"
    )->execute([$code, $dl, mb_substr($orig, 0, 180), $stored, (int)$file['size'], '+' . $hours . ' hours']);

    return [
        'ok' => true,
        'msg' => '上传成功',
        'code' => $code,
        'dl_code' => $dl,
        'hours' => $hours,
    ];
}

function find_share(string $code): ?array
{
    clean_expired();
    $st = db()->prepare("SELECT * FROM shares WHERE code = ? AND expires_at > datetime('now')");
    $st->execute([strtoupper(trim($code))]);
    return $st->fetch() ?: null;
}

function view(string $tpl, array $data = []): void
{
    extract($data, EXTR_SKIP);
    require __DIR__ . '/templates/' . $tpl . '.php';
}

index.php

<?php
declare(strict_types=1);
session_start();
require __DIR__ . '/lib.php';

$r = $_GET['r'] ?? 'home';
header('Content-Type: text/html; charset=utf-8');
clean_expired();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check();
    if ($r === 'upload') {
        $hours = (int)($_POST['hours'] ?? cfg('default_hours', 24));
        $res = save_upload($_FILES['file'] ?? [], $hours);
        if ($res['ok']) {
            $_SESSION['last_share'] = $res;
            header('Location: index.php?r=done');
        } else {
            flash('msg', $res['msg']);
            header('Location: index.php');
        }
        exit;
    }
    if ($r === 'download') {
        $share = find_share((string)($_POST['code'] ?? ''));
        if (!$share) {
            flash('msg', '提取码无效或已过期');
            header('Location: index.php?r=get');
            exit;
        }
        if ($share['dl_code'] !== '' && strtoupper(trim((string)($_POST['dl_code'] ?? ''))) !== strtoupper($share['dl_code'])) {
            flash('msg', '下载码错误');
            header('Location: index.php?r=get&code=' . urlencode($share['code']));
            exit;
        }
        $path = cfg('upload_dir') . '/' . $share['stored_name'];
        if (!is_file($path)) {
            flash('msg', '文件已丢失');
            header('Location: index.php');
            exit;
        }
        db()->prepare('UPDATE shares SET hits = hits + 1 WHERE id = ?')->execute([$share['id']]);
        header('Content-Type: application/octet-stream');
        header('Content-Length: ' . filesize($path));
        $name = str_replace(["r", "n"], '', $share['original_name']);
        header('Content-Disposition: attachment; filename="' . $name . '"; filename*=UTF-8''' . rawurlencode($share['original_name']));
        readfile($path);
        exit;
    }
}

if ($r === 'done') {
    $last = $_SESSION['last_share'] ?? null;
    unset($_SESSION['last_share']);
    view('done', ['share' => $last, 'site_name' => cfg('site_name')]);
    exit;
}

if ($r === 'get') {
    $code = (string)($_GET['code'] ?? '');
    $share = $code !== '' ? find_share($code) : null;
    view('get', [
        'site_name' => cfg('site_name'),
        'share' => $share,
        'code' => $code,
        'msg' => flash('msg'),
    ]);
    exit;
}

view('home', [
    'site_name' => cfg('site_name'),
    'max_size' => format_bytes((int)cfg('max_size')),
    'default_hours' => (int)cfg('default_hours', 24),
    'max_hours' => (int)cfg('max_hours', 168),
    'use_dl_code' => (bool)cfg('download_code'),
    'msg' => flash('msg'),
]);

templates/_top.php

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?= e($site_name ?? '轻分享') ?></title>
  <style>
    :root { --bg:#0f1115; --panel:#1a1d24; --border:#2a2f3a; --text:#e8eaed; --muted:#8b93a7; --accent:#f04494; }
    * { box-sizing:border-box; }
    body { margin:0; font-family:system-ui,-apple-system,"PingFang SC",sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
    .wrap { max-width:680px; margin:0 auto; padding:36px 16px 60px; }
    .brand { display:flex; align-items:center; gap:12px; margin-bottom:18px; }
    .logo { width:42px; height:42px; border-radius:12px; background:linear-gradient(135deg,#f04494,#ff8f3a); display:grid; place-items:center; color:#fff; font-weight:800; }
    h1 { margin:0; font-size:26px; }
    .sub { color:var(--muted); margin:6px 0 22px; }
    .card { background:var(--panel); border:1px solid var(--border); border-radius:16px; padding:22px; }
    .drop { border:2px dashed var(--border); border-radius:14px; padding:28px; text-align:center; color:var(--muted); cursor:pointer; }
    .drop.hot { border-color:var(--accent); color:var(--accent); background:rgba(240,68,148,.08); }
    label { display:block; font-size:13px; color:var(--muted); margin:14px 0 6px; }
    input, select { width:100%; padding:11px 12px; border-radius:10px; border:1px solid var(--border); background:#12151c; color:var(--text); font:inherit; }
    button, .btn { border:0; cursor:pointer; background:var(--accent); color:#fff; font-weight:600; border-radius:999px; padding:12px 18px; font:inherit; text-decoration:none; display:inline-flex; }
    .btn-ghost { background:transparent; border:1px solid var(--border); color:var(--text); }
    .msg { padding:11px 13px; border-radius:10px; background:rgba(240,68,148,.12); color:#ff8fbc; margin-bottom:12px; font-size:14px; }
    .okbox { background:rgba(61,214,140,.1); border:1px solid rgba(61,214,140,.3); color:#7dffa3; }
    .codebox { font:600 22px/1.4 ui-monospace,Consolas,monospace; letter-spacing:2px; background:#12151c; border:1px solid var(--border); border-radius:12px; padding:14px; text-align:center; margin:10px 0; }
    .actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; }
    footer { text-align:center; color:var(--muted); font-size:13px; margin-top:24px; }
    footer a { color:var(--accent); text-decoration:none; }
  </style>
</head>
<body>
<div class="wrap">
  <div class="brand">
    <div class="logo">F</div>
    <div>
      <h1><?= e($site_name ?? '轻分享') ?></h1>
      <div class="sub" style="margin:0">临时文件分享 · 提取码 · 自动过期</div>
    </div>
  </div>

templates/_bottom.php

<footer>轻量 PHP 临时分享 · <a href="index.php?r=get">用提取码取文件</a></footer>
</div>
<script>
(function () {
  var drop = document.getElementById('drop');
  var file = document.getElementById('file');
  if (!drop || !file) return;
  drop.addEventListener('click', function () { file.click(); });
  file.addEventListener('change', function () {
    drop.textContent = file.files.length ? file.files[0].name : '点击选择或拖拽文件到此处';
  });
  drop.addEventListener('dragover', function (e) {
    e.preventDefault(); drop.classList.add('hot');
  });
  drop.addEventListener('dragleave', function () {
    drop.classList.remove('hot');
  });
  drop.addEventListener('drop', function (e) {
    e.preventDefault(); drop.classList.remove('hot');
    if (e.dataTransfer.files.length) {
      file.files = e.dataTransfer.files;
      drop.textContent = file.files[0].name;
    }
  });
})();
</script>
</body>
</html>

templates/home.php

<?php require __DIR__ . '/_top.php'; ?>
<?php if (!empty($msg)): ?><div class="msg"><?= e($msg) ?></div><?php endif; ?>

<div class="card">
  <form method="post" action="index.php?r=upload" enctype="multipart/form-data">
    <input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
    <div class="drop" id="drop">点击选择或拖拽文件到此处(最大 <?= e($max_size) ?>)</div>
    <input type="file" name="file" id="file" required style="display:none">

    <label>有效期</label>
    <select name="hours">
      <?php foreach ([1, 6, 12, 24, 48, 72, 168] as $h): ?>
        <option value="<?= $h ?>" <?= $h === (int)$default_hours ? 'selected' : '' ?>>
          <?= $h >= 24 ? ($h / 24) . ' 天' : $h . ' 小时' ?>
        </option>
      <?php endforeach; ?>
    </select>

    <div style="margin-top:18px">
      <button type="submit">上传并生成提取码</button>
    </div>
    <p class="sub" style="margin:14px 0 0;font-size:13px">
      支持任意常规文件;禁止可执行脚本。链接过期后文件自动删除。
      <?php if ($use_dl_code): ?>开启下载码保护。<?php endif; ?>
    </p>
  </form>
</div>

<?php require __DIR__ . '/_bottom.php'; ?>

templates/done.php

<?php require __DIR__ . '/_top.php'; ?>
<div class="card">
  <h2 style="margin-top:0">上传成功</h2>
  <?php if (empty($share) || empty($share['ok'])): ?>
    <p>未找到上传结果,请重新上传。</p>
    <a class="btn" href="index.php">返回上传</a>
  <?php else: ?>
    <p class="sub" style="margin:0">文件已生成提取码,有效期约 <?= (int)$share['hours'] ?> 小时。</p>
    <label>提取码</label>
    <div class="codebox"><?= e($share['code']) ?></div>
    <?php if (($share['dl_code'] ?? '') !== ''): ?>
      <label>下载码(打开文件时需要)</label>
      <div class="codebox"><?= e($share['dl_code']) ?></div>
    <?php endif; ?>
    <label>分享链接(把提取码给对方)</label>
    <div class="codebox" style="font-size:15px;letter-spacing:0">
      <?= e((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http')
        . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost')
        . rtrim(str_replace('\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/')
        . '/index.php?r=get&code=' . urlencode($share['code'])) ?>
    </div>
    <div class="actions">
      <button type="button" onclick="navigator.clipboard.writeText('<?= e($share['code']) ?>')">复制提取码</button>
      <a class="btn btn-ghost" href="index.php">再传一个</a>
    </div>
  <?php endif; ?>
</div>
<?php require __DIR__ . '/_bottom.php'; ?>

templates/get.php

<?php require __DIR__ . '/_top.php'; ?>
<h1>提取文件</h1>
<?php if (!empty($msg)): ?><div class="msg"><?= e($msg) ?></div><?php endif; ?>

<div class="card">
  <form method="post" action="index.php?r=download">
    <input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
    <label>提取码</label>
    <input name="code" required maxlength="12" value="<?= e($code) ?>" placeholder="例如 A1B2C3">
    <?php if ($share && $share['dl_code'] !== ''): ?>
      <label>下载码</label>
      <input name="dl_code" required maxlength="8" placeholder="4 位下载码">
    <?php endif; ?>
    <?php if ($share): ?>
      <p class="sub" style="margin:12px 0 0;font-size:13px">
        文件:<strong><?= e($share['original_name']) ?></strong>
        (<?= e(format_bytes((int)$share['size'])) ?> · 过期时间 <?= e($share['expires_at']) ?>)
      </p>
    <?php endif; ?>
    <div style="margin-top:16px">
      <button type="submit">下载文件</button>
    </div>
  </form>
</div>
<div class="actions">
  <a class="btn btn-ghost" href="index.php">← 去上传</a>
</div>
<?php require __DIR__ . '/_bottom.php'; ?>

? 源码下载

下载 php-file-share.zip(完整可用源码)

下载解压后按文中「部署步骤」即可运行。若正文代码与压缩包不一致,以压缩包为准。

部署步骤

  1. 上传整个目录到 PHP 空间(解压 zip 即可)。
  2. 确保 storage/、data/ 可写。
  3. 访问首页上传文件,把提取码发给对方。
  4. 对方打开 index.php?r=get 输入提取码(如有下载码也要填)下载。
  5. 建议在 Nginx 禁止列目录:对 /storage/ 关闭 autoindex,并只允许脚本走 index.php。

安全与合规说明

  • 请勿分享木马、盗版、隐私侵权等违规文件;站长需对内容负责。
  • 生产环境务必 HTTPS;可把 storage/ 挪到 Web 根目录外并用 PHP 读文件。
  • 默认拦截 php/exe/sh 等扩展名,请按业务继续收紧。
  • 本源码仅供学习交流,使用后果自负。

小结

一套「上传—提取码—过期删除」闭环,适合团队临时传包、给客户交付文件。下一步可扩展:密码保护、下载次数上限、管理面板、邮件通知。需要增强版可在评论区留言。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容