champs-libres/async-uploader-bundle

使用临时URL中间件从浏览器上传文件到Openstack Swift或Amazon S3(稍后)。

1.5.0 2022-03-16 23:04 UTC

This package is auto-updated.

Last update: 2024-09-03 15:23:22 UTC


README

此包帮助管理从浏览器到Openstack Swift服务的文件异步上传。

它避免了直接在服务器上处理文件,这样可以节省磁盘空间和IO、RAM和CPU。

[[目录]]

它是如何工作的?

Schema

当前限制

  • 仅支持openstack

如果您愿意提供帮助以消除这些限制,请不要犹豫,提出一些合并请求。

安装

此功能与symfony 3.4兼容

注册包

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new ChampsLibres\AsyncUploaderBundle\ChampsLibresAsyncUploaderBundle()
        );

        return $bundles;
    }
}

创建将存储文件名的实体

创建实体是最常用的方法,但可能存在其他方法(在redis表中存储名称等)

以下是一个包含一些元数据(例如加密文件的密钥)的示例。

实体必须实现ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface

namespace Chill\DocStoreBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;

/**
 * Represent a document stored in an object store 
 *
 * 
 * @ORM\Entity()
 * @ORM\Table("chill_doc.stored_object")
 * @AsyncFileExists(
 *  message="The file is not stored properly"
 * )
 */
class StoredObject implements AsyncFileInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    
    /**
     * @ORM\Column(type="text")
     */
    private $filename;
    
    /**
     *
     * @var \DateTime
     * @ORM\Column(type="datetime", name="creation_date")
     */
    private $creationDate;
    
    
    public function __construct()
    {
        $this->creationDate = new \DateTime();
    }

    public function getObjectName()
    {
        return $this->filename;
    }

注意:存在一个验证器,在创建或编辑后检查文件在存储库中的存在。此验证器通过此注解添加到类中

 * @AsyncFileExists(
 *  message="The file is not stored properly"
 * )

配置openstack容器

创建一个容器来存储您的数据。目前,仅支持v2身份验证(与OVH提供商)。

必须添加两个参数

  • 一个临时URL密钥,用于签名URL;
  • 允许CORS请求的https头
# load environment variables
swift post mycontainer -m "Temp-URL-Key:mySecretKeyWithAtLeast20Characters"
swift post mycontainer -m "Access-Control-Allow-Origin: https://my.website.com https://my.other.website.com"

为了允许从所有URL(在测试、开发和调试期间更可取)进行CORS请求

swift post mycontainer -m "Access-Control-Allow-Origin: *"

容器必须具有以下数据和元数据

$ swift stat mycontainer
                         Account: AUTH_abcde
                       Container: mycontainer
                         Objects: 0
                           Bytes: 0
                        Read ACL:
                       Write ACL:
                         Sync To:
                        Sync Key:
               Meta Temp-Url-Key: mySecretKeyWithAtLeast20Characters
Meta Access-Control-Allow-Origin: https://my.website.com https://my.other.website.com
                   Accept-Ranges: bytes
                 X-Iplb-Instance: 12308
                X-Storage-Policy: PCS
                   Last-Modified: Tue, 11 Sep 2018 14:52:16 GMT
                    Content-Type: text/plain; charset=utf-8

进一步参考

配置包

# app/config/config.yaml
champs_libres_async_uploader:
    persistence_checker: 'path.to.your_service'
    openstack:
        os_username:          '%env(OS_USERNAME)%' # Required
        os_password:          '%env(OS_PASSWORD)%' # Required
        os_tenant_id:         '%env(OS_TENANT_ID)%' # Required
        os_region_name:       '%env(OS_REGION_NAME)%' # Required
        os_auth_url:          '%env(OS_AUTH_URL)%' # Required
    temp_url:
        temp_url_key:         '%env(ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
        container:            '%env(ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' #Required
        temp_url_base_path:   '%env(ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required. Do not forget a trailing slash
        max_post_file_size:   15000000 # 15Mo, exprimés en bytes
        max_expires_delay:    180
        max_submit_delay:     3600

注意 不要忘记使用参数 temp_url_base_path 时的尾部斜杠

表单中的使用

可以使用AsyncUploaderType生成一个隐藏字段,并存储文件名

namespace Chill\DocStoreBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType;


/**
 * Form type which allow to join a document 
 *
 */
class StoredObjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('filename', AsyncUploaderType::class)
            ;
    }
}

浏览器负责

  1. 使用此URL获取上传文件的签名

    /asyncupload/temp_url/generate/post?expires_delay=180&submit_delay=3600

    参数

    • expires_delay:签名过期前的延迟,以秒为单位;
    • submit_delay:在文件到达Openstack之前检查文件的延迟。此验证应在服务器端完成。

    示例响应

     {
    
         "method": "POST",
         "max_file_size": 15000000,
         "max_file_count": 1,
         "expires": 1592301124,
         "submit_delay": 3600,
         "redirect": "",
         "prefix": "TUPyhlXvoApgJim",
         "url": "https://storage.gra.cloud.ovh.net/v1/AUTH_c123456/container/TUPyhlXvoApgJim",
         "signature": "abcdefghijklmnopqrstuvwxyz01234567890123"
    
     }
    
  2. 为文件名生成随机后缀;
  3. 使用“数据”元素中提供的“前缀”、“密钥”和“签名”将文件推送到Openstack容器中。

    文件名将由“数据”元素中提供的“前缀”和第1步中选择的“后缀”组成。

    POST请求示例

          <--- url given by data --> <-- prefix  -->
     POST /v1/AUTH_c123456/container/TUPyhlXvoApgJim HTTP/1.1
    

    一个文件的示例数据

     -----------------------------336081439295927874898201087
     Content-Disposition: form-data; name="redirect"
    
-----------------------------336081439295927874898201087
Content-Disposition: form-data; name="max_file_size"

15000000
-----------------------------336081439295927874898201087
Content-Disposition: form-data; name="max_file_count"

1
-----------------------------336081439295927874898201087
Content-Disposition: form-data; name="expires"

1592300722
-----------------------------336081439295927874898201087
Content-Disposition: form-data; name="signature"

abcdefghijklmnopqrstuvwxyz01234567890123
-----------------------------336081439295927874898201087
Content-Disposition: form-data; name="file"; filename="v0Rl2qr"
Content-Type: application/octet-stream

”.
<!-- file come here -->
-----------------------------336081439295927874898201087--
```
  1. 将文件名存储在隐藏字段中。

实现示例

js请求示例

var
    fileInput = ev.target, // the file input element
    uniqid = fileInput.name,
    fileObject = fileInput.files[0],
    asyncFileInput = document.querySelector('input[type="hidden"][data-input-name="'+uniqid+'"]'),
    formData = new FormData(),
    fileName = asyncupload.makeid(),
    jsonData,
    objectName,
    existingAsyncFileInputValueJson,
    url = asyncFileInput.dataset.tempUrl
    ;

// Get the asyncupload parameters
window.fetch(url)
    .then(function(r) {
      // handle asyncupload parameters
      if (r.ok) {
        return r.json();
      } else {
        throw new Error('not ok');
      }
    }).then(function(data) {
      // upload to openstack swift
      if (fileObject.size > data.max_file_size){console.log("Upload file too large");}

      formData.append("redirect", data.redirect);
      formData.append("max_file_size", data.max_file_size);
      formData.append("max_file_count", data.max_file_count);
      formData.append("expires", data.expires);
      formData.append("signature", data.signature);
      formData.append("file", fileObject, fileName);

      // prepare the form data which will be used in next step
      objectName = data.prefix + fileName;

      jsonData = { "object_name": objectName };

      return window.fetch(data.url, {
        method: 'POST',
        mode: 'cors',
        body: formData
      });

    }).then(function(r) {
      if (r.ok) {
        console.log('Succesfully uploaded');

        // Update info in the form, as upload is successful
        if (asyncFileInput.value === "") {
          existingAsyncFileInputValueJson = { "files": [ jsonData ] };
        } else {
          existingAsyncFileInputValueJson = JSON.parse(asyncFileInput.value);
          existingAsyncFileInputValueJson.files.push(jsonData);
        }
        asyncFileInput.value = JSON.stringify(existingAsyncFileInputValueJson);

      } else {
        console.log('bad');
        console.log(r.status);
        // Handle errors

      }
    }).catch(function(err) {
      /* error :( */
      console.log("catch an error: " + err.name + " - " + err.message);
      alert("There was an error posting your images. Please try again.");
      throw new Error('openstack error: ' + err );
    });

检查文件的存在

验证器AsyncFileExists将在表单提交时检查文件的存在

/**
 * Represent a document stored in an object store 
 *
 * 
 * @ORM\Entity()
 * @AsyncFileExists(
 *  message="The file is not stored properly"
 * )
 */
class StoredObject implements AsyncFileInterface
{
}

获取签名的URL

您还可以使用GET请求使用javascript下载文件

GET /asyncupload/temp_url/generate/GET?object_name=abcdefhiI

示例响应

{

    "method": "GET",
    "url": "https://storage.gra.cloud.ovh.net/v1/AUTH_c611d5d3f457449cb709793003282426/comedienbe/FVsbQVDS0dAIvb4eqWqbDI?temp_url_sig=f10ddb5516f1b1b197a5ce63f98e2056696577c7&temp_url_expires=1592303020"

}

不允许使用 DELETE 和 PUT 方法。

在模板中显示文件(twig 过滤器)

可以使用这些函数获取文件的 URL

{# asyncFile implements AsyncFileInterface or a string (filename) #}
<img src="{{ asyncFile|file_url }}" />

您还可以向服务器发送 GET 请求来获取签名

<!-- the generate_url will be the GET url described in previous section -->
<button data-get-url="{{ asyncFile|generate_url }}">

如果容器是公开的,您可以直接使用对文件的访问

<img src="https://storage.gra.cloud.ovh.net/v1/AUTH_c123456/container/{{ asyncFile }}" />

安全性

在打印文件 URL 签名之前,您应该确保用户确实有权查看它。

不允许从 HTTP 请求中为 DELETEPUT 方法生成签名。您仍然可以从 PHP 代码中生成它。

限制 openstack 容器的使用

有时,用户选择一个文件进行上传,该文件立即上传到 openstack 容器。然后,用户在 UI 中删除该文件,并选择第二个文件(也将上传)并提交表单。

第一个上传的文件仍然记录在容器中。

过了一段时间后,文件可能会使容器膨胀。

为了防止这种情况,您可以注册 POST 签名并将它们存储在队列中。然后,在 submit_delay 过期后,检查前缀下每个文件的存在性,如果文件没有存储在数据库中,则删除它。

当生成签名时,将触发事件 async_uploader.generate_url

您可以监听此事件以获取签名的生成并实现自己的逻辑。您应该等待提交延迟。

对上传的文件应用逻辑(图像缩放等)

当生成签名时,将触发事件 async_uploader.generate_url

您可以监听此事件以获取签名的生成并实现自己的逻辑。您应该等待提交延迟以确保文件已上传。