false, 'error' => 'method not allowed'], 405); } $mode = fb_store_mode(); $putAt = (isset($_GET['action']) && (string)$_GET['action'] === 'put'); if ($mode === 'off') { fb_up_json(['ok' => false, 'error' => '附件上传未启用。'], 403); } if ($mode === 'oss' && !fb_oss_enabled()) { fb_up_json(['ok' => false, 'error' => '对象存储未配置完整,请到后台「站点设置」补齐桶信息。'], 403); } if ($mode === 'local' && !fb_local_writable()) { $ld = fb_local_diag(); fb_up_json(['ok' => false, 'error' => '本机附件目录不可写(uploads/)。' . ($ld['tip'] !== '' ? ' ' . $ld['tip'] : '')], 403); } if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) { fb_up_json(['ok' => false, 'error' => '会话已过期,请刷新页面后重试。'], 400); } /* ================================================================== * 接收文件本体并落地(local → 本机磁盘;oss → 服务器代理 PUT 到内网 OSS) * ------------------------------------------------------------------ * 私有桶 + 内网 Endpoint 下浏览器无法直传,所以 OSS 模式也走这里: * 浏览器把文件 POST 给本站,本站再用服务器身份 PUT 进 OSS。 * ================================================================== */ if ($putAt) { if ($mode !== 'local' && $mode !== 'oss') { fb_up_json(['ok' => false, 'error' => '当前存储方式不支持上传。'], 400); } $kind = strtolower(trim(isset($_POST['kind']) ? (string)$_POST['kind'] : '')); $key = trim(isset($_POST['key']) ? (string)$_POST['key'] : ''); if ($kind !== 'image' && $kind !== 'video') { fb_up_json(['ok' => false, 'error' => '不支持的附件类型。'], 400); } if (!fb_upload_kind_on($kind)) { fb_up_json(['ok' => false, 'error' => '该类型附件未开放上传。'], 403); } if (!fb_upload_key_ok($key)) { fb_up_json(['ok' => false, 'error' => '附件地址不合法,请重新上传。'], 400); } // key 的存储类型必须与当前 mode 一致(防止 local 会话塞 oss key 或反之) $keyIsLocal = fb_attach_is_local($key); if ($mode === 'local' && !$keyIsLocal) { fb_up_json(['ok' => false, 'error' => '附件地址与当前存储方式不符。'], 400); } if ($mode === 'oss' && $keyIsLocal) { fb_up_json(['ok' => false, 'error' => '附件地址与当前存储方式不符。'], 400); } $ext = fb_upload_key_ext($key); if (!fb_upload_ext_ok($kind, $ext)) { fb_up_json(['ok' => false, 'error' => '不支持的文件格式。'], 400); } if (empty($_FILES['file']) || !is_array($_FILES['file'])) { fb_up_json(['ok' => false, 'error' => '没有收到文件。'], 400); } $f = $_FILES['file']; if (!isset($f['error']) || (int)$f['error'] !== UPLOAD_ERR_OK) { fb_up_json(['ok' => false, 'error' => '文件上传失败(错误码 ' . (int)(isset($f['error']) ? $f['error'] : -1) . ')。'], 400); } if (!is_uploaded_file((string)$f['tmp_name'])) { fb_up_json(['ok' => false, 'error' => '文件上传失败。'], 400); } $size = (int)@filesize($f['tmp_name']); $maxB = (int)fb_upload_cfg($kind . '_max_bytes', 0); if ($size <= 0) { fb_up_json(['ok' => false, 'error' => '文件为空。'], 400); } if ($maxB > 0 && $size > $maxB) { fb_up_json(['ok' => false, 'error' => '文件超过大小限制(' . fb_size_h($maxB) . ')。'], 400); } $mime = strtolower(trim(isset($_POST['mime']) ? (string)$_POST['mime'] : '')); if ($mime === '') { $mime = fb_upload_mime_of_ext($ext); } if (!fb_upload_mime_ok($kind, $mime)) { fb_up_json(['ok' => false, 'error' => '不支持的文件类型。'], 400); } // 二次确认总量上限(防止并发绕过) $pdo = fb_db(); $cap = (int)fb_upload_cfg('total_cap_bytes', 0); if ($cap > 0 && (fb_attach_total_bytes($pdo) + $size) > $cap) { fb_up_json(['ok' => false, 'error' => '附件存储空间已满,暂时无法上传。'], 507); } if ($mode === 'local') { $path = fb_local_path_of($key); $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (!is_dir($dir) || !@move_uploaded_file((string)$f['tmp_name'], $path)) { fb_up_json(['ok' => false, 'error' => '保存文件失败,请稍后重试。'], 500); } @chmod($path, 0644); } else { // OSS:服务器代理 PUT 到内网桶(流式,不整文件进内存) if (!fb_oss_enabled()) { fb_up_json(['ok' => false, 'error' => '对象存储未配置完整。'], 403); } list($ok, $ossErr, $ossStatus) = fb_oss_put_file($key, (string)$f['tmp_name'], $mime); if (!$ok) { fb_up_json(['ok' => false, 'error' => '写入对象存储失败:' . $ossErr], 502); } } // 登记「待入库」:让上传后、提交表单前的缩略图预览能通过 media.php 鉴权 fb_attach_pending_mark($key, $kind, $mime, $size); fb_up_json([ 'ok' => true, 'key' => $key, 'url' => fb_attach_url($key), 'size' => $size, 'mime' => $mime, ]); } /* ================================================================== * 领取上传凭证 * ================================================================== */ $kind = strtolower(trim(isset($_POST['kind']) ? (string)$_POST['kind'] : '')); if ($kind !== 'image' && $kind !== 'video') { fb_up_json(['ok' => false, 'error' => '不支持的附件类型。'], 400); } if (!fb_upload_kind_on($kind)) { fb_up_json(['ok' => false, 'error' => '该类型附件未开放上传。'], 403); } $ext = fb_upload_norm_ext(isset($_POST['ext']) ? (string)$_POST['ext'] : ''); $size = (int)(isset($_POST['size']) ? $_POST['size'] : 0); $mime = strtolower(trim(isset($_POST['mime']) ? (string)$_POST['mime'] : '')); if ($mime === '') { $mime = fb_upload_mime_of_ext($ext); } $maxBytes = (int)fb_upload_cfg($kind . '_max_bytes', 0); if ($ext === '' || !fb_upload_ext_ok($kind, $ext)) { fb_up_json(['ok' => false, 'error' => '不支持的文件格式。'], 400); } if (!fb_upload_mime_ok($kind, $mime)) { fb_up_json(['ok' => false, 'error' => '不支持的文件类型。'], 400); } if ($size <= 0) { fb_up_json(['ok' => false, 'error' => '文件为空。'], 400); } if ($maxBytes > 0 && $size > $maxBytes) { fb_up_json(['ok' => false, 'error' => '文件超过大小限制(' . fb_size_h($maxBytes) . ')。'], 400); } $limit = (int)fb_upload_cfg('upload_per_hour', 60); if (!fb_rate_check('upload', $limit)) { fb_up_json(['ok' => false, 'error' => '上传过于频繁,请稍后再试。'], 429); } // 全站总量上限:达到后不再发放凭证 $pdo = fb_db(); $cap = (int)fb_upload_cfg('total_cap_bytes', 0); if ($cap > 0 && (fb_attach_total_bytes($pdo) + $size) > $cap) { fb_up_json(['ok' => false, 'error' => '附件存储空间已满,暂时无法上传,请稍后再试。'], 507); } /* ---------- 领取上传凭证:local 与 oss 都指向本站 action=put ---------- * 私有桶 + 内网 Endpoint 下浏览器无法直传 OSS,所以两种模式统一为: * 第一步(这里):服务器生成对象键、返回本站上传地址与随传字段; * 第二步:浏览器把文件 multipart POST 到 upload.php?action=put, * 由服务器落地(本机磁盘 / 或代理 PUT 到内网 OSS)。 * 前端 app.js 因此对两种模式走完全相同的代码路径。 */ if ($mode === 'local') { $key = fb_local_make_key($ext); } else { $key = fb_oss_make_key($ext); } fb_rate_hit('upload'); fb_up_json([ 'ok' => true, 'credential' => [ 'host' => 'upload.php?action=put', 'key' => $key, 'url' => fb_attach_url($key), ], 'fields' => [ 'csrf' => fb_csrf_token(), 'key' => $key, 'kind' => $kind, 'mime' => $mime, ], 'mime' => $mime, 'kind' => $kind, ]);