| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- <?php
- declare(strict_types=1);
- use Tinywan\Storage\Adapter\LocalAdapter;
- use Tinywan\Storage\Exception\StorageException;
- use Webman\Http\UploadFile;
- function makeChunkAdapter(string $root, array $overrides = []): LocalAdapter
- {
- return new class(array_merge([
- 'adapter' => LocalAdapter::class,
- 'root' => $root,
- 'dirname' => 'unit-test',
- 'domain' => 'http://127.0.0.1:8787',
- 'uri' => '/storage',
- 'algo' => 'sha1',
- 'chunk_size' => 1024,
- 'chunk_path' => $root . '.chunks',
- '_is_file_upload' => false,
- ], $overrides)) extends LocalAdapter {};
- }
- function makeChunkFile(string $content): UploadFile
- {
- $path = tempnam(sys_get_temp_dir(), 'storage_chunk_');
- file_put_contents($path, $content);
- return new UploadFile($path, 'chunk.part', 'application/octet-stream', UPLOAD_ERR_OK);
- }
- function makeChunkRoot(): string
- {
- return sys_get_temp_dir() . '/storage_chunk_' . uniqid() . '/';
- }
- test('分片上传成功落盘并返回标准信息', function () {
- $content = str_repeat('A', 512);
- $result = makeChunkAdapter(makeChunkRoot())->uploadChunk(makeChunkFile($content), 'demo-file', 0, md5($content));
- expect($result)
- ->toHaveKeys(['file_id', 'chunk_index', 'save_name', 'save_path', 'size', 'md5'])
- ->and($result['file_id'])
- ->toBe('demo-file')
- ->and($result['chunk_index'])
- ->toBe(0)
- ->and($result['save_name'])
- ->toBe('00000000.part')
- ->and($result['size'])
- ->toBe(512)
- ->and($result['md5'])
- ->toBe(md5($content))
- ->and($result['save_path'])
- ->toBeFile()
- ->and(file_get_contents($result['save_path']))
- ->toBe($content);
- });
- test('分片 md5 校验失败时抛异常且无残留文件', function () {
- $root = makeChunkRoot();
- $adapter = makeChunkAdapter($root);
- expect(fn() => $adapter->uploadChunk(makeChunkFile('hello'), 'demo-file', 0, 'invalid-md5'))
- ->toThrow(StorageException::class, 'md5');
- expect(is_dir($root . '.chunks/demo-file'))->toBeFalse();
- });
- test('file_id 非法时抛异常防止路径穿越', function (string $fileId) {
- $adapter = makeChunkAdapter(makeChunkRoot());
- expect(fn() => $adapter->chunkProgress($fileId, 4))->toThrow(StorageException::class, 'file_id');
- })->with(['../etc', '', str_repeat('a', 65), '-leading-dash', '中文id']);
- test('分片大小超出限制时抛异常', function () {
- $adapter = makeChunkAdapter(makeChunkRoot(), ['chunk_size' => 10]);
- $content = str_repeat('B', 32);
- expect(fn() => $adapter->uploadChunk(makeChunkFile($content), 'demo-file', 0, md5($content)))
- ->toThrow(StorageException::class, '分片大小超出限制');
- });
- test('断点续传查询返回已传与缺失分片', function () {
- $adapter = makeChunkAdapter(makeChunkRoot());
- $adapter->uploadChunk(makeChunkFile('aaa'), 'demo-file', 0, md5('aaa'));
- $adapter->uploadChunk(makeChunkFile('ccc'), 'demo-file', 2, md5('ccc'));
- $progress = $adapter->chunkProgress('demo-file', 4);
- expect($progress['total'])
- ->toBe(4)
- ->and($progress['uploaded'])
- ->toBe([0, 2])
- ->and($progress['missing'])
- ->toBe([1, 3])
- ->and($progress['finished'])
- ->toBeFalse();
- });
- test('未知 file_id 查询进度返回全量缺失', function () {
- $progress = makeChunkAdapter(makeChunkRoot())->chunkProgress('no-such-file', 3);
- expect($progress['uploaded'])
- ->toBe([])
- ->and($progress['missing'])
- ->toBe([0, 1, 2])
- ->and($progress['finished'])
- ->toBeFalse();
- });
- test('合并分片后内容与源文件逐字节一致', function () {
- $root = makeChunkRoot();
- $adapter = makeChunkAdapter($root, ['chunk_size' => 512]);
- $origin = str_repeat('webman-storage-', 100);
- $chunks = str_split($origin, 512);
- foreach ($chunks as $index => $chunk) {
- $adapter->uploadChunk(makeChunkFile($chunk), 'demo-file', $index, md5($chunk));
- }
- $result = $adapter->mergeChunk('demo-file', count($chunks), 'video.mp4');
- expect($result)
- ->toHaveKeys(['origin_name', 'save_name', 'save_path', 'url', 'unique_id', 'size', 'mime_type', 'extension'])
- ->and($result['origin_name'])
- ->toBe('video.mp4')
- ->and($result['extension'])
- ->toBe('mp4')
- ->and($result['size'])
- ->toBe(strlen($origin))
- ->and($result['unique_id'])
- ->toBe(sha1($origin))
- ->and($result['url'])
- ->toStartWith('http://127.0.0.1:8787/storage')
- ->and(file_get_contents($result['save_path']))
- ->toBe($origin)
- ->and(is_dir($root . '.chunks/demo-file'))
- ->toBeFalse();
- });
- test('缺少分片时合并失败并提示缺失序号', function () {
- $adapter = makeChunkAdapter(makeChunkRoot());
- $adapter->uploadChunk(makeChunkFile('aaa'), 'demo-file', 0, md5('aaa'));
- expect(fn() => $adapter->mergeChunk('demo-file', 3, 'demo.txt'))->toThrow(StorageException::class, '缺少分片:1,2');
- });
- test('合并时校验文件后缀白名单与黑名单', function () {
- $includeAdapter = makeChunkAdapter(makeChunkRoot(), ['include' => ['png']]);
- expect(fn() => $includeAdapter->mergeChunk('demo-file', 1, 'demo.txt'))
- ->toThrow(StorageException::class, '文件扩展名不合法');
- $excludeAdapter = makeChunkAdapter(makeChunkRoot(), ['exclude' => ['php']]);
- expect(fn() => $excludeAdapter->mergeChunk('demo-file', 1, 'shell.php'))
- ->toThrow(StorageException::class, '文件扩展名不合法');
- $adapter = makeChunkAdapter(makeChunkRoot());
- $adapter->uploadChunk(makeChunkFile('zip'), 'demo-file', 0, md5('zip'));
- expect($adapter->mergeChunk('demo-file', 1, 'any.zip')['extension'])->toBe('zip');
- });
- test('同一分片序号重传覆盖旧分片', function () {
- $adapter = makeChunkAdapter(makeChunkRoot());
- $adapter->uploadChunk(makeChunkFile('AAA'), 'demo-file', 0, md5('AAA'));
- $adapter->uploadChunk(makeChunkFile('BBB'), 'demo-file', 0, md5('BBB'));
- $result = $adapter->mergeChunk('demo-file', 1, 'demo.txt');
- expect(file_get_contents($result['save_path']))->toBe('BBB');
- });
- test('合并锁存在时抛异常', function () {
- $root = makeChunkRoot();
- $adapter = makeChunkAdapter($root);
- $adapter->uploadChunk(makeChunkFile('AAA'), 'demo-file', 0, md5('AAA'));
- $lockPath = $root . '.chunks/demo-file.lock';
- file_put_contents($lockPath, 'lock');
- expect(fn() => $adapter->mergeChunk('demo-file', 1, 'demo.txt'))->toThrow(StorageException::class, '正在合并中');
- @unlink($lockPath);
- });
|