MineAdmin 交流群

官方QQ群: 150105478

Skip to content

File Upload

Backend Upload

TIP

File upload is built by MineAdmin integrating the mineadmin/upload component.

MineAdmin provides a default server-side file upload logic. The interface /admin/attachment/upload is used, and after successful upload, it is integrated into the resource manager.

php
#[Post(
    path: '/admin/attachment/upload',
    operationId: 'UploadAttachment',
    summary: 'Upload Attachment',
    security: [['Bearer' => [], 'ApiKey' => []]],
    tags: ['Data Center'],
)]
#[Permission(code: 'attachment:upload')]
#[ResultResponse(instance: new Result())]
public function upload(UploadRequest $request): Result
{
    $uploadFile = $request->file('file');
    $newTmpPath = sys_get_temp_dir() . '/' . uniqid() . '.' . $uploadFile->getExtension();
    $uploadFile->moveTo($newTmpPath);
    $splFileInfo = new SplFileInfo($newTmpPath, '', '');
    return $this->success(
        $this->service->upload($splFileInfo, $uploadFile, $this->currentUser->id())
    );
}
php

namespace App\Service;

use App\Model\Attachment;
use App\Repository\AttachmentRepository;
use Hyperf\HttpMessage\Upload\UploadedFile;
use Mine\Upload\UploadInterface;
use Symfony\Component\Finder\SplFileInfo;

/**
 * @extends IService<AttachmentRepository>
 */
final class AttachmentService extends IService
{
    public function __construct(
        protected readonly AttachmentRepository $repository,
        protected readonly UploadInterface $upload
    ) {}

    public function upload(SplFileInfo $fileInfo, UploadedFile $uploadedFile, int $userId): Attachment
    {
        $fileHash = md5_file($fileInfo->getRealPath());
        if ($attachment = $this->repository->findByHash($fileHash)) {
            return $attachment;
        }
        $upload = $this->upload->upload(
            $fileInfo,
        );
        return $this->repository->create([
            'created_by' => $userId,
            'origin_name' => $uploadedFile->getClientFilename(),
            'storage_mode' => $upload->getStorageMode(),
            'object_name' => $upload->getObjectName(),
            'mime_type' => $upload->getMimeType(),
            'storage_path' => $upload->getStoragePath(),
            'hash' => $fileHash,
            'suffix' => $upload->getSuffix(),
            'size_byte' => $upload->getSizeByte(),
            'size_info' => $upload->getSizeInfo(),
            'url' => $upload->getUrl(),
        ]);
    }

Replace Local Storage with OSS Storage

In daily business scenarios, files are usually stored on OSS. At this point, the default file upload processing needs to be replaced. Taking Alibaba Cloud as an example, first, we need to configure the config/autoload/file.php file. Add an Alibaba Cloud channel. Then, create a new AliyunUploadSubscribe to replace the default UploadSubscribe in config/autoload/listeners.php and specify the Alibaba Cloud channel.

php
<?php
// app/Http/Common/Subscribe/AliyunUploadSubscribe.php
declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */

namespace App\Http\Common\Subscriber\Upload;

use Mine\Upload\Listener\UploadListener as AbstractUploadListener;

final class AliyunUploadSubscribe extends AbstractUploadListener
{
    public const ADAPTER_NAME = 'oss';
}
php
<?php
// config/autoload/file.php
declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */
use Hyperf\Filesystem\Adapter\AliyunOssAdapterFactory;
use Hyperf\Filesystem\Adapter\CosAdapterFactory;
use Hyperf\Filesystem\Adapter\FtpAdapterFactory;
use Hyperf\Filesystem\Adapter\LocalAdapterFactory;
use Hyperf\Filesystem\Adapter\MemoryAdapterFactory;
use Hyperf\Filesystem\Adapter\QiniuAdapterFactory;
use Hyperf\Filesystem\Adapter\S3AdapterFactory;

return [
    'default' => 'local',
    'storage' => [
        'local' => [
            'driver' => LocalAdapterFactory::class,
            'root' => BASE_PATH . '/storage/uploads',
            'public_url' => env('APP_URL') . '/uploads',
        ],
        'oss' => [
            'driver' => AliyunOssAdapterFactory::class,
            'accessId' => '',
            'accessSecret' => '',
            'bucket' => '',
            'endpoint' => '',
            'domain' => '',
            'schema' => 'http://',
            'isCName' => false,
            // 'timeout'        => 3600,
            // 'connectTimeout' => 10,
            // 'token'          => '',
        ],
    ],
];
php
// config/autoload/listeners.php
<?php

declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */
use Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler;
use Mine\Core\Subscriber\BootApplicationSubscriber;
use Mine\Core\Subscriber\DbQueryExecutedSubscriber;
use Mine\Core\Subscriber\FailToHandleSubscriber;
use Mine\Core\Subscriber\QueueHandleSubscriber;
use Mine\Core\Subscriber\ResumeExitCoordinatorSubscriber;
use Mine\Core\Subscriber\Upload\UploadSubscriber;
use App\Http\Common\Subscriber\AliyunUploadSubscribe;
use Mine\Support\Listener\RegisterBlueprintListener;

return [
    ErrorExceptionHandler::class,
    // Default file upload
    // UploadSubscriber::class,
    // Alibaba Cloud OSS upload
    AliyunUploadSubscribe::class,
    // Handle application startup
    BootApplicationSubscriber::class,
    // Handle SQL execution
    DbQueryExecutedSubscriber::class,
    // Handle command exceptions
    FailToHandleSubscriber::class,
    // Handle worker exit
    ResumeExitCoordinatorSubscriber::class,
    // Handle queue
    QueueHandleSubscriber::class,
    // Register new Blueprint macros
    RegisterBlueprintListener::class,
];

Modify Default Upload File Naming and Directory Naming

The default file naming and directory naming are implemented by Mine\Upload\Listener\UploadListener->generatorPath() and Mine\Upload\Listener\UploadListener->generatorId(). All upload processing classes inherit from this class. Taking the previous example of replacing OSS storage, you only need to replace these two methods in your upload processing class.

php
<?php

declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */

namespace Mine\Core\Subscriber\Upload;

use Hyperf\Stringable\Str;
use Mine\Upload\Listener\UploadListener as AbstractUploadListener;

final class UploadSubscriber extends AbstractUploadListener
{
    public const ADAPTER_NAME = 'local';

    protected function generatorId(): string
    {
        // Generate file name, random string length is 10
        return Str::random(10);
    }

    protected function generatorPath(): string
    {
        // Directory format: year/month/day
        return date('Y/m/d');
    }

}

Processing Flow

Frontend calls the interface /admin/attachment/upload, passing the parameter file, which is of type file. The server-side file controller calls the file service to process the file passed from the frontend. The file service checks if the hash value of the uploaded file has been uploaded before. If it has, it queries the database and returns the information from the previous upload. If it hasn't, it calls the UploadInterface instance to dispatch an UploadEvent. After dispatching, it checks if UploadEvent->isUploaded() is successful. If the upload is successful, it returns the Upload upload instance. If the upload is not successful, it throws an upload failure exception.

Flowchart

plantuml diagram

Sequence Diagram

plantuml diagram

Frontend Direct Upload to OSS

TODO

致力于为品牌和企业创造价值