138 - Upload the thumbnail once per publish, not per retry
This commit is contained in:
parent
34b78e6e54
commit
e16368a662
3 changed files with 106 additions and 17 deletions
|
|
@ -37,46 +37,63 @@ public function publishToChannel(Article $article, array $extractedData, Platfor
|
||||||
$authService = resolve(LemmyAuthService::class);
|
$authService = resolve(LemmyAuthService::class);
|
||||||
$token = $authService->getToken($this->account);
|
$token = $authService->getToken($this->account);
|
||||||
|
|
||||||
|
$thumbnail = $this->hostedThumbnail($extractedData, $channel, $article, $token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->createPost($token, $extractedData, $channel, $article);
|
return $this->createPost($token, $extractedData, $channel, $article, $thumbnail);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// If the cached token was stale, refresh and retry once
|
// If the cached token was stale, refresh and retry once
|
||||||
if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
|
if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
|
||||||
$token = $authService->refreshToken($this->account);
|
$token = $authService->refreshToken($this->account);
|
||||||
|
|
||||||
return $this->createPost($token, $extractedData, $channel, $article);
|
return $this->createPost($token, $extractedData, $channel, $article, $thumbnail);
|
||||||
}
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploaded once per publish, outside the stale-token retry: the upload is the expensive
|
||||||
|
* part and a retry would otherwise re-download, re-encode and re-log.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $extractedData
|
||||||
|
*/
|
||||||
|
private function hostedThumbnail(array $extractedData, PlatformChannel $channel, Article $article, string $token): ?string
|
||||||
|
{
|
||||||
|
$source = $extractedData['thumbnail'] ?? null;
|
||||||
|
$source = is_string($source) && $source !== '' ? $source : null;
|
||||||
|
|
||||||
|
if ($source === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hosted = $this->thumbnailUploader->upload($source, $token);
|
||||||
|
|
||||||
|
if ($hosted === null) {
|
||||||
|
app(LogSaver::class)->warning('Thumbnail upload failed; publishing without one', $channel, [
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'source' => $source,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hosted;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $extractedData
|
* @param array<string, mixed> $extractedData
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article): array
|
private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article, ?string $thumbnail = null): array
|
||||||
{
|
{
|
||||||
$languageId = $extractedData['language_id'] ?? null;
|
$languageId = $extractedData['language_id'] ?? null;
|
||||||
|
|
||||||
$thumbnail = $extractedData['thumbnail'] ?? null;
|
|
||||||
$thumbnail = is_string($thumbnail) && $thumbnail !== '' ? $thumbnail : null;
|
|
||||||
|
|
||||||
$hosted = $this->thumbnailUploader->upload($thumbnail, $token);
|
|
||||||
|
|
||||||
if ($thumbnail !== null && $hosted === null) {
|
|
||||||
resolve(LogSaver::class)->warning('Thumbnail upload failed; publishing without one', $channel, [
|
|
||||||
'article_id' => $article->id,
|
|
||||||
'source' => $thumbnail,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->api->createPost(
|
return $this->api->createPost(
|
||||||
$token,
|
$token,
|
||||||
$extractedData['title'] ?? 'Untitled',
|
$extractedData['title'] ?? 'Untitled',
|
||||||
$extractedData['description'] ?? '',
|
$extractedData['description'] ?? '',
|
||||||
$channel->channel_id,
|
$channel->channel_id,
|
||||||
$article->url,
|
$article->url,
|
||||||
$hosted,
|
$thumbnail,
|
||||||
$languageId
|
$languageId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -428,11 +428,75 @@ public function test_it_does_not_log_when_there_was_no_thumbnail_to_upload(): vo
|
||||||
$apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);
|
$apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);
|
||||||
|
|
||||||
$uploader = Mockery::mock(ThumbnailUploader::class);
|
$uploader = Mockery::mock(ThumbnailUploader::class);
|
||||||
$uploader->shouldReceive('upload')->once()->with(null, 'tok')->andReturn(null);
|
$uploader->shouldNotReceive('upload');
|
||||||
|
|
||||||
$publisher = new LemmyPublisher($account);
|
$publisher = new LemmyPublisher($account);
|
||||||
$this->injectMocks($publisher, $apiMock, $uploader);
|
$this->injectMocks($publisher, $apiMock, $uploader);
|
||||||
|
|
||||||
$this->assertSame(['ok' => true], $publisher->publishToChannel($article, ['title' => 'T'], $channel));
|
$this->assertSame(['ok' => true], $publisher->publishToChannel($article, ['title' => 'T'], $channel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_a_stale_token_retry_does_not_re_upload_the_thumbnail(): void
|
||||||
|
{
|
||||||
|
$account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$authMock = Mockery::mock(LemmyAuthService::class);
|
||||||
|
$authMock->shouldReceive('getToken')->once()->andReturn('stale');
|
||||||
|
$authMock->shouldReceive('refreshToken')->once()->andReturn('fresh');
|
||||||
|
$this->app->instance(LemmyAuthService::class, $authMock);
|
||||||
|
|
||||||
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
|
$apiMock->shouldReceive('createPost')
|
||||||
|
->once()
|
||||||
|
->with('stale', Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), 'https://lemmy.world/pictrs/image/x.jpg', Mockery::any())
|
||||||
|
->andThrow(new Exception('not_logged_in'));
|
||||||
|
$apiMock->shouldReceive('createPost')
|
||||||
|
->once()
|
||||||
|
->with('fresh', Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), 'https://lemmy.world/pictrs/image/x.jpg', Mockery::any())
|
||||||
|
->andReturn(['ok' => true]);
|
||||||
|
|
||||||
|
$uploader = Mockery::mock(ThumbnailUploader::class);
|
||||||
|
$uploader->shouldReceive('upload')->once()->andReturn('https://lemmy.world/pictrs/image/x.jpg');
|
||||||
|
|
||||||
|
$publisher = new LemmyPublisher($account);
|
||||||
|
$this->injectMocks($publisher, $apiMock, $uploader);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['ok' => true],
|
||||||
|
$publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/b.jpg'], $channel)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_a_stale_token_retry_logs_the_upload_failure_only_once(): void
|
||||||
|
{
|
||||||
|
$account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$authMock = Mockery::mock(LemmyAuthService::class);
|
||||||
|
$authMock->shouldReceive('getToken')->andReturn('stale');
|
||||||
|
$authMock->shouldReceive('refreshToken')->andReturn('fresh');
|
||||||
|
$this->app->instance(LemmyAuthService::class, $authMock);
|
||||||
|
|
||||||
|
$logMock = Mockery::mock(LogSaver::class);
|
||||||
|
$logMock->shouldReceive('warning')->once();
|
||||||
|
$this->app->instance(LogSaver::class, $logMock);
|
||||||
|
|
||||||
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
|
$apiMock->shouldReceive('createPost')->once()->andThrow(new Exception('not_logged_in'));
|
||||||
|
$apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);
|
||||||
|
|
||||||
|
$uploader = Mockery::mock(ThumbnailUploader::class);
|
||||||
|
$uploader->shouldReceive('upload')->once()->andReturn(null);
|
||||||
|
|
||||||
|
$publisher = new LemmyPublisher($account);
|
||||||
|
$this->injectMocks($publisher, $apiMock, $uploader);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['ok' => true],
|
||||||
|
$publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/b.jpg'], $channel)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,14 @@ public function test_it_returns_null_when_the_source_exceeds_the_size_cap(): voi
|
||||||
$this->assertNull($this->uploader()->upload('https://cdn.example.com/huge.jpg', 'token'));
|
$this->assertNull($this->uploader()->upload('https://cdn.example.com/huge.jpg', 'token'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_null_when_the_source_exceeds_the_pixel_cap(): void
|
||||||
|
{
|
||||||
|
// A small file can still decode to a huge bitmap; this is the guard that protects the worker.
|
||||||
|
$this->fake($this->jpeg(9000, 6000), ['files' => [['file' => 'abc123.jpg']]]);
|
||||||
|
|
||||||
|
$this->assertNull($this->uploader()->upload('https://cdn.example.com/huge-dimensions.jpg', 'token'));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_returns_null_when_the_download_throws(): void
|
public function test_it_returns_null_when_the_download_throws(): void
|
||||||
{
|
{
|
||||||
Http::fake(fn () => throw new \RuntimeException('connection refused'));
|
Http::fake(fn () => throw new \RuntimeException('connection refused'));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue