Api 平台处理文件上传

Posted

技术标签:

【中文标题】Api 平台处理文件上传【英文标题】:Api platform handling fille uploads 【发布时间】:2020-11-05 04:22:35 【问题描述】:

我正在尝试使用 Api Platform 和 Vich Uploader Bundle 上传文件。 当我发送带有 multipart/form-data 和要附加图像文件的实体 ID 的 POST 请求时,我的实体收到 200 响应。但是上传的文件不会上传到目标目录,并且它生成的文件名不会保留。没有错误,没有任何线索,不知道。

这是我的代码:

//vich uploader mappings
vich_uploader:
    db_driver: orm
    mappings:
        logo:
            uri_prefix: /logo
            upload_destination: '%kernel.project_dir%/public/images/logo/'
            namer: App\Infrastructure\Naming\LogoNamer
//Organization Entity
<?php

namespace App\Infrastructure\Dto;

...use

/**
 * @ORM\Entity()
 * @ApiResource(
 *     iri="https://schema.org/Organization",
 *     shortName="Place",
 *     collectionOperations=
 *          "post" = 
 *              "denormalization_context" = 
 *                  "groups"=
 *                      "organization:collection:post"
 *                  
 *              
 *          ,
 *          "get" = 
 *              "normalization_context" = 
 *                  "groups"=
 *                      "organization:collection:get"
 *                  
 *              
 *          
 *     ,
 *     itemOperations=
 *          "get",
 *          "CreateOrganizationLogoAction::OPERATION_NAME" = 
 *              "groups"="logo:post",
 *              "method"="POST",
 *              "path"=CreateOrganizationLogoAction::OPERATION_PATH,
 *              "controller"=CreateOrganizationLogoAction::class,
 *              "deserialize"=false,
 *              "validation_groups"="Default", "logo_create",
 *              "openapi_context"=
 *                  "summary"="Uploads logo file to given Organization resource",
 *                  "requestBody"=
 *                      "content"=
 *                          "multipart/form-data"=
 *                              "schema"=
 *                                  "type"="object",
 *                                  "properties"=
 *                                      "logoFile"=
 *                                          "type"="string",
 *                                          "format"="binary"
 *                                      
 *                                  
 *                              
 *                          
 *                      
 *                  
 *              
 *          
 *     
 * )
 * @Vich\Uploadable
 */
final class Organization

    /**
     * @Groups("organization:collection:get")
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class=UuidGenerator::class)
     * @ApiProperty(identifier=true)
     */
    protected Uuid $id;

    /**
     * @Groups("organization:collection:get", "organization:collection:post")
     * @ORM\Column(type="string", length=100, unique=true)
     */
    public string $slug;

    /**
     * @ORM\Column(type="smallint")
     */
    public int $status;

    /**
     * @ApiProperty(iri="https://schema.org/name")
     * @Groups("organization:collection:get")
     * @ORM\Column(type="string", length=100, nullable=true)
     */
    public ?string $title = null;

    /**
     * @ApiProperty(iri="http://schema.org/logo")
     * @Groups("organization:collection:get", "logo:post")
     * @ORM\Column(nullable=true)
     */
    public ?string $logoPath = null;

    /**
     * @ApiProperty(iri="https://schema.org/description")
     * @ORM\Column(type="text", nullable=true)
     */
    public ?string $description = null;

    /**
     * @ApiProperty(iri="https://schema.org/disambiguatingDescription")
     * @Groups("organization:collection:get")
     * @ORM\Column(type="string", length=150, nullable=true)
     */
    public ?string $disambiguating_description = null;

    /**
     * @ApiProperty(iri="https://schema.org/addressCountry")
     * @ORM\Column(type="string", length=2, nullable=true)
     */
    public ?string $country = null;

    /**
     * @ApiProperty(iri="https://schema.org/addressRegion")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $region = null;

    /**
     * @ApiProperty(iri="https://schema.org/streetAddress")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $street = null;

    /**
     * @ApiProperty(iri="https://schema.org/telephone")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $telephone = null;

    /**
     * @ApiProperty(iri="https://schema.org/email")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $email = null;

    /**
     * @ApiProperty(iri="https://schema.org/contentUrl")
     * @Groups("logo_read")
     */
    public ?string $logoContentUrl = null;

    /**
     * @var File|null
     *
     * @Assert\NotNull(groups="logo_create")
     * @Vich\UploadableField(mapping="logo", fileNameProperty="logoPath")

     */
    public ?File $logoFile = null;

    public function __construct()
    
        $this->status = OrganizationStatus::DRAFT()->getValue();
    

    public function getId(): ?Uuid
    
        return $this->id ?? null;
    

    public function setId(Uuid $id)
    
        $this->id = $id;
    

final class CreateOrganizationLogoAction extends AbstractController

    const OPERATION_NAME = 'post_logo';

    const OPERATION_PATH = '/places/id/logo';

    private OrganizationPgRepository $repository;

    public function __construct(OrganizationPgRepository $repository)
    
        $this->repository = $repository;
    

    /**
     * @param Request $request
     *
     * @return EntityOrganization
     */
    public function __invoke(Request $request): EntityOrganization
    
        $uploadedFile = $request->files->get('logoFile');
        if (!$uploadedFile) 
            throw new BadRequestHttpException('"file" is required');
        

        $organization = $this->repository->find(Uuid::fromString($request->attributes->get('id')));
        $organization->logoFile = $uploadedFile;

        return $organization;
    

我正在发送请求:

curl -X POST "http://localhost:8081/api/places/0dc43a86-6402-4a45-8392-19d5e398a7ab/logo" -H "accept: application/ld+json" -H "Content-Type: multipart/form-data" -F "logoFile=@test.png;type=image/png"

...并得到响应:


  "@context": "/api/contexts/Place",
  "@id": "/api/places/0dc43a86-6402-4a45-8392-19d5e398a7ab",
  "@type": "https://schema.org/Organization",
  "slug": "consequatur-aut-optio-corrupti-quod-sit-libero-aspernatur",
  "status": 0,
  "title": "Block LLC",
  "logoPath": "a268cde1-d93e-4d48-9f0d-177b4f89f1f8.png",
  "description": "Nisi sint ducimus consequatur dicta sint maxime. Et soluta facere in quisquam quia. Tempore quae non qui dignissimos optio rem cum illum. Eum similique vitae autem aut. Reiciendis nesciunt rerum libero in consequuntur excepturi repellendus unde. Tempore ea perferendis sunt quibusdam autem est. Similique qui illum necessitatibus velit dolores. Voluptas sapiente excepturi ad assumenda exercitationem est. Nesciunt sint sint fugiat quis blanditiis. Rerum vel sint temporibus nobis fugiat nostrum aut. Voluptatibus temporibus magnam cumque asperiores. Adipisci qui perferendis mollitia tempore accusantium aut. Possimus numquam asperiores repellendus non facilis.",
  "disambiguating_description": "Et libero temporibus ut impedit esse ipsum quam.",
  "country": "RU",
  "region": "Idaho",
  "street": "15544 Delbert Underpass",
  "telephone": "+78891211558",
  "email": "lhintz@corwin.com",
  "pictures": [],
  "social_profiles": [],
  "logoContentUrl": "/logo/a268cde1-d93e-4d48-9f0d-177b4f89f1f8.png",
  "logoFile": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjog
...
...
... TgjNWnJ7YWPrMCWGxWbi57Tj58TfPQL1Hi54DRFD/FkuLcuXBKFB3TFLcuaUvpqKuYUJaLL/yV/R/+kf/Z",
  "id": "0dc43a86-6402-4a45-8392-19d5e398a7ab"

如您所见,一切正常。找到了适当的组织。甚至 logoFile 字段都充满了上传的图片。但上传的文件没有移动到目的地。并且 logoPath 包含旧的 logo 文件名。

正如我所说,没有错误。 请帮我弄清楚在哪里挖掘。

【问题讨论】:

您说:“上传的文件没有移动到目的地”。但老实说,我真的没有看到任何代码应该发生这种移动。我怀疑它会在__invoke 函数中。在那里你得到图像,但你不移动它。您将其分配给organisation,但您不保存更新后的组织。这样您的返回数据看起来是正确的,但任何新请求都将使用旧设置 @MaartenVeerman 但在文档中没有提到手动移动或持久化。不在 Api Platform 文档中,也不在 Vich Uploader 文档中。我认为这项工作是由捆绑自己完成的。当我尝试重复这些文档中的示例时,一切正常。但我不会手动移动或坚持任何东西。 @avkryukov 我的解决方案是否解决了您的问题?我在我从事的项目中已经有很多次了,所以我很确定就是这样。但如果不是这样会更有趣。 @PhilipWeinke 实际上我已经拒绝将 VichUploader Bundle 与 ApiPlatform 结合使用。但!您的解决方案看起来很合理!我绝对需要分叉我的项目来测试它。我注意到我的代码没有保留更新的组织。但是有了新记录,它就可以正常工作了。所以你 100% 是正确的。 【参考方案1】:

VichUploaderBundle 使用 prePersist 和 preUpdate 钩子在学说事件侦听器中进行上传处理。您的问题是,从教义的角度来看,没有持久性属性发生变化。由于没有变化,所以不会调用上传监听器。

一个简单的解决方法是在上传文件时始终更改持久属性。我将updatedAt 添加到您的实体和方法updateLogo 以保持logoFileupdatedAt 所需的更改在一起。

final class Organization

    (...)

    /**
     * @ApiProperty(iri="http://schema.org/logo")
     * @Groups("organization:collection:get", "logo:post")
     * @ORM\Column(nullable=true)
     */
    public ?string $logoPath = null;

    /**
     * @ORM\Column(type="datetime")
     */
    private ?DateTime $updatedAt = null;

    /**
     * @var File|null
     *
     * @Assert\NotNull(groups="logo_create")
     * @Vich\UploadableField(mapping="logo", fileNameProperty="logoPath")
     */
    private ?File $logoFile = null;
    
    (...)

    public function updateLogo(File $logo): void
    
       $this->logoFile  = $logo;
       $this->updatedAt = new DateTime();
    

final class CreateOrganizationLogoAction extends AbstractController

    (...)

    /**
     * @param Request $request
     *
     * @return EntityOrganization
     */
    public function __invoke(Request $request): EntityOrganization
    
        $uploadedFile = $request->files->get('logoFile');
        if (!$uploadedFile) 
            throw new BadRequestHttpException('"file" is required');
        

        $organization = $this->repository->find(Uuid::fromString($request->attributes->get('id')));
        $organization->updateLogo($uploadedFile);

        return $organization;
    

【讨论】:

谢谢!这真是个好主意!需要测试一下。 顺便说一句,我发现在 _invoke 方法中不需要使用存储库查找实体。您可以像这样自动装配它: public function __invoke(Request $request, EntityOrganization $data): EntityOrganization $data 将包含适当的实体。但不知何故,它必然将这个变量命名为 $data。否则就不行了。 你能分享一下关于图片上传验证的想法吗??【参考方案2】:

我目前正在开发一个允许用户上传媒体文件的项目。

我已经丢弃了 Vich 捆绑包。 Api-platform是面向application/ld+json的。

相反,我让用户提供 base64 编码的内容文件(即仅包含可读字符的字符串表示)。

我得到的唯一对应物是在 http 传输期间文件大小增加了约 30%。老实说,没关系。

我建议你像下面的代码那样做。

OrganizationController --use--> Organization 1 ---> 0..1 ImageObject

徽标(注意 $encodingFormat 属性上的断言):

<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * An image file.
 *
 * @see http://schema.org/ImageObject Documentation on Schema.org
 *
 * @ORM\Entity
 * @ApiResource(
 *     iri="http://schema.org/ImageObject",
 *     normalizationContext="groups" = "imageobject:get"
 *     collectionOperations="get",
 *     itemOperations="get"
 * )
 */
class ImageObject

    /**
     * @var int|null
     *
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     * @Groups("imageobject:get")
     */
    private $id;

    /**
     * @var string|null the name of the item
     *
     * @ORM\Column(type="text", nullable=true)
     * @ApiProperty(iri="http://schema.org/name")
     * @Groups("imageobject:get")
     */
    private $name;

    /**
     * @var string|null actual bytes of the media object, for example the image file or video file
     *
     * @ORM\Column(type="text", nullable=true)
     * @ApiProperty(iri="http://schema.org/contentUrl")
     * @Groups("imageobject:get")
     */
    private $contentUrl;

    /**
     * @var string|null mp3, mpeg4, etc
     *
     * @Assert\Regex("#^image/.*$#", message="This is not an image, this is a  value  file.")
     * @ORM\Column(type="text", nullable=true)
     * @ApiProperty(iri="http://schema.org/encodingFormat")
     * @Groups("imageobject:get")
     */
    private $encodingFormat;
    
    // getters and setters, nothing specific here

您剥离的 Organization 类,它声明了 OrganizationController

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\OrganizationRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use App\Controller\OrganizationController;

/**
 * @ApiResource(
 *     normalizationContext=
            "groups" = "organization:get"
 *     ,
 *     denormalizationContext=
            "groups" = "organization:post"
 *     ,
 *     collectionOperations=
            "get",
 *          "post" = 
 *              "controller" = OrganizationController::class
 *          
 *     
 * )
 * @ORM\Entity(repositoryClass=OrganizationRepository::class)
 */
class Organization

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups("organization:get")
     */
    private $id;

    /**
     * @var string
     * @ORM\Column(type="string", length=100, unique=true)
     * @Groups("organization:get", "organization:post")
     */
    private $slug;

    /**
     * @var null|ImageObject
     * @Assert\Valid()
     * @ORM\OneToOne(targetEntity=ImageObject::class, cascade="persist", "remove")
     * @Groups("organization:get")
     */
    private $logo;

    /**
     * @var string the logo BLOB, base64-encoded, without line separators.
     * @Groups("organization:post")
     */
    private $b64LogoContent;

    // getters and setters, nothing specific here...


注意 $logo$b64LogoContent 属性的序列化组。

然后是控制器(动作类),以便解码、分配和写入标志内容。

<?php


namespace App\Controller;

use App\Entity\ImageObject;
use App\Entity\Organization;
use finfo;

/**
 * Handle the base64-encoded logo content.
 */
class OrganizationController

    public function __invoke(Organization $data)
    
        $b64LogoContent = $data->getB64LogoContent();
        if (! empty($b64LogoContent)) 
            $logo = $this->buildAndWriteLogo($b64LogoContent);
            $data->setLogo($logo);
        
        return $data;
    

    private function buildAndWriteLogo(string $b64LogoContent): ImageObject
    
        $logo = new ImageObject();
        $content = str_replace("\n", "", base64_decode($b64LogoContent));
        $mimeType = (new finfo())->buffer($content, FILEINFO_MIME_TYPE);
        $autoGeneratedId = $this->createFileName($content, $mimeType); // Or anything to generate an ID, like md5sum
        $logo->setName($autoGeneratedId);
        $logo->setContentUrl("/public/images/logo/$autoGeneratedId");
        $logo->setEncodingFormat($mimeType);
        // check the directory permissions!
        // writing the file should be done after data validation
        file_put_contents("images/logo/$autoGeneratedId", $content);
        return $logo;
    

    private function createFileName(string $content, string $mimeType): string
    
        if (strpos($mimeType, "image/") === 0) 
            $extension = explode('/', $mimeType)[1];
         else 
            $extension = "txt";
        
        return time() . ".$extension";
    

它检查提供的徽标是否是带有 ImageObject 类的 @Assert 注释(encodingFormat、宽度、高度等)的“小图像”,它们由 @Assert 触发Organization::$logo 属性的 \Valid 注释。

这样,您可以通过发送单个 HTTP POST /organizations 请求来创建一个带有其徽标的组织。

【讨论】:

感谢您的回答。如果我找不到如何强制 Vich Uploader Bundle 工作,我会使用你的方法。

以上是关于Api 平台处理文件上传的主要内容,如果未能解决你的问题,请参考以下文章

通过 google-api-javascript-client 库通过批处理文件上传获取 404 错误

thinkphp中怎么处理上传文件

thinkphp中怎么处理上传文件

Web API之基于H5客户端分段上传大文件

使用 rest api 从 Web 应用程序将文件上传到 Azure 文件存储

Vichuploader & API 平台文件未保存