168 lines
8.4 KiB
PHP
168 lines
8.4 KiB
PHP
<?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";
|
|
}
|