Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Api platform handling fille uploads

I'm trying to upload files with Api Platform and Vich Uploader Bundle. When I send POST request with multipart/form-data and Id of the entity to attach image file to, I get 200 response with my entity. But uploaded file doesn't uploads to destination directory and it's generated filename doesn't persists. No errors, no any clues, no idea.

Here is my code:

//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;
    }
}

I'm sending request:

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 "[email protected];type=image/png"

... and getting response:

{
  "@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": "[email protected]",
  "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"
}

As you can see it's all ok. Proper organization was found. And even logoFile field was filled with uploaded picture. But uploaded file wasn't moved to destination. And logoPath contains old logo filename.

As I said no errors. Please help me to figure out where to dig.

like image 630
Andrey Kryukov Avatar asked Jul 15 '20 15:07

Andrey Kryukov


People also ask

Can we upload file using REST API?

REST API File Upload with Java In the above code block, we have created an endpoint of type HTTP Post that is waiting for file. This endpoint prints the name of the external file to the console of the application. That's all we'll do for File Upload on the code side.

Can we upload file in Postman?

To upload a file using postman, you begin by calling the API using a POST HTTP request. This launches a Postman template. After calling the API, create a file processing method under the collections tab. On this tab, you have options to upload, download, or visualize your file.


1 Answers

I am currently working on a project which allow users to upload media files.

I have discarded the Vich bundle. Api-platform is application/ld+json oriented.

Instead, i let the user provide a base64-encoded content file (i.e a string representation with readable characters only).

The only counterpart i got is that the file size is increased by ~30% during http transfer. Honestly, it does not matter.

I suggest you to do something like the code below.

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

The logo (note the assertion on the $encodingFormat property):

<?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

Your stripped Organization class, which declare the 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...

}

Note the serialization groups of both $logo and $b64LogoContent properties.

Then the controller (action class), in order to decode, assign and write the logo content.

<?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";
    }
}

It checks whether the supplied logo is a "tiny image" with @Assert annotations of the ImageObject class (encodingFormat, width, height etc.), they are triggered by the @Assert\Valid annotation of the Organization::$logo property.

With that, you can create an organization with its logo by sending a single HTTP POST /organizations request.

like image 183
rugolinifr Avatar answered Sep 20 '22 12:09

rugolinifr