定制 uploadx/uploadx 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

uploadx/uploadx

最新稳定版本:0.0.1

Composer 安装命令:

composer require uploadx/uploadx

包简介

A PHP library for uploading files with chunking, resume, multiple, and extensive validation.

README 文档

README

PHP Version

UploadX is a PHP file upload library that supports single and multiple uploads, chunked uploads with resume capability, multiple storage backends, and extensive validation.

Features

  • 🚀 Single & Multiple Uploads - Upload one or many files at once
  • 📦 Chunked Uploads - Split large files into manageable chunks
  • 🔄 Resume Capability - Resume interrupted uploads from the last position
  • 🗄️ Multiple Storage Backends - Local filesystem, Amazon S3, MinIO, and more
  • Validation - MIME type, file size, checksum, and custom validation
  • 🔒 Security - Path traversal protection, secure filenames, checksum verification
  • 📊 Progress Tracking - Real-time upload progress and metrics
  • 🔔 Event System - PSR-14 event dispatcher integration
  • 📝 Logging - PSR-3 compatible logging with Monolog
  • Performance - Memory-efficient chunk processing, streaming support
  • 🧪 Fully Tested - Comprehensive unit, feature, and integration tests

Requirements

  • PHP 8.3 or higher
  • Composer
  • Required extensions: json, fileinfo, hash, mbstring

Installation

composer require uploadx/uploadx

Quick Start

Basic Upload

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Exceptions\UploadException;

$uploadx = new UploadX([
    'storage_path' => __DIR__ . '/uploads',
    'storage_url' => '/uploads',
]);

try {
    $result = $uploadx->upload(
        __DIR__ . '/test-files/sample.txt',
        'documents/sample.txt'
    );

    echo "Upload successful!\n";
    echo "Path: {$result->path()}\n";
    echo "Filename: {$result->filename()}\n";
    echo "Size: {$result->size()} bytes\n";
    echo "MIME Type: {$result->mimeType()}\n";
    echo "URL: {$result->url()}\n";
    echo "Checksum: {$result->checksum()}\n";
    echo "Duration: {$result->duration()} seconds\n";
} catch (UploadException $e) {
    echo "Upload failed: {$e->getMessage()}\n";
}

Upload with Validation

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Exceptions\UploadException;
use UploadX\Exceptions\ChecksumException;

$uploadx = new UploadX([
    'storage_path' => __DIR__ . '/uploads',
    'allowed_mimes' => ['image/jpeg', 'image/png', 'application/pdf'],
    'max_file_size' => 10 * 1024 * 1024, // 10MB
]);

try {
    $result = $uploadx->upload(
        __DIR__ . '/test-files/photo.jpeg',
        'images/photo.jpeg',
        [
            'allowed_mimes' => ['image/jpeg', 'image/png'],
            'max_size' => 5 * 1024 * 1024, // 5MB
            'checksum' => hash_file('sha256', __DIR__ . '/test-files/photo.jpeg'),
            'secure_filename' => true,
            'metadata' => [
                'user_id' => 123,
                'category' => 'profile_photos',
                'uploaded_at' => date('Y-m-d H:i:s'),
            ],
        ]
    );

    echo "Upload with validation successful!\n";
    echo "Metadata: " . json_encode($result->metadata(), JSON_PRETTY_PRINT) . "\n";
} catch (ChecksumException $e) {
    echo "Checksum mismatch!\n";
    echo "Expected: {$e->expected()}\n";
    echo "Actual: {$e->actual()}\n";
} catch (UploadException $e) {
    echo "Upload failed: {$e->getMessage()}\n";
}

Multiple Upload

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Exceptions\UploadException;

$uploadx = new UploadX([
    'storage_path' => __DIR__ . '/uploads',
    'storage_url' => '/uploads',
]);

$files = [
    [
        'source' => __DIR__ . '/test-files/file1.txt',
        'destination' => 'documents/file1.txt',
        'options' => [
            'metadata' => ['type' => 'text', 'priority' => 1],
        ],
    ],
    [
        'source' => __DIR__ . '/test-files/file2.pdf',
        'destination' => 'documents/file2.pdf',
        'options' => [
            'metadata' => ['type' => 'pdf', 'priority' => 2],
        ],
    ],
    [
        'source' => __DIR__ . '/test-files/file3.jpg',
        'destination' => 'images/file3.jpg',
        'options' => [
            'allowed_mimes' => ['image/jpeg', 'image/png'],
            'max_size' => 10 * 1024 * 1024,
            'metadata' => ['type' => 'image', 'priority' => 3],
        ],
    ],
];

try {
    $results = $uploadx->uploadMultiple($files);

    echo "Successful uploads: " . count($results['successful']) . "\n";
    foreach ($results['successful'] as $index => $result) {
        echo "[{$index}] {$result->filename()} ({$result->size()} bytes)\n";
        echo "     URL: {$result->url()}\n";
    }

    echo "\nFailed uploads: " . count($results['failed']) . "\n";
    foreach ($results['failed'] as $failure) {
        echo "[{$failure['index']}] {$failure['file']}: {$failure['error']}\n";
    }
} catch (UploadException $e) {
    echo "Error: {$e->getMessage()}\n";
}

Batch Upload from Directory

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Exceptions\UploadException;

function uploadFromDirectory(UploadX $uploadx, string $directory, string $prefix = ''): array
{
    $files = [];
    $allowedExtensions = ['txt', 'pdf', 'jpg', 'png', 'zip'];

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
    );

    foreach ($iterator as $file) {
        if ($file->isFile()) {
            $extension = strtolower($file->getExtension());
            if (in_array($extension, $allowedExtensions)) {
                $relativePath = str_replace($directory, '', $file->getPathname());
                $relativePath = ltrim($relativePath, '/');

                $files[] = [
                    'source' => $file->getPathname(),
                    'destination' => ($prefix ? $prefix . '/' : '') . $relativePath,
                    'options' => [
                        'secure_filename' => false,
                    ],
                ];
            }
        }
    }

    return $uploadx->uploadMultiple($files);
}

$uploadx = new UploadX([
    'storage_path' => __DIR__ . '/uploads',
]);

try {
    $results = uploadFromDirectory($uploadx, __DIR__ . '/test-files', 'batch-uploads');

    echo "Batch upload complete!\n";
    echo "Successful: " . count($results['successful']) . "\n";
    echo "Failed: " . count($results['failed']) . "\n";

    foreach ($results['successful'] as $result) {
        echo "- {$result->filename()}\n";
    }
} catch (UploadException $e) {
    echo "Error: {$e->getMessage()}\n";
}

Chunked Upload

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Support\HashHelper;
use UploadX\Exceptions\UploadException;
use UploadX\Exceptions\InvalidChunkException;

$uploadx = new UploadX([
    'storage_path' => __DIR__ . '/uploads',
    'chunk_size' => 1 * 1024 * 1024, // 1MB chunks
    'temp_directory' => __DIR__ . '/temp/chunks',
    'sessions_directory' => __DIR__ . '/temp/sessions',
]);

$testFilePath = __DIR__ . '/test-files/large-file.dat';
$chunkSize = 1 * 1024 * 1024;

// Initialize chunk upload
$init = $uploadx->initChunkUpload(
    'large-file.dat',
    filesize($testFilePath),
    (int) ceil(filesize($testFilePath) / $chunkSize),
    ['description' => 'Chunk upload demo file']
);

$sessionId = $init['session_id'];

// Upload chunks
$handle = fopen($testFilePath, 'rb');
$totalChunks = (int) ceil(filesize($testFilePath) / $chunkSize);

for ($i = 1; $i <= $totalChunks; $i++) {
    $chunkData = fread($handle, $chunkSize);
    if ($chunkData === false) break;

    try {
        $result = $uploadx->uploadChunk([
            'session_id' => $sessionId,
            'chunk_number' => $i,
            'total_chunks' => $totalChunks,
            'chunk_size' => strlen($chunkData),
            'total_size' => filesize($testFilePath),
            'data' => $chunkData,
            'checksum' => HashHelper::chunkChecksum($chunkData),
            'filename' => 'large-file.dat',
            'mime_type' => 'application/octet-stream',
        ]);

        echo "Chunk {$i}/{$totalChunks} OK\n";
    } catch (UploadException | InvalidChunkException $e) {
        echo "Chunk {$i} FAILED: {$e->getMessage()}\n";
    }
}

fclose($handle);

// Final result
if ($result->isSuccessful()) {
    echo "Upload complete!\n";
    echo "URL: {$result->url()}\n";
    echo "Path: {$result->path()}\n";
}

Resume Upload

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Support\HashHelper;
use UploadX\Exceptions\UploadException;
use UploadX\Exceptions\InvalidChunkException;

$uploadx = new UploadX([
    'storage_path' => __DIR__ . '/uploads',
    'chunk_size' => 1 * 1024 * 1024, // 1MB chunks
    'temp_directory' => __DIR__ . '/temp/chunks',
    'sessions_directory' => __DIR__ . '/temp/sessions',
]);

// Initialize
$init = $uploadx->initChunkUpload('resume-file.dat', $fileSize, $totalChunks);
$sessionId = $init['session_id'];

// ... upload some chunks ...

// Resume interrupted upload
$sessionInfo = $uploadx->resume($sessionId);

echo "Session found:\n";
echo "Filename: {$sessionInfo['filename']}\n";
echo "Total chunks: {$sessionInfo['total_chunks']}\n";
echo "Uploaded chunks: " . implode(', ', $sessionInfo['uploaded_chunks']) . "\n";
echo "Missing chunks: " . implode(', ', $sessionInfo['missing_chunks']) . "\n";
echo "Progress: " . round($sessionInfo['progress'], 1) . "%\n";

// Upload remaining missing chunks
$handle = fopen($testFilePath, 'rb');
foreach ($sessionInfo['missing_chunks'] as $chunkNum) {
    $offset = ($chunkNum - 1) * $chunkSize;
    fseek($handle, $offset);

    $chunkData = fread($handle, $chunkSize);

    $uploadx->uploadChunk([
        'session_id' => $sessionId,
        'chunk_number' => $chunkNum,
        'total_chunks' => $totalChunks,
        'chunk_size' => strlen($chunkData),
        'total_size' => $fileSize,
        'data' => $chunkData,
        'checksum' => HashHelper::chunkChecksum($chunkData),
        'filename' => 'resume-file.dat',
        'mime_type' => 'application/octet-stream',
    ]);
}
fclose($handle);

S3 Storage

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Storage\S3Storage;
use UploadX\Exceptions\StorageException;

$s3Storage = new S3Storage(
    config: [
        'version' => 'latest',
        'region' => getenv('AWS_REGION') ?: 'us-east-1',
        'credentials' => [
            'key' => getenv('AWS_KEY') ?: 'YOUR_AWS_KEY',
            'secret' => getenv('AWS_SECRET') ?: 'YOUR_AWS_SECRET',
        ],
    ],
    bucket: getenv('S3_BUCKET') ?: 'my-bucket',
    prefix: 'uploads/example'
);

// Upload file
try {
    $result = $s3Storage->store(
        __DIR__ . '/test-files/sample.txt',
        'documents/sample.txt'
    );
    echo "Uploaded to: {$result}\n";
    echo "URL: {$s3Storage->url('documents/sample.txt')}\n";

    // Upload with metadata and encryption
    $result2 = $s3Storage->store(
        __DIR__ . '/test-files/photo.jpg',
        'images/photo.jpg',
        [
            'acl' => 'public-read',
            'storage_class' => 'STANDARD',
            'metadata' => [
                'author' => 'UploadX',
                'category' => 'photos',
            ],
            'server_side_encryption' => 'AES256',
        ]
    );

    // Other operations
    $exists = $s3Storage->exists('documents/sample.txt');
    $size = $s3Storage->size('documents/sample.txt');
    $mime = $s3Storage->mimeType('documents/sample.txt');
    $presignedUrl = $s3Storage->temporaryUrl('documents/sample.txt', 3600);
    $content = $s3Storage->retrieve('documents/sample.txt');
    $s3Storage->delete('documents/sample.txt');
} catch (StorageException $e) {
    echo "Storage error: {$e->getMessage()}\n";
}

// UploadX with S3 Storage
$uploadx = new UploadX(storage: $s3Storage);

$result = $uploadx->upload(
    __DIR__ . '/test-files/sample.txt',
    'general/sample.txt'
);

echo "Upload via UploadX successful!\n";
echo "Path: {$result->path()}\n";
echo "Storage Driver: {$result->storageDriver()}\n";

MinIO Storage

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Storage\MinioStorage;
use UploadX\Exceptions\StorageException;

$minioStorage = new MinioStorage(
    endpoint: getenv('MINIO_ENDPOINT') ?: 'http://localhost:9000',
    accessKey: getenv('MINIO_ACCESS_KEY') ?: 'minioadmin',
    secretKey: getenv('MINIO_SECRET_KEY') ?: 'minioadmin',
    bucket: getenv('MINIO_BUCKET') ?: 'uploads',
    region: 'us-east-1',
    prefix: 'uploads/example',
    usePathStyleEndpoint: true
);

// Ensure bucket exists
$minioStorage->ensureBucketExists();

// Basic operations
try {
    $storedPath = $minioStorage->store(
        __DIR__ . '/test-files/sample.txt',
        'documents/sample.txt'
    );
    echo "Stored at: {$storedPath}\n";
    echo "URL: {$minioStorage->url('documents/sample.txt')}\n";

    // Store with metadata
    $minioStorage->store(
        __DIR__ . '/test-files/photo.jpg',
        'images/photo.jpg',
        [
            'acl' => 'public-read',
            'metadata' => [
                'author' => 'UploadX',
                'description' => 'Example photo for MinIO',
            ],
        ]
    );

    // Check existence, info, temporary URL, retrieve, delete
    $exists = $minioStorage->exists('documents/sample.txt');
    $size = $minioStorage->size('documents/sample.txt');
    $mime = $minioStorage->mimeType('documents/sample.txt');
    $tempUrl = $minioStorage->temporaryUrl('documents/sample.txt', 3600);
    $content = $minioStorage->retrieve('documents/sample.txt');
    $minioStorage->delete('documents/sample.txt');
} catch (StorageException $e) {
    echo "Storage error: {$e->getMessage()}\n";
}

// UploadX with MinIO
$uploadx = new UploadX(storage: $minioStorage);

$result = $uploadx->upload(
    __DIR__ . '/test-files/sample.txt',
    'general/sample.txt'
);

Web Upload Handler

<?php

require_once __DIR__ . '/vendor/autoload.php';

use UploadX\UploadX;
use UploadX\Support\HashHelper;
use UploadX\Exceptions\UploadException;
use UploadX\Exceptions\InvalidChunkException;

class WebUploadHandler
{
    private UploadX $uploadx;
    private array $config;

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'upload_dir' => __DIR__ . '/uploads/web',
            'max_file_size' => 100 * 1024 * 1024, // 100MB
            'allowed_types' => [
                'image/jpeg', 'image/png', 'image/gif',
                'application/pdf', 'application/zip', 'text/plain',
            ],
            'chunk_size' => 5 * 1024 * 1024, // 5MB
        ], $config);

        $this->uploadx = new UploadX([
            'storage_path' => $this->config['upload_dir'],
            'storage_url' => '/uploads/web',
            'max_file_size' => $this->config['max_file_size'],
            'allowed_mimes' => $this->config['allowed_types'],
            'chunk_size' => $this->config['chunk_size'],
            'temp_directory' => __DIR__ . '/temp/web-chunks',
            'sessions_directory' => __DIR__ . '/temp/web-sessions',
        ]);
    }

    /**
     * Handle single file upload from form
     */
    public function handleSingleUpload(array $fileField, string $destination = ''): array
    {
        if (!isset($fileField['tmp_name']) || empty($fileField['tmp_name'])) {
            return [
                'success' => false,
                'error' => 'No file uploaded',
                'code' => 'NO_FILE',
            ];
        }

        if ($fileField['error'] !== UPLOAD_ERR_OK) {
            $errorMessages = [
                UPLOAD_ERR_INI_SIZE => 'File exceeds server upload limit',
                UPLOAD_ERR_FORM_SIZE => 'File exceeds form upload limit',
                UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
                UPLOAD_ERR_NO_FILE => 'No file was uploaded',
                UPLOAD_ERR_NO_TMP_DIR => 'Server missing temporary directory',
                UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
            ];
            return [
                'success' => false,
                'error' => $errorMessages[$fileField['error']] ?? 'Unknown upload error',
                'code' => 'UPLOAD_ERROR',
            ];
        }

        $filename = $fileField['name'];
        $source = $fileField['tmp_name'];
        $destination = $destination ?: 'uploads/' . date('Y/m/d/') . $filename;

        try {
            $result = $this->uploadx->upload($source, $destination, [
                'metadata' => [
                    'original_name' => $filename,
                    'client_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
                    'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
                    'upload_time' => date('Y-m-d H:i:s'),
                ],
            ]);

            return [
                'success' => true,
                'file' => [
                    'id' => $result->sessionId(),
                    'name' => $result->filename(),
                    'size' => $result->size(),
                    'type' => $result->mimeType(),
                    'url' => $result->url(),
                    'checksum' => $result->checksum(),
                ],
            ];
        } catch (UploadException $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
                'code' => 'UPLOAD_FAILED',
            ];
        }
    }

    /**
     * Handle chunk upload from web (JavaScript File API)
     */
    public function handleChunkUpload(array $input): array
    {
        $action = $input['action'] ?? 'upload';

        switch ($action) {
            case 'init':
                return $this->initChunkUpload($input);
            case 'upload':
                return $this->uploadChunk($input);
            case 'resume':
                return $this->resumeUpload($input);
            case 'cancel':
                return $this->cancelUpload($input);
            default:
                return [
                    'success' => false,
                    'error' => 'Unknown action',
                    'code' => 'UNKNOWN_ACTION',
                ];
        }
    }

    private function initChunkUpload(array $input): array
    {
        $filename = $input['filename'] ?? '';
        $fileSize = (int)($input['fileSize'] ?? 0);
        $totalChunks = (int)($input['totalChunks'] ?? 0);

        if (empty($filename) || $fileSize <= 0 || $totalChunks <= 0) {
            return [
                'success' => false,
                'error' => 'Invalid upload parameters',
                'code' => 'INVALID_PARAMS',
            ];
        }

        try {
            $init = $this->uploadx->initChunkUpload(
                $filename, $fileSize, $totalChunks,
                ['client_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
                 'start_time' => date('Y-m-d H:i:s')]
            );
            return [
                'success' => true,
                'session_id' => $init['session_id'],
                'chunk_size' => $this->config['chunk_size'],
            ];
        } catch (\Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
                'code' => 'INIT_FAILED',
            ];
        }
    }

    private function uploadChunk(array $input): array
    {
        $sessionId = $input['session_id'] ?? '';
        $chunkNumber = (int)($input['chunk_number'] ?? 0);
        $totalChunks = (int)($input['total_chunks'] ?? 0);
        $totalSize = (int)($input['total_size'] ?? 0);
        $filename = $input['filename'] ?? '';

        $chunkData = $input['data'] ?? '';
        if (empty($chunkData) && isset($_FILES['chunk'])) {
            $chunkData = file_get_contents($_FILES['chunk']['tmp_name']);
        }
        if (isset($input['data_base64'])) {
            $chunkData = base64_decode($input['data_base64']);
        }

        if (empty($chunkData)) {
            return ['success' => false, 'error' => 'No chunk data received', 'code' => 'NO_DATA'];
        }

        try {
            $result = $this->uploadx->uploadChunk([
                'session_id' => $sessionId,
                'chunk_number' => $chunkNumber,
                'total_chunks' => $totalChunks,
                'chunk_size' => strlen($chunkData),
                'total_size' => $totalSize,
                'data' => $chunkData,
                'checksum' => HashHelper::chunkChecksum($chunkData),
                'filename' => $filename,
                'mime_type' => $input['mime_type'] ?? 'application/octet-stream',
            ]);

            return [
                'success' => true,
                'chunk_number' => $chunkNumber,
                'session_id' => $sessionId,
                'progress' => ($chunkNumber / $totalChunks) * 100,
                'file' => $result->isSuccessful() ? [
                    'url' => $result->url(),
                    'filename' => $result->filename(),
                    'size' => $result->size(),
                ] : null,
            ];
        } catch (InvalidChunkException $e) {
            return ['success' => false, 'error' => "Chunk {$chunkNumber} validation failed: {$e->getMessage()}", 'code' => 'CHUNK_INVALID'];
        } catch (UploadException $e) {
            return ['success' => false, 'error' => $e->getMessage(), 'code' => 'CHUNK_FAILED'];
        }
    }

    private function resumeUpload(array $input): array
    {
        try {
            $session = $this->uploadx->resume($input['session_id'] ?? '');
            return [
                'success' => true,
                'session' => $session,
                'missing_chunks' => $session['missing_chunks'],
                'progress' => $session['progress'],
            ];
        } catch (UploadException $e) {
            return ['success' => false, 'error' => $e->getMessage(), 'code' => 'RESUME_FAILED'];
        }
    }

    private function cancelUpload(array $input): array
    {
        try {
            $this->uploadx->cancel($input['session_id'] ?? '');
            return ['success' => true, 'message' => 'Upload cancelled successfully'];
        } catch (\Exception $e) {
            return ['success' => false, 'error' => $e->getMessage(), 'code' => 'CANCEL_FAILED'];
        }
    }

    public function getMetrics(): array
    {
        return $this->uploadx->getMetrics();
    }

    public function cleanup(int $maxAge = 86400): int
    {
        return $this->uploadx->cleanup($maxAge);
    }
}

// Example usage
$handler = new WebUploadHandler([
    'upload_dir' => __DIR__ . '/uploads/web',
    'max_file_size' => 50 * 1024 * 1024,
    'allowed_types' => ['image/jpeg', 'image/png', 'application/pdf'],
]);

// Single upload (simulated from $_FILES)
$result = $handler->handleSingleUpload([
    'name' => 'test-image.jpg',
    'type' => 'image/jpeg',
    'tmp_name' => __DIR__ . '/test-files/photo.jpeg',
    'error' => UPLOAD_ERR_OK,
    'size' => filesize(__DIR__ . '/test-files/photo.jpeg'),
], 'images/test-image.jpg');

// Chunk upload init (simulated from JavaScript)
$initResult = $handler->handleChunkUpload([
    'action' => 'init',
    'filename' => 'large-video.mp4',
    'fileSize' => 10485760, // 10MB
    'totalChunks' => 2,
]);

Configuration

Option Default Description
storage_driver local Storage driver (local, s3, minio)
storage_path ./uploads Base path for local storage
storage_url /uploads Base URL for file access
chunk_size 5242880 Default chunk size (5MB)
max_file_size 104857600 Maximum file size (100MB)
allowed_mimes [] Allowed MIME types (empty = all)
temp_directory sys_get_temp_dir() . '/uploadx' Temporary directory for chunks
sessions_directory sys_get_temp_dir() . '/uploadx/sessions' Directory for session data
secret_key '' Secret key for signed uploads
log_file '' Path to log file
log_level Warning Logging level

Storage Drivers

Local Storage

Default storage driver. Stores files on the local filesystem.

use UploadX\Storage\LocalStorage;

$storage = new LocalStorage(
    basePath: '/path/to/uploads',
    baseUrl: '/uploads',
    permissions: 0755
);

Supported operations:

  • store($sourcePath, $destinationPath, $options) - Store a file
  • retrieve($path) - Retrieve a file
  • delete($path) - Delete a file
  • exists($path) - Check file existence
  • url($path) - Get file URL
  • size($path) - Get file size
  • mimeType($path) - Get MIME type
  • temporaryUrl($path, $expiresIn) - Generate temporary URL
  • name() - Driver name
  • basePath() - Base path

Amazon S3

Requires AWS SDK. Stores files on Amazon S3.

use UploadX\Storage\S3Storage;

$storage = new S3Storage(
    config: [
        'version' => 'latest',
        'region' => getenv('AWS_REGION') ?: 'us-east-1',
        'credentials' => [
            'key' => getenv('AWS_KEY') ?: 'YOUR_AWS_KEY',
            'secret' => getenv('AWS_SECRET') ?: 'YOUR_AWS_SECRET',
        ],
    ],
    bucket: getenv('S3_BUCKET') ?: 'my-bucket',
    prefix: 'uploads'
);

Additional options when storing:

  • storage_class - Storage class (STANDARD, REDUCED_REDUNDANCY, etc.)
  • acl - Access control (private, public-read, etc.)
  • metadata - Custom metadata
  • server_side_encryption - Server-side encryption (AES256)

MinIO

Compatible with S3 API. Stores files on MinIO server.

use UploadX\Storage\MinioStorage;

$storage = new MinioStorage(
    endpoint: getenv('MINIO_ENDPOINT') ?: 'http://localhost:9000',
    accessKey: getenv('MINIO_ACCESS_KEY') ?: 'minioadmin',
    secretKey: getenv('MINIO_SECRET_KEY') ?: 'minioadmin',
    bucket: getenv('MINIO_BUCKET') ?: 'uploads',
    region: 'us-east-1',
    prefix: 'uploads',
    usePathStyleEndpoint: true
);

MinIO-specific methods:

  • ensureBucketExists() - Create bucket if it doesn't exist

Validation

MIME Type Validation

$result = $uploadx->upload($source, $destination, [
    'allowed_mimes' => ['image/jpeg', 'image/png', 'application/pdf'],
]);

Size Validation

$result = $uploadx->upload($source, $destination, [
    'max_size' => 10485760 // 10MB
]);

Checksum Validation

$expectedChecksum = hash_file('sha256', $sourceFile);

$result = $uploadx->upload($source, $destination, [
    'checksum' => $expectedChecksum,
]);

Multiple Validations

$result = $uploadx->upload($source, $destination, [
    'allowed_mimes' => ['image/jpeg', 'image/png'],
    'max_size' => 5 * 1024 * 1024, // 5MB
    'checksum' => hash_file('sha256', $source),
    'secure_filename' => true,
    'metadata' => [
        'user_id' => 123,
        'category' => 'photos',
    ],
]);

Events

UploadX dispatches PSR-14 events during the upload process:

  • UploadStarted - When upload begins
  • ChunkUploaded - When a chunk is received
  • UploadCompleted - When upload completes
  • UploadFailed - When upload fails
use Symfony\Component\EventDispatcher\EventDispatcher;
use UploadX\Events\UploadCompleted;

$dispatcher = new EventDispatcher();
$dispatcher->addListener(UploadCompleted::class, function (UploadCompleted $event) {
    echo "File {$event->filename()} has been uploaded successfully!\n";
    echo "Size: {$event->fileSize()} bytes\n";
    echo "Duration: {$event->durationSeconds()} seconds\n";
});

$uploadx = new UploadX(eventDispatcher: $dispatcher);

Error Handling

UploadX throws specific exceptions for different error types:

  • UploadException - General upload error (codes: 1001-1005)
  • InvalidChunkException - Chunk validation error
  • StorageException - Storage backend error
  • ChecksumException - Checksum validation error
try {
    $result = $uploadx->upload($source, $destination);
} catch (UploadException $e) {
    echo "Upload failed: " . $e->getMessage();
    echo "Code: " . $e->getCode();
} catch (StorageException $e) {
    echo "Storage error: " . $e->getMessage();
} catch (ChecksumException $e) {
    echo "Checksum mismatch!\n";
    echo "Expected: " . $e->expected() . "\n";
    echo "Actual: " . $e->actual();
}

API Reference

UploadX (Main Class)

Method Description
upload($source, $destination, $options) Upload a single file
uploadMultiple($files) Upload multiple files
initChunkUpload($filename, $fileSize, $totalChunks, $metadata) Initialize chunk upload
uploadChunk($chunkData) Upload a single chunk
resume($sessionId) Resume interrupted upload
cancel($sessionId) Cancel upload
progress() Get upload progress
getMetrics() Get upload metrics
cleanup($maxAge) Clean up old sessions
storage() Get storage instance
manager() Get upload manager

UploadResult

Method Description
path() Storage path
filename() Original filename
size() File size in bytes
mimeType() MIME type
url() Public URL
storageDriver() Storage driver name
sessionId() Upload session ID
checksum() SHA-256 checksum
duration() Upload duration in seconds
metadata() Upload metadata
isSuccessful() Whether the upload succeeded
toArray() Convert to array

UploadSession

Method Description
sessionId() Session identifier
filename() Filename
totalSize() Total file size
totalChunks() Total chunks
uploadedChunks() Uploaded chunks
missingChunks() Missing chunks
progress() Progress percentage
duration() Session duration
averageSpeed() Average speed
isCompleted() Whether session is complete
isExpired($timeout) Whether session is expired
toArray() Convert to array

Chunk Interface

Method Description
chunkId() Chunk identifier
sessionId() Session identifier
chunkNumber() Chunk number (1-based)
totalChunks() Total chunks
chunkSize() Chunk size
totalSize() Total file size
data() Chunk data
checksum() Chunk checksum
originalFilename() Original filename
mimeType() MIME type
offset() Offset in file
validate() Validate checksum

Storage Interface

Method Description
store($source, $destination, $options) Store a file
retrieve($path) Retrieve a file
delete($path) Delete a file
exists($path) Check existence
url($path) Get URL
size($path) Get size
mimeType($path) Get MIME type
temporaryUrl($path, $expiresIn) Generate temporary URL
name() Driver name
basePath() Base path

License

MIT License. See LICENSE file for more information.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: Unknown
  • 更新时间: 2026-06-12

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固