feat(extensions): 新增系统工具页面与前台投稿分片上传

This commit is contained in:
2026-10-06 11:09:05 +08:00
parent 613b8a57a3
commit b75bf197a3
25 changed files with 1982 additions and 58 deletions
@@ -0,0 +1,167 @@
<?php
/** Split upload (分片上传) runtime contract harness: chunk params, part naming, upload/status/merge protocol. */
declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('HOUR_IN_SECONDS', 3600);
define('WP_CONTENT_DIR', sys_get_temp_dir() . '/aksrt-split-test-' . getmypid());
function sanitize_file_name(string $name): string { return preg_replace('/[^A-Za-z0-9._\-]/', '', $name) ?? ''; }
function __(string $text, string $domain = ''): string { return $text; }
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
function get_current_blog_id(): int { return 1; }
function get_current_user_id(): int { return 7; }
function apply_filters(string $tag, $value) { return $value; }
function wp_mkdir_p(string $dir): bool { return is_dir($dir) || mkdir($dir, 0777, true); }
function is_wp_error($thing): bool { return $thing instanceof \WP_Error; }
function size_format(int $bytes): string { return $bytes . 'B'; }
function get_allowed_mime_types(): array { return ['mp4' => 'video/mp4']; }
function wp_check_filetype_and_ext(string $file, string $name, array $mimes = []): array {
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
return ['ext' => $ext, 'type' => $mimes[$ext] ?? ''];
}
class WP_Error {
private array $errors;
public function __construct($code = '', $message = '') { $this->errors = [$code => $message]; }
public function get_error_message(): string { return (string) reset($this->errors); }
public function get_error_code() { return (string) array_key_first($this->errors); }
}
class WP_REST_Server {
const READABLE = 'GET';
}
class WP_REST_Response {
private array $data;
private int $status;
public function __construct($data = [], int $status = 200) { $this->data = $data; $this->status = $status; }
public function get_data(): array { return $this->data; }
public function get_status(): int { return $this->status; }
}
class WP_REST_Request {
}
class FakeRestRequest extends \WP_REST_Request {
private array $params;
private array $files;
public function __construct(array $params = [], array $files = []) {
$this->params = $params;
$this->files = $files;
}
public function get_param(string $key) { return $this->params[$key] ?? null; }
public function get_file_params(): array { return $this->files; }
}
function split_upload_check(bool $condition, string $message): void {
if (!$condition) throw new \RuntimeException($message);
}
require dirname(__DIR__, 2) . '/src/Core/FrontSubmitChunk.php';
}
namespace AKSRT\Core {
final class FakeFrontSubmit {
use \AKSRT\Core\FrontSubmitChunk;
public static int $limit_bytes = 10 * 1048576;
public static array $persisted = [];
public static function upload_limit_bytes(string $kind): int {
return self::$limit_bytes;
}
/** Stand-in for FrontSubmit::persist_uploaded_file(): records and echoes merged content. */
public function persist_uploaded_file(string $kind, array $file, bool $sideload = false) {
self::$persisted[] = ['kind' => $kind, 'file' => $file, 'sideload' => $sideload];
return 'persisted:' . (string) file_get_contents((string) $file['tmp_name']);
}
}
}
namespace {
use AKSRT\Core\FakeFrontSubmit;
$submit = new FakeFrontSubmit();
$temp_dir = WP_CONTENT_DIR . '/aksrt-temp';
// 1) Chunk parameter validation.
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => '', 'chunks' => 3, 'chunk' => 0, 'size' => 10]));
split_upload_check($err instanceof \WP_Error, 'empty name should be rejected');
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => 'a.mp4', 'chunks' => 0, 'chunk' => 0, 'size' => 10]));
split_upload_check($err instanceof \WP_Error, 'zero chunk count should be rejected');
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => 'a.mp4', 'chunks' => 3, 'chunk' => 5, 'size' => 10]));
split_upload_check($err instanceof \WP_Error, 'out-of-range chunk index should be rejected');
// 2) Chunk upload lands per-index .part files, oversized total is refused.
$make_file = static function (string $content): string {
$path = tempnam(sys_get_temp_dir(), 'chunk');
file_put_contents($path, $content);
return $path;
};
$params = static fn (int $chunk, int $chunks, int $size): array => [
'kind' => 'video', 'chunk' => $chunk, 'chunks' => $chunks, 'name' => 'movie.mp4', 'size' => $size,
];
$response = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 3, 6), ['file' => ['tmp_name' => $make_file('AAA'), 'error' => 0]]));
split_upload_check($response instanceof \WP_REST_Response, 'first chunk should upload');
split_upload_check($response->get_data()['uploaded_size'] === 3, 'uploaded_size should count the stored chunk');
$submit->rest_upload_chunk(new FakeRestRequest($params(2, 3, 6), ['file' => ['tmp_name' => $make_file('CCC'), 'error' => 0]]));
$parts = glob($temp_dir . '/*.part');
split_upload_check(count($parts) === 2, 'two part files should exist');
foreach ($parts as $part) {
split_upload_check(strpos(basename($part), '1-') === 0, 'part name should be blog-id prefixed');
}
// 3) Status reports the uploaded indexes (resume contract).
$response = $submit->rest_upload_chunk_status(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3]));
split_upload_check($response instanceof \WP_REST_Response, 'status should respond');
split_upload_check($response->get_data()['uploaded'] === [0, 2], 'status should list uploaded chunk indexes');
// 4) Merge refuses when a chunk is missing.
$err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_missing', 'merge should refuse with missing chunks');
// 5) Merge with a size mismatch is refused.
$submit->rest_upload_chunk(new FakeRestRequest($params(1, 3, 6), ['file' => ['tmp_name' => $make_file('BBB'), 'error' => 0]]));
$err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 999]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_size_mismatch', 'size mismatch should refuse the merge');
// 6) Happy-path merge concatenates in order, flags sideload, and cleans up.
$response = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9]));
split_upload_check(is_string($response) && $response === 'persisted:AAABBBCCC', 'merge should concatenate chunks in order');
split_upload_check(count(FakeFrontSubmit::$persisted) === 1, 'merge should persist exactly once');
$call = FakeFrontSubmit::$persisted[0];
split_upload_check($call['kind'] === 'video' && $call['sideload'] === true, 'merge should persist as a sideloaded video');
split_upload_check($call['file']['name'] === 'movie.mp4' && $call['file']['size'] === 9, 'merge should rebuild the $_FILES-style payload');
split_upload_check(glob($temp_dir . '/*.part') === [], 'merge should clean up all parts');
// 7) Per-user keying: a different file name never sees another name's parts.
$response = $submit->rest_upload_chunk_status(new FakeRestRequest(['kind' => 'video', 'name' => 'other.mp4', 'chunks' => 3]));
split_upload_check($response instanceof \WP_REST_Response && $response->get_data()['uploaded'] === [], 'a different name should start with no uploaded chunks');
// 8) Chunk size cap refuses oversized chunk blobs.
$big = $make_file(str_repeat('x', 3 * 1048576));
$err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 1, 3 * 1048576), ['file' => ['tmp_name' => $big, 'error' => 0]]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_invalid_size', 'oversized chunk blobs should be refused');
// 9) Kind size limit caps cumulative uploads.
FakeFrontSubmit::$limit_bytes = 4;
$err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 2, 9), ['file' => ['tmp_name' => $make_file('AAAAA'), 'error' => 0]]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_upload_too_large', 'cumulative size over the kind limit should be refused');
// Cleanup.
FakeFrontSubmit::$persisted = [];
foreach ((array) glob(WP_CONTENT_DIR . '/*.part') as $leftover) {
@unlink((string) $leftover);
}
@unlink(WP_CONTENT_DIR . '/aksrt-temp/index.php');
@rmdir(WP_CONTENT_DIR . '/aksrt-temp');
@rmdir(WP_CONTENT_DIR);
echo "Split upload harness passed\n";
}