138 - Add multipart upload support to LemmyRequest

This commit is contained in:
myrmidex 2026-08-13 20:46:06 +02:00
parent 0163fa53b4
commit 6ea3f922da
2 changed files with 79 additions and 0 deletions

View file

@ -7,6 +7,9 @@
class LemmyRequest
{
// Uploads carry an image payload; the 30s used for JSON calls is not enough.
private const UPLOAD_TIMEOUT_SECONDS = 60;
private string $instance;
private ?string $token;
@ -83,6 +86,27 @@ public function post(string $endpoint, array $data = []): Response
return $request->post($url, $data);
}
/**
* pict-rs is mounted outside the /api/v3 prefix, so this takes a root-relative path.
*
* @param string $path Root-relative, e.g. 'pictrs/image'
* @param string $name Multipart field name, e.g. 'images[]'
* @param string $contents Raw file bytes
* @param string $filename Filename sent with the part
*/
public function postMultipart(string $path, string $name, string $contents, string $filename): Response
{
$url = sprintf('%s://%s/%s', $this->scheme, $this->instance, ltrim($path, '/'));
$request = Http::timeout(self::UPLOAD_TIMEOUT_SECONDS);
if ($this->token) {
$request = $request->withToken($this->token);
}
return $request->attach($name, $contents, $filename)->post($url);
}
public function withToken(string $token): self
{
$this->token = $token;

View file

@ -263,6 +263,61 @@ public function test_chaining_methods(): void
});
}
public function test_post_multipart_targets_a_root_relative_path(): void
{
Http::fake(['*' => Http::response(['files' => []])]);
$request = new LemmyRequest('lemmy.world', 'test-token');
$response = $request->postMultipart('pictrs/image', 'images[]', 'binary-data', 'thumb.jpg');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/pictrs/image'
&& $httpRequest->header('Authorization')[0] === 'Bearer test-token';
});
}
public function test_post_multipart_does_not_use_the_api_prefix(): void
{
Http::fake(['*' => Http::response(['files' => []])]);
(new LemmyRequest('lemmy.world'))->postMultipart('/pictrs/image', 'images[]', 'data', 'thumb.jpg');
Http::assertSent(fn ($httpRequest) => ! str_contains($httpRequest->url(), '/api/v3/'));
}
public function test_post_multipart_sends_the_file_as_multipart_data(): void
{
Http::fake(['*' => Http::response(['files' => []])]);
(new LemmyRequest('lemmy.world'))->postMultipart('pictrs/image', 'images[]', 'binary-data', 'thumb.jpg');
Http::assertSent(function ($httpRequest) {
return str_contains($httpRequest->header('Content-Type')[0], 'multipart/form-data')
&& str_contains($httpRequest->body(), 'binary-data')
&& str_contains($httpRequest->body(), 'thumb.jpg');
});
}
public function test_post_multipart_omits_authorization_without_a_token(): void
{
Http::fake(['*' => Http::response(['files' => []])]);
(new LemmyRequest('lemmy.world'))->postMultipart('pictrs/image', 'images[]', 'data', 'thumb.jpg');
Http::assertSent(fn ($httpRequest) => ! $httpRequest->hasHeader('Authorization'));
}
public function test_post_multipart_respects_the_scheme(): void
{
Http::fake(['*' => Http::response(['files' => []])]);
(new LemmyRequest('http://lemmy.world'))->postMultipart('pictrs/image', 'images[]', 'data', 'thumb.jpg');
Http::assertSent(fn ($httpRequest) => $httpRequest->url() === 'http://lemmy.world/pictrs/image');
}
private function getPrivateProperty(object $object, string $property): mixed
{
$reflection = new \ReflectionClass($object);