Skip to content

Commit b4f9138

Browse files
authored
Feature/update job (#5)
* Implement non-extraction calls * cs fix * cs fix * implement extraction logic * cs fix * fix test * fix * fix tests * cs fix * cs fix * compare status codes
1 parent 7194c29 commit b4f9138

File tree

12 files changed

+436
-31
lines changed

12 files changed

+436
-31
lines changed

app/Enum/WebserviceEndpoint.php

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,62 @@
22

33
namespace App\Enum;
44

5+
use App\RemoteSite\Responses\FinalizeUpdate;
6+
use App\RemoteSite\Responses\GetUpdate;
7+
use App\RemoteSite\Responses\HealthCheck;
8+
use App\RemoteSite\Responses\PrepareUpdate;
9+
510
enum WebserviceEndpoint: string
611
{
7-
case HEALTH_CHECK = "/api/index.php/v1/joomlaupdate/healthcheck";
8-
case FETCH_UPDATES = "/api/index.php/v1/joomlaupdate/fetchUpdate";
9-
case PREPARE_UPDATE = "/api/index.php/v1/joomlaupdate/prepareUpdate";
10-
case FINALIZE_UPDATE = "/api/index.php/v1/joomlaupdate/finalizeUpdate";
12+
case checkHealth = "/api/index.php/v1/joomlaupdate/healthcheck";
13+
case getUpdate = "/api/index.php/v1/joomlaupdate/getUpdate";
14+
case prepareUpdate = "/api/index.php/v1/joomlaupdate/prepareUpdate";
15+
case finalizeUpdate = "/api/index.php/v1/joomlaupdate/finalizeUpdate";
16+
17+
public function getMethod(): HttpMethod
18+
{
19+
switch ($this->name) {
20+
case self::checkHealth->name:
21+
case self::getUpdate->name:
22+
return HttpMethod::GET;
23+
24+
// no break
25+
case self::prepareUpdate->name:
26+
case self::finalizeUpdate->name:
27+
return HttpMethod::POST;
28+
}
29+
30+
throw new \ValueError("No method defined");
31+
}
32+
33+
public function getResponseClass(): string
34+
{
35+
switch ($this->name) {
36+
case self::checkHealth->name:
37+
return HealthCheck::class;
38+
case self::getUpdate->name:
39+
return GetUpdate::class;
40+
case self::prepareUpdate->name:
41+
return PrepareUpdate::class;
42+
case self::finalizeUpdate->name:
43+
return FinalizeUpdate::class;
44+
}
45+
}
46+
47+
public function getUrl(): string
48+
{
49+
return $this->value;
50+
}
51+
52+
public static function tryFromName(string $name): ?static
53+
{
54+
$reflection = new \ReflectionEnum(static::class);
55+
56+
if (!$reflection->hasCase($name)) {
57+
return null;
58+
}
59+
60+
/** @var static */
61+
return $reflection->getCase($name)->getValue();
62+
}
1163
}

app/Jobs/UpdateSite.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66

77
use App\Models\Site;
88
use App\RemoteSite\Connection;
9+
use App\RemoteSite\Responses\PrepareUpdate;
910
use Illuminate\Contracts\Queue\ShouldQueue;
1011
use Illuminate\Foundation\Queue\Queueable;
12+
use Illuminate\Support\Facades\App;
13+
use Illuminate\Support\Facades\Log;
1114

1215
class UpdateSite implements ShouldQueue
1316
{
1417
use Queueable;
18+
protected int $preUpdateCode;
1519

1620
/**
1721
* Create a new job instance.
@@ -30,5 +34,92 @@ public function handle(): void
3034

3135
// Test connection and get current version
3236
$healthResult = $connection->checkHealth();
37+
38+
// Check the version
39+
if (version_compare($healthResult->cms_version, $this->targetVersion, ">=")) {
40+
Log::info("Site is already up to date: " . $this->site->id);
41+
42+
return;
43+
}
44+
45+
// Store pre-update response code
46+
$this->preUpdateCode = $this->site->getFrontendStatus();
47+
48+
// Let site fetch available updates
49+
$updateResult = $connection->getUpdate();
50+
51+
// Check if update is found and return if not
52+
if (is_null($updateResult->availableUpdate)) {
53+
Log::info("No update available for site: " . $this->site->id);
54+
55+
return;
56+
}
57+
58+
// Check the version and return if it does not match
59+
if ($updateResult->availableUpdate !== $this->targetVersion) {
60+
Log::info("Update version mismatch for site: " . $this->site->id);
61+
62+
return;
63+
}
64+
65+
$prepareResult = $connection->prepareUpdate($this->targetVersion);
66+
67+
// Perform the actual extraction
68+
$this->performExtraction($prepareResult);
69+
70+
// Run the postupdate steps
71+
if (!$connection->finalizeUpdate()->success) {
72+
throw new \Exception("Update for site failed in postprocessing: " . $this->site->id);
73+
}
74+
75+
// Compare codes
76+
if ($this->site->getFrontendStatus() !== $this->preUpdateCode) {
77+
throw new \Exception("Status code has changed after update for site: " . $this->site->id);
78+
}
79+
}
80+
81+
protected function performExtraction(PrepareUpdate $prepareResult): void
82+
{
83+
/** Create a separate connection with the extraction password **/
84+
$connection = App::makeWith(Connection::class, [
85+
"baseUrl" => $this->site->url,
86+
"key" => $prepareResult->password
87+
]);
88+
89+
// Ping server
90+
$pingResult = $connection->performExtractionRequest(["task" => "ping"]);
91+
92+
if (empty($pingResult["message"]) || $pingResult["message"] === 'Invalid login') {
93+
throw new \Exception(
94+
"Invalid ping response for site: " . $this->site->id
95+
);
96+
}
97+
98+
// Start extraction
99+
$stepResult = $connection->performExtractionRequest(["task" => "startExtract"]);
100+
101+
// Run actual core update
102+
while (array_key_exists("done", $stepResult) && $stepResult["done"] !== true) {
103+
if ($stepResult["status"] !== true) {
104+
throw new \Exception(
105+
"Invalid extract response for site: " . $this->site->id
106+
);
107+
}
108+
109+
// Make next extraction step
110+
$stepResult = $connection->performExtractionRequest(
111+
[
112+
"task" => "stepExtract",
113+
"instance" => $stepResult["instance"]
114+
]
115+
);
116+
}
117+
118+
// Clean up restore
119+
$connection->performExtractionRequest(
120+
[
121+
"task" => "finalizeUpdate"
122+
]
123+
);
33124
}
34125
}

app/Models/Site.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace App\Models;
66

77
use App\RemoteSite\Connection;
8+
use GuzzleHttp\Client;
89
use Illuminate\Database\Eloquent\Model;
10+
use Illuminate\Support\Facades\App;
911

1012
class Site extends Model
1113
{
@@ -45,4 +47,12 @@ public function getConnectionAttribute(): Connection
4547
{
4648
return new Connection($this->url, $this->key);
4749
}
50+
51+
public function getFrontendStatus(): int
52+
{
53+
/** @var Client $httpClient */
54+
$httpClient = App::make(Client::class);
55+
56+
return $httpClient->get($this->url)->getStatusCode();
57+
}
4858
}

app/RemoteSite/Connection.php

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,55 @@
66

77
use App\Enum\HttpMethod;
88
use App\Enum\WebserviceEndpoint;
9+
use App\RemoteSite\Responses\FinalizeUpdate as FinalizeUpdateResponse;
910
use App\RemoteSite\Responses\HealthCheck as HealthCheckResponse;
11+
use App\RemoteSite\Responses\GetUpdate as GetUpdateResponse;
12+
use App\RemoteSite\Responses\PrepareUpdate as PrepareUpdateResponse;
13+
use App\RemoteSite\Responses\ResponseInterface;
1014
use GuzzleHttp\Client;
1115
use GuzzleHttp\Exception\RequestException;
1216
use GuzzleHttp\Psr7\Request;
1317
use GuzzleHttp\Psr7\Response;
1418
use Illuminate\Support\Facades\App;
1519
use Psr\Http\Message\RequestInterface;
1620

21+
/**
22+
* @method HealthCheckResponse checkHealth()
23+
* @method GetUpdateResponse getUpdate()
24+
* @method PrepareUpdateResponse prepareUpdate(string $targetVersion)
25+
* @method FinalizeUpdateResponse finalizeUpdate()
26+
*/
1727
class Connection
1828
{
1929
public function __construct(protected readonly string $baseUrl, protected readonly string $key)
2030
{
2131
}
2232

23-
public function checkHealth(): HealthCheckResponse
33+
public function __call(string $method, array $arguments): ResponseInterface
2434
{
25-
$healthData = $this->performWebserviceRequest(
26-
HttpMethod::GET,
27-
WebserviceEndpoint::HEALTH_CHECK
35+
$endpoint = WebserviceEndpoint::tryFromName($method);
36+
37+
if (is_null($endpoint)) {
38+
throw new \BadMethodCallException();
39+
}
40+
41+
// Call
42+
$data = $this->performWebserviceRequest(
43+
$endpoint->getMethod(),
44+
$endpoint->getUrl(),
45+
...$arguments
2846
);
2947

30-
return HealthCheckResponse::from($healthData['data']['attributes']);
48+
$responseClass = $endpoint->getResponseClass();
49+
50+
return $responseClass::from($data);
3151
}
3252

3353
public function performExtractionRequest(array $requestData): array
3454
{
3555
$request = new Request(
3656
'POST',
37-
$this->baseUrl . 'extract.php'
57+
$this->baseUrl . '/extract.php'
3858
);
3959

4060
$data['password'] = $this->key;
@@ -55,12 +75,12 @@ public function performExtractionRequest(array $requestData): array
5575

5676
protected function performWebserviceRequest(
5777
HttpMethod $method,
58-
WebserviceEndpoint $endpoint,
78+
string $endpoint,
5979
array $requestData = []
6080
): array {
6181
$request = new Request(
6282
$method->name,
63-
$this->baseUrl . $endpoint->value,
83+
$this->baseUrl . $endpoint,
6484
[
6585
'X-JUpdate-Token' => $this->key
6686
]
@@ -85,7 +105,7 @@ protected function performWebserviceRequest(
85105
);
86106
}
87107

88-
return $responseData;
108+
return $responseData['data']['attributes'];
89109
}
90110

91111
protected function performHttpRequest(
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace App\RemoteSite\Responses;
4+
5+
use App\DTO\BaseDTO;
6+
7+
class FinalizeUpdate extends BaseDTO implements ResponseInterface
8+
{
9+
public function __construct(
10+
public bool $success
11+
) {
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace App\RemoteSite\Responses;
4+
5+
use App\DTO\BaseDTO;
6+
7+
class GetUpdate extends BaseDTO implements ResponseInterface
8+
{
9+
public function __construct(
10+
public ?string $availableUpdate
11+
) {
12+
}
13+
}

app/RemoteSite/Responses/HealthCheck.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use App\DTO\BaseDTO;
66

7-
class HealthCheck extends BaseDTO
7+
class HealthCheck extends BaseDTO implements ResponseInterface
88
{
99
public function __construct(
1010
public string $php_version,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\RemoteSite\Responses;
4+
5+
use App\DTO\BaseDTO;
6+
7+
class PrepareUpdate extends BaseDTO implements ResponseInterface
8+
{
9+
public function __construct(
10+
public string $password,
11+
public int $filesize
12+
) {
13+
}
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\RemoteSite\Responses;
4+
5+
interface ResponseInterface
6+
{
7+
public static function from(array $data): static;
8+
9+
public function toArray(): array;
10+
}

pint.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
{
2-
"preset": "psr12"
2+
"preset": "psr12",
3+
"notName": [
4+
"WebserviceEndpoint.php"
5+
]
36
}

0 commit comments

Comments
 (0)