itstructure/laravel-media-file-uploader

用于 Laravel 框架的媒体文件上传器

1.0.3 2024-07-15 13:39 UTC

This package is auto-updated.

Last update: 2024-09-23 08:59:48 UTC


README

Latest Stable Version Latest Unstable Version License Total Downloads Build Status Scrutinizer Code Quality

1 简介

此包用于将不同的媒体文件上传到本地或远程的 Amazon S3 存储。

MFU logotip

2 要求

  • laravel 6+ | 7+ | 8+ | 9+ | 10+ | 11+
  • Bootstrap 4 用于样式
  • JQuery
  • php >= 7.2.5
  • composer
  • 以下 PHP 扩展之一:GD|Imagick|Gmagick

3 安装

3.1 安装包

从远程 Packagist 仓库的通用部分

运行 composer 命令

composer require itstructure/laravel-media-file-uploader "^1.0.3"

如果您正在从本地服务器目录测试此包

在应用程序 composer.json 文件中设置仓库,如下例所示

"repositories": [
    {
        "type": "path",
        "url": "../laravel-media-file-uploader",
        "options": {
            "symlink": true
        }
    }
],

这里,

../laravel-media-file-uploader - 目录路径,与应用程序处于同一目录级别,并包含 MFU 包。

然后运行命令

composer require itstructure/laravel-media-file-uploader:dev-main --prefer-source

3.3 发布文件 - 必需部分

  • 要发布配置,运行命令

    php artisan uploader:publish --only=config

    它将配置文件存储到 config 文件夹。

    否则,您可以使用 --force 参数重写已发布的文件。

  • 要发布迁移,运行命令

    php artisan uploader:publish --only=migrations

    它将迁移文件存储到 database/migrations 文件夹。

    否则,您可以使用 --force 参数重写已发布的文件。

  • 要发布资产(js 和 css),运行命令

    php artisan uploader:publish --only=assets

    它将 js 和 css 文件存储到 public/vendor/uploader 文件夹。

    否则,您可以使用 --force 参数重写已发布的文件。

3.4 发布文件 - 自定义部分

  • 要发布视图,运行命令

    php artisan uploader:publish --only=views

    它将视图文件存储到 resources/views/vendor/uploader 文件夹。

    否则,您可以使用 --force 参数重写已发布的文件。

  • 要发布翻译,运行命令

    php artisan uploader:publish --only=lang

    它将翻译文件存储到 resources/lang/vendor/uploader 文件夹。

    否则,您可以使用 --force 参数重写已发布的文件。

3.5 发布文件 - 如有需要,发布所有部分

  • 要发布所有部分,请运行不带 only 参数的命令

    php artisan uploader:publish

    否则,您可以使用 --force 参数重写已发布的文件。

3.6 运行迁移

  • 运行命令

php artisan migrate

4 配置

4.1 设置默认文件系统磁盘

在配置文件 filesystems.php 中设置以下自定义设置(设置您希望设置的默认磁盘)并创建所需的基上传文件夹

'default' => env('FILESYSTEM_DISK', 'local'),

'disks' => [
    'local' => [
        'driver' => 'local',
        'root' => storage_path('app/uploads'),
        'throw' => false,
        'url' => env('APP_URL') . '/storage/',
    ],
    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url' => env('AWS_URL'),
        'endpoint' => env('AWS_ENDPOINT'),
        'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
        'throw' => false,
        'visibility' => 'public',
    ],
],

'links' => [
    public_path('storage') => storage_path('app/uploads'),
],

运行命令以创建符号链接

php artisan storage:link

4.2 设置资产

注意!此选项仅在以下情况下需要

  • 如果您在应用程序 html 表单中使用 File setter(请参阅 5.3 深入研究 / 5.3.3 使用 FileSetter 点)。

  • 如果您使用 相册编辑器(请参阅 5.1 路由部分)。

请确保您在应用程序中使用 Bootstrap 4 进行样式和 JQuery

4.2.1 自定义主体布局情况

因此,在您的应用程序布局的 head 标签之间设置以下 css 资产

<link rel="stylesheet" href="{{ asset('vendor/uploader/css/modal.css') }}">
<link rel="stylesheet" href="{{ asset('vendor/uploader/css/file-area.css') }}">

在您的应用程序布局的 body 标签的末尾设置以下 js 资产

<script src="{{ asset('vendor/uploader/js/jquery.min.js') }}"></script>
<script src="{{ asset('vendor/uploader/js/file-setter.js') }}"></script>

注意:vendor/uploader/js/jquery.min.js 仅当您的应用程序中缺少 JQuery 时才需要。

4.2.2 如果您在项目中使用 AdminLTE

'plugins' => [
    'Uploader' => [
        'active' => true,
        'files' => [
            [
                'type' => 'css',
                'asset' => true,
                'location' => '/vendor/uploader/css/modal.css',
            ],
            [
                'type' => 'css',
                'asset' => true,
                'location' => '/vendor/uploader/css/file-area.css',
            ],
            [
                'type' => 'js',
                'asset' => true,
                'location' => '/vendor/uploader/js/file-setter.js',
            ],
        ],
    ],
]

注意:不建议在 AdminLTE 配置文件中使用 asset(),因为这可能在您稍后运行例如 composer install 时导致与 UrlGenerator.php 相关的错误。

4.3 修改 uploader.php 配置文件。

此文件是 直观的

但是在这个阶段,请注意下一个重要的选项

  • 路由中间件。默认情况下为空数组,您可以设置如下

    'routing' => [
        'middlewares' => ['auth'],
    ],

    这些中间件将在 routes/uploader.php 中应用。

  • 相册布局(如果使用相册编辑器则必须设置)。默认情况下为空字符串,您可以设置如下

    'albums' => [
        'layout' => 'adminlte::page', // In case if you use AdminLTE. Or you can set your special layout.
    ],

5 使用方法

5.1 路由部分

已经集成了基础 MFU 路由来管理 文件相册。请参阅 routes/uploader.php 包文件。

以下路由默认可用

  • 上传部分

    对于 GET 请求方法

    • http://example-domain.com/uploader/file/download/{id} (名称: uploader_file_download)

    对于 POST 请求方法

    • http://example-domain.com/uploader/file/upload (名称: uploader_file_upload)

      • 必需的请求字段是 file
      • 可选/必需的请求元数据字段: [data]alt, [data]title, [data]description, [data]owner_id, [data]owner_name, [data]owner_attribute, [data]needed_file_type, [data]sub_dir
    • http://example-domain.com/uploader/file/update (名称: uploader_file_update)

      • 必需的请求字段是 id
      • 可选的请求字段是 file
      • 可选/必需的请求元数据字段: [data]alt, [data]title, [data]description, [data]needed_file_type, [data]sub_dir
    • http://example-domain.com/uploader/file/delete (名称: uploader_file_delete)

      • 必需的请求字段是 id
    • http://example-domain.com/uploader/file/preview (名称: uploader_file_preview)

      • 必需的请求字段是 id
      • 可选的请求字段是 location。有关 preview 选项的更多信息,请参阅配置文件 uploader.php(其中包含针对具体文件类型及其预览的一些 html 属性)。如果没有设置,返回的预览将具有与 Previewer::LOCATION_FILE_INFO 相符的 html 属性。如果它是一个图像,Previewer 将返回一个带有大小并根据配置中的 thumbAlias 选项的图像。如果没有设置 thumbAlias 或没有这样的位置,Previewer 将返回一个根据 SaveProcessor::THUMB_ALIAS_SMALL 的大小调整的图像。

    注意

    • 如果上传的文件是 图像,则会根据 thumbSizes 配置选项中的设置创建额外的缩略图。
    • 请求元数据字段的必要性/可选性在配置文件 uploader.php 中的 metaDataValidationRules 选项中设置。
    • 如果 checkExtensionByFileType 选项为 true,则自动要求 [data]needed_file_type
  • 管理部分

    对于 GET 请求方法

    • http://example-domain.com/uploader/managers/file-list (名称: uploader_file_list_manager)
    • http://example-domain.com/uploader/managers/file-upload (名称: uploader_file_upload_manager)
    • http://example-domain.com/uploader/managers/file-edit/{id} (名称: uploader_file_edit_manager)

    对于 POST 请求方法

    • http://example-domain.com/uploader/managers/file-list/delete (名称: uploader_file_list_delete)
      • 必需的请求字段是 items - 它是一个文件 ID 的数组。
  • 相册编辑器部分

    • 图片相册

      对于 GET 请求方法

      • http://example-domain.com/uploader/albums/image/list (名称: uploader_image_album_list)
      • http://example-domain.com/uploader/albums/image/create (名称: uploader_image_album_create)
      • http://example-domain.com/uploader/albums/image/edit/{id} (名称: uploader_image_album_edit)
      • http://example-domain.com/uploader/albums/image/view/{id} (名称: uploader_image_album_view)

      对于 POST 请求方法

      • http://example-domain.com/uploader/albums/image/store (名称: uploader_image_album_store)
      • http://example-domain.com/uploader/albums/image/update/{id} (名称: uploader_image_album_update)
      • http://example-domain.com/uploader/albums/image/delete (名称: uploader_image_album_delete)
    • 音频相册

      对于 GET 请求方法

      • http://example-domain.com/uploader/albums/audio/list (名称: uploader_audio_album_list)
      • http://example-domain.com/uploader/albums/audio/create (名称: uploader_audio_album_create)
      • http://example-domain.com/uploader/albums/audio/edit/{id} (名称: uploader_audio_album_edit)
      • http://example-domain.com/uploader/albums/audio/view/{id}(名称:uploader_audio_album_view

      对于 POST 请求方法

      • http://example-domain.com/uploader/albums/audio/store(名称:uploader_audio_album_store
      • http://example-domain.com/uploader/albums/audio/update/{id}(名称:uploader_audio_album_update
      • http://example-domain.com/uploader/albums/audio/delete(名称:uploader_audio_album_delete
    • 视频专辑

      对于 GET 请求方法

      • http://example-domain.com/uploader/albums/video/list(名称:uploader_video_album_list
      • http://example-domain.com/uploader/albums/video/create(名称:uploader_video_album_create
      • http://example-domain.com/uploader/albums/video/edit/{id}(名称:uploader_video_album_edit
      • http://example-domain.com/uploader/albums/video/view/{id}(名称:uploader_video_album_view

      对于 POST 请求方法

      • http://example-domain.com/uploader/albums/video/store(名称:uploader_video_album_store
      • http://example-domain.com/uploader/albums/video/update/{id}(名称:uploader_video_album_update
      • http://example-domain.com/uploader/albums/video/delete(名称:uploader_video_album_delete
    • 应用程序专辑

      对于 GET 请求方法

      • http://example-domain.com/uploader/albums/application/list(名称:uploader_application_album_list
      • http://example-domain.com/uploader/albums/application/create(名称:uploader_application_album_create
      • http://example-domain.com/uploader/albums/application/edit/{id}(名称:uploader_application_album_edit
      • http://example-domain.com/uploader/albums/application/view/{id}(名称:uploader_application_album_view

      对于 POST 请求方法

      • http://example-domain.com/uploader/albums/application/store(名称:uploader_application_album_store
      • http://example-domain.com/uploader/albums/application/update/{id}(名称:uploader_application_album_update
      • http://example-domain.com/uploader/albums/application/delete(名称:uploader_application_album_delete
    • 文档专辑

      对于 GET 请求方法

      • http://example-domain.com/uploader/albums/word/list(名称:uploader_word_album_list
      • http://example-domain.com/uploader/albums/word/create(名称:uploader_word_album_create
      • http://example-domain.com/uploader/albums/word/edit/{id}(名称:uploader_word_album_edit
      • http://example-domain.com/uploader/albums/word/view/{id}(名称:uploader_word_album_view

      对于 POST 请求方法

      • http://example-domain.com/uploader/albums/word/store(名称:uploader_word_album_store

      • http://example-domain.com/uploader/albums/word/update/{id}(名称:uploader_word_album_update

      • http://example-domain.com/uploader/albums/word/delete(名称:uploader_word_album_delete

        .........................................................

        ……下一专辑……

        .........................................................

等等...routes/uploader.php 包文件中查看下一专辑的路由,包括 ExcelVisioPowerPointPDFText其他 专辑。它们遵循相似的原则。

专辑编辑器 POST 请求方法的字段对所有专辑都相同

  • 存储所需的请求字段:标题描述
  • 更新所需的请求字段:标题描述。字段 id 在路由 URL 中。
  • 删除所需的请求字段:items - 它是包含专辑 ID 的数组。

5.2 简便快捷的方式

5.2.1 访问文件列表管理器

  • 直接访问 uploader_file_list_manager 路由:http://example-domain.com/uploader/managers/file-list

  • 使用 iframe 访问文件列表

<section class="content container-fluid">
    <div class="row">
        <div class="col-12 mt-1">
            <iframe src="{{ route('uploader_file_list_manager') }}" frameborder="0" style="width: 100%; min-height: 800px"></iframe>
        </div>
    </div>
</section>

MFU file list manager

5.2.2 访问文件上传管理器

如果在文件列表管理器中点击绿色的 Uploader 按钮,您将转到 uploader_file_upload_manager 路由:http://example-domain.com/uploader/managers/file-upload

MFU file upload manager

5.2.3 访问文件编辑管理器

如果在文件列表管理器中点击绿色的编辑按钮,您将转到 uploader_file_edit_manager 路由:http://example-domain.com/uploader/managers/file-edit/{id}

route('uploader_file_edit_manager', ['id' => 1])

MFU file edit manager

5.2.4 访问媒体文件预览

如果您通过 Itstructure\MFU\Models\Mediafile 模型实体获得了媒体文件条目 $mediaFile

<a href="{{ $mediaFile->getOriginalUrl() }}" target="_blank">
    {!! \Itstructure\MFU\Facades\Previewer::getPreviewHtml($mediaFile, \Itstructure\MFU\Services\Previewer::LOCATION_FILE_INFO) !!}
</a>

您可以使用以下选项之一

\Itstructure\MFU\Services\Previewer::LOCATION_FILE_ITEM \Itstructure\MFU\Services\Previewer::LOCATION_FILE_INFO \Itstructure\MFU\Services\Previewer::LOCATION_EXISTING

5.2.5 下载媒体文件

使用路由

route('uploader_file_download', ['id' => 1])

5.2.6 访问专辑编辑器

只需使用上述 5.1 路由部分 中的 专辑编辑器 部分描述的专辑路由。

但请注意!您必须为专辑编辑器设置布局

'albums' => [
    'layout' => 'adminlte::page',
],

adminlte::page 用于您使用 AdminLTE 的情况。或者您也可以设置您自己的特殊布局。

图片专辑列表示例看起来像这样

MFU album list

图片专辑编辑页面示例看起来像这样

MFU album edit

5.3 深入挖掘

5.3.1 数据库结构

MFU db

5.3.2 简单地说,上传过程的短架构结构和请求方式

  1. 调用 UploadController 方法。

  2. 在控制器方法中调用来自 Itstructure\MFU\Facades\Uploader 门面的静态方法。

  3. 获取 Uploader 服务实例 Itstructure\MFU\Services\Uploader::getInstance($config) 并在此处调用门面的方法。

  4. 获取所需处理器的实例,并将配置数据从服务设置到此处

    Itstructure\MFU\Processors\UploadProcessor

    Itstructure\MFU\Processors\UpdateProcessor

    Itstructure\MFU\Processors\DeleteProcessor.

  5. 设置处理参数,然后调用其 run() 方法。

查看核心内部。

5.3.3 使用 FileSetter

FileSetter 在保存某些实体(如页面、目录、产品等)之前,将文件 ID 设置到表单字段并将文件预览设置到特殊容器中时是必需的。

以下是用于 缩略图 的 FileSetter 示例

@php
    $thumbModel = isset($model) ? $model->getThumbnailModel() : null;
@endphp
<div id="{{ isset($model) ? 'thumbnail_container_' . $model->id : 'thumbnail_container' }}">
    @if(!empty($thumbModel))
        <a href="{{ $thumbModel->getOriginalUrl() }}" target="_blank">
            {!! \Itstructure\MFU\Facades\Previewer::getPreviewHtml($thumbModel, \Itstructure\MFU\Services\Previewer::LOCATION_FILE_INFO) !!}
        </a>
    @endif
</div>
<div id="{{ isset($model) ? 'thumbnail_title_' . $model->id : 'thumbnail_title' }}">
    @if(!empty($thumbModel))
        {{ $thumbModel->title }}
    @endif
</div>
<div id="{{ isset($model) ? 'thumbnail_description_' . $model->id : 'thumbnail_description' }}">
    @if(!empty($thumbModel))
        {{ $thumbModel->description }}
    @endif
</div>
@php
    $fileSetterConfig = [
        'attribute' => Itstructure\MFU\Processors\SaveProcessor::FILE_TYPE_THUMB,
        'value' => !empty($thumbModel) ? $thumbModel->id : null,
        'openButtonName' => trans('uploader::main.set_thumbnail'),
        'clearButtonName' => trans('uploader::main.clear'),
        'mediafileContainerId' => isset($model) ? 'thumbnail_container_' . $model->id : 'thumbnail_container',
        'titleContainerId' => isset($model) ? 'thumbnail_title_' . $model->id : 'thumbnail_title',
        'descriptionContainerId' => isset($model) ? 'thumbnail_description_' . $model->id : 'thumbnail_description',
        //'callbackBeforeInsert' => 'function (e, v) {console.log(e, v);}',//Custom
        'neededFileType' => Itstructure\MFU\Processors\SaveProcessor::FILE_TYPE_THUMB,
        'subDir' => isset($model) ? $model->getTable() : null
    ];

    $ownerConfig = isset($ownerParams) && is_array($ownerParams) ? array_merge([
        'ownerAttribute' => Itstructure\MFU\Processors\SaveProcessor::FILE_TYPE_THUMB
    ], $ownerParams) : [];

    $fileSetterConfig = array_merge($fileSetterConfig, $ownerConfig);
@endphp
@fileSetter($fileSetterConfig)

从视觉上看,它看起来像这样

MFU file setter

如果单击 设置缩略图 按钮,则将打开文件列表管理器,但带有额外的按钮 "V"

MFU file list with file setter button

此按钮用于选择具体文件,并将其预览插入到 thumbnail_container 中,并将其 ID 插入到由 attribute 选项自动渲染的表单字段中。

查看下一节 5.3.4 以了解如何将所选文件与父所有者关联,例如:页面、产品等...

5.3.4 将媒体文件与父所有者关联

例如,您使用包含 专辑媒体文件Product eloquent 模型。

在产品保存后,通过 owners_albumsowners_mediafiles 数据库关系,可以将专辑和媒体文件与产品关联。

这些关系由 BehaviorMediafileBehaviorAlbum 类自动设置。

namespace App\Models;
    
use Illuminate\Database\Eloquent\Model;
use Itstructure\MFU\Interfaces\BeingOwnerInterface;
use Itstructure\MFU\Behaviors\Owner\{BehaviorMediafile, BehaviorAlbum};
use Itstructure\MFU\Processors\SaveProcessor;
use Itstructure\MFU\Models\Albums\AlbumTyped;
use Itstructure\MFU\Traits\{OwnerBehavior, Thumbnailable};
    
class Product extends Model implements BeingOwnerInterface
{
    use Thumbnailable, OwnerBehavior;
    
    protected $table = 'products';
    
    protected $fillable = ['title', 'alias', 'description', 'price', 'category_id'];
    
    public function getItsName(): string
    {
        return $this->getTable();
    }
    
    public function getPrimaryKey()
    {
        return $this->getKey();
    }
    
    public static function getBehaviorMadiafileAttributes(): array
    {
        return [SaveProcessor::FILE_TYPE_THUMB, SaveProcessor::FILE_TYPE_IMAGE];
    }
    
    public static function getBehaviorAlbumAttributes(): array
    {
        return [AlbumTyped::ALBUM_TYPE_IMAGE];
    }
    
    public static function getBehaviorAttributes(): array
    {
        return array_merge(static::getBehaviorMadiafileAttributes(), static::getBehaviorAlbumAttributes());
    }
    
    protected static function booted(): void
    {
        $behaviorMediafile = BehaviorMediafile::getInstance(static::getBehaviorMadiafileAttributes());
        $behaviorAlbum = BehaviorAlbum::getInstance(static::getBehaviorAlbumAttributes());
    
        static::saved(function (Model $ownerModel) use ($behaviorMediafile, $behaviorAlbum) {
            if ($ownerModel->wasRecentlyCreated) {
                $behaviorMediafile->link($ownerModel);
                $behaviorAlbum->link($ownerModel);
            } else {
                $behaviorMediafile->refresh($ownerModel);
                $behaviorAlbum->refresh($ownerModel);
            }
        });
    
        static::deleted(function (Model $ownerModel) use ($behaviorMediafile, $behaviorAlbum) {
            $behaviorMediafile->clear($ownerModel);
            $behaviorAlbum->clear($ownerModel);
        });
    }
}

主要规则

  • 非常重要,必须从 BeingOwnerInterface 实现!

  • 非常重要,必须使用 OwnerBehavior 特性。一些由 BeingOwnerInterface 要求的 BASE 方法已经存在于这个特性中。

  • 非常重要,必须创建以下方法: getItsName()getPrimaryKey()

  • 非常重要,必须添加带有行为实例的 booted() 方法。

  • 非常重要,必须使用具有用于 FileSetter 的属性列表的 getBehaviorAttributes()

查看核心更深入,想象一下它是如何工作的 :-)

继续...

非常重要,必须正确使用您的应用程序 blade 表单中的 MFU blade 部分正确!

以下是使用 blade 表单的快捷示例

uploader::partials.thumbnail,

uploader::partials.new-mediafiles,

uploader::partials.existing-mediafiles,

uploader::partials.albums-form-list:

<form action="{{ route('admin_product_store') }}" method="post">

<div class="row">
    <div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-4">
        @include('uploader::partials.thumbnail', ['model' => $model ?? null, 'ownerParams' => $ownerParams ?? null])
    </div>
</div>

<div class="row">
    <div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-4">
        <div class="form-group">
            <label for="id_title">Title</label>
            <input id="id_title" type="text" class="form-control @if ($errors->has('title')) is-invalid @endif"
                   name="title" value="{{ old('title', !empty($model) ? $model->title : null) }}" required autofocus>
            @if ($errors->has('title'))
                <div class="invalid-feedback">
                    <strong>{{ $errors->first('title') }}</strong>
                </div>
            @endif
        </div>
    </div>
</div>

..........

..........

<hr />
<h5>{{ trans('uploader::main.new_files') }}</h5>
<div class="row mb-3">
    @include('uploader::partials.new-mediafiles', [
        'fileType' => \Itstructure\MFU\Processors\SaveProcessor::FILE_TYPE_IMAGE,
        'ownerParams' => $ownerParams ?? null
    ])
</div>

@if(!empty($edition))
    <hr />
    <h5>{{ trans('uploader::main.existing_files') }}</h5>
    <div class="row mb-3">
        @include('uploader::partials.existing-mediafiles', [
            'edition' => true,
            'fileType' => \Itstructure\MFU\Processors\SaveProcessor::FILE_TYPE_IMAGE,
            'ownerParams' => $ownerParams ?? null,
            'mediaFiles' => $mediaFiles ?? []
        ])
    </div>
@endif

@if(!empty($allImageAlbums) && !$allImageAlbums->isEmpty())
    <hr />
    <h5>{{ trans('uploader::main.image_albums') }}</h5>
    <div class="row mb-3">
        @include('uploader::partials.albums-form-list', [
            'albums' => $allImageAlbums,
            'edition' => true
        ])
    </div>
@endif

<button class="btn btn-primary" type="submit">Create</button>
<input type="hidden" value="{!! csrf_token() !!}" name="_token">

</form>

为了澄清

通过 fileType 将设置一个字段 image[],它将由 Itstructure\MFU\Traits\OwnerBehavior 特性中的 fill() 方法使用 getBehaviorAttributes() 设置,然后将其值放入在 booted() 调用后保存的 ProductBehaviorMediafile 对象中。然后,将填充表 owners_mediafiles。将在 ProductMediafile 之间创建链接。

产品编辑页面示例看起来像这样

MFU product edit

要了解该示例在全局中的工作方式,请在此处查看真实示例: Laravel Microshop Simple

我希望您会对这个包感到满意。祝您开发顺利!

敬上,Andrey!

许可证

版权所有 © 2024 Andrey Girnik girnikandrey@gmail.com

遵循MIT许可协议。详情请参阅LICENSE.txt文件。