Preskočiť na obsah

Príklad: nahrávanie súborov ​

POST /api/v1/avatar prijme jeden obrázok a meno používateľa. POST /api/v1/attachments prijme viac súborov v jednom poli.

Otvoriť diagram v plnej veľkosti
NahrávanieOdpoveď
PNG do 100 kB201 s uloženým menom
bez súboru400 Field is required
nad 100 kB413
textový súbor poslaný ako image/png415 text/plain is not allowed
tri súbory vo files[]200 s tromi položkami

Handler pre jeden súbor ​

$params['avatar'] je položka z $_FILES: name, type, tmp_name, error a size.

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\FileInputParam;
use Tomaj\NetteApi\Params\PostInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class AvatarUploadHandler extends BaseHandler
{
    private const MAX_BYTES = 100_000;

    private const TYPES = ['image/png' => 'png', 'image/jpeg' => 'jpg'];

    public function __construct(private string $storageDir)
    {
        parent::__construct();
    }

    public function summary(): string
    {
        return 'Upload an avatar';
    }

    public function params(): array
    {
        return [
            // $_FILES['avatar']: name, type, tmp_name, error, size
            (new FileInputParam('avatar'))->setRequired()->setDescription('PNG or JPEG, at most 100 kB'),
            (new PostInputParam('user'))->setRequired(),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        $file = $params['avatar'];

        // nette-api checks only that the file field is there; everything else is up to the handler
        if ($file['error'] !== UPLOAD_ERR_OK || !is_uploaded_file($file['tmp_name'])) {
            return new JsonApiResponse(400, ['status' => 'error', 'message' => 'Upload failed']);
        }
        if ($file['size'] > self::MAX_BYTES) {
            return new JsonApiResponse(413, ['status' => 'error', 'message' => 'The file is larger than 100 kB']);
        }

        // never trust $file['type'], it comes from the client
        $type = (string) (new \finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
        if (!isset(self::TYPES[$type])) {
            return new JsonApiResponse(415, ['status' => 'error', 'message' => "Type $type is not allowed"]);
        }

        // never use $file['name'] as a path either
        $target = $this->storageDir . '/' . bin2hex(random_bytes(8)) . '.' . self::TYPES[$type];
        move_uploaded_file($file['tmp_name'], $target);

        return new JsonApiResponse(201, ['user' => $params['user'], 'type' => $type, 'size' => $file['size']]);
    }
}

Viac súborov ​

setMulti() urobí z parametra zoznam. Handler dostane jednu položku na súbor.

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\FileInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class AttachmentsUploadHandler extends BaseHandler
{
    public function summary(): string
    {
        return 'Upload several files at once';
    }

    public function params(): array
    {
        return [
            // files[]=@a.txt files[]=@b.txt; setMulti() turns $_FILES into one array per file
            (new FileInputParam('files'))->setMulti()->setRequired(),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        $files = [];
        foreach ($params['files'] as $file) {
            $files[] = ['name' => basename($file['name']), 'size' => $file['size'], 'ok' => $file['error'] === UPLOAD_ERR_OK];
        }

        return new JsonApiResponse(200, ['files' => $files]);
    }
}

Služby ​

yaml
application:
	mapping:
		Api: Tomaj\NetteApi\Presenters\*Presenter

services:
	router: App\RouterFactory::create
	apiPresenter:
		factory: Tomaj\NetteApi\Presenters\ApiPresenter
	- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
	- Tomaj\NetteApi\Error\DefaultErrorHandler

	apiDecider:
		factory: Tomaj\NetteApi\ApiDecider
		setup:
			# POST /api/v1/avatar, multipart/form-data with avatar and user
			- addApi(Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'avatar'), App\Api\AvatarUploadHandler(%tempDir%), Tomaj\NetteApi\Authorization\NoAuthorization())
			# POST /api/v1/attachments, multipart/form-data with files[]
			- addApi(Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'attachments'), App\Api\AttachmentsUploadHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())

Test ​

bash
#!/usr/bin/env bash
# End-to-end check: one file and many files sent as multipart/form-data.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
serve 8092
files="$(mktemp -d)"
# a 1x1 PNG, a text file and a 200 kB file
printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\1\0\0\0\1\x08\x06\0\0\0\x1f\x15\xc4\x89\0\0\0\rIDATx\x9cc\xf8\x0f\0\0\x01\x01\0\x05\x18\xd8N\0\0\0\0IEND\xaeB`\x82' > "$files/a.png"
echo 'hello' > "$files/a.txt"
head -c 200000 /dev/zero > "$files/big.png"

request -F "avatar=@$files/a.png" -F user=ada "$BASE/api/v1/avatar"
expect_status 201 "png"
expect_body '"user":"ada","type":"image/png"' "png"
ok "a PNG is stored, the type is detected from the content"

request -F "avatar=@$files/a.txt;type=image/png" -F user=ada "$BASE/api/v1/avatar"
expect_status 415 "fake type"
expect_body 'text/plain is not allowed' "fake type"
ok "a text file sent as image/png answers 415"

request -F "avatar=@$files/big.png" -F user=ada "$BASE/api/v1/avatar"
expect_status 413 "too big"
ok "a file over 100 kB answers 413"

request -F user=ada "$BASE/api/v1/avatar"
expect_status 400 "no file"
expect_body '"avatar":["Field is required"]' "no file"
ok "a missing file answers 400 from nette-api"

request -F "files[]=@$files/a.txt" -F "files[]=@$files/a.png" "$BASE/api/v1/attachments"
expect_status 200 "many"
expect_body '{"files":[{"name":"a.txt","size":6,"ok":true},{"name":"a.png"' "many"
ok "setMulti() gives one array per uploaded file"
rm -rf "$files"

Časté otázky ​

Prečo neveriť poľu type? ​

Posiela ho klient. finfo sa namiesto toho pozrie na bajty súboru.

Prečo náhodné meno súboru? ​

Meno od klienta môže obsahovať ../ alebo prepísať iný súbor. Ak pôvodné meno potrebujete, uložte ho do databázy.

Kde je limit veľkosti? ​

V handleri a v PHP: upload_max_filesize a post_max_size musia byť aspoň také veľké, inak PHP súbor zahodí skôr, než ho handler uvidí.

Funguje to s PUT? ​

Nie, PHP plní $_FILES iba pre multipart požiadavky POST. Pre PUT pošlite súbor ako surové telo, pozrite si JSON a surové telo.

Ako posiela súbory konzola? ​

Pre každý FileInputParam ukáže pole na výber súboru. Pozrite si Webová konzola.

Vydané pod licenciou MIT.