diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c69e12152..55c13a3e0 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -46,6 +46,8 @@ protected function schedule(Schedule $schedule): void return (new CronExpression($settings->speedtest_schedule)) ->isDue(now()->timezone($settings->timezone ?? 'UTC')); }); + $schedule->command('model:prune')->daily() + ->timezone($settings->timezone ?? 'UTC'); } /** diff --git a/app/Http/Controllers/API/Speedtest/GetLatestController.php b/app/Http/Controllers/API/Speedtest/GetLatestController.php deleted file mode 100644 index 9ff4bc454..000000000 --- a/app/Http/Controllers/API/Speedtest/GetLatestController.php +++ /dev/null @@ -1,44 +0,0 @@ -latest() - ->first(); - - if (! $latest) { - return response()->json([ - 'message' => 'No results found.', - ], 404); - } - - return response()->json([ - 'message' => 'ok', - 'data' => [ - 'id' => $latest->id, - 'ping' => $latest->ping, - 'download' => ! blank($latest->download) ? toBits(convertSize($latest->download)) : null, - 'upload' => ! blank($latest->upload) ? toBits(convertSize($latest->upload)) : null, - 'server_id' => $latest->server_id, - 'server_host' => $latest->server_host, - 'server_name' => $latest->server_name, - 'url' => $latest->url, - 'scheduled' => $latest->scheduled, - 'failed' => ! $latest->successful, - 'created_at' => $latest->created_at->toISOString(true), - 'updated_at' => $latest->created_at->toISOString(true), // faking updated at to match legacy api payload - ], - ]); - } -} diff --git a/app/Http/Controllers/API/Speedtest/MeasurementController.php b/app/Http/Controllers/API/Speedtest/MeasurementController.php new file mode 100644 index 000000000..50bdb43dd --- /dev/null +++ b/app/Http/Controllers/API/Speedtest/MeasurementController.php @@ -0,0 +1,99 @@ +toString(); + + $config = []; + $settings = new GeneralSettings(); + if (is_array($settings->speedtest_server) && count($settings->speedtest_server)) { + $config = array_merge($config, [ + 'ookla_server_id' => Arr::random($settings->speedtest_server), + ]); + } + try { + ExecSpeedtest::dispatch( + speedtest: $config, + tracking_key: $uuid, + scheduled: false, + tracked: true + ); + $this->createTracking($uuid); + } catch (\Throwable $th) { + Log::warning($th); + return response()->json(['exception' => $th]); + + } + + // Return the UUID to the API caller + return response()->json(['uuid' => $uuid]); + } + + public function getLatest(): JsonResponse + { + $latest = Result::query() + ->latest() + ->first(); + + if (!$latest) { + return response()->json([ + 'message' => 'No results found.', + ], 404); + } + + return response()->json([ + 'message' => 'ok', + 'data' => new ResultResource($latest), + ]); + } + + public function getMeasurementByTrackingId(string $uuid) + { + $trackingData = JobTracking::query()->where('tracking_key', $uuid)->first(); + if ($trackingData == null) { + return response()->json([ + 'status' => 'Not Found', + 'trackingid' => $uuid, + 'message' => "The requested speedtest tracking information, could not be found."], 404); + } + if ($trackingData->status != 'complete') { + return response()->json([ + 'status' => $trackingData->status, + 'trackingid' => $uuid, + 'message' => "The requested speedtest is currently {$trackingData->status}"]); + } + + $result = Result::find($trackingData->result_id); + return response(new ResultResource($result)); + + } + + + private function createTracking(string $tracking_key): void + { + JobTracking::create([ + 'tracking_key' => $tracking_key, + 'status' => JobTrackingStatusEnum::Queued, + 'result_id' => null + ]); + } + + +} diff --git a/app/Http/Resources/ResultResource.php b/app/Http/Resources/ResultResource.php new file mode 100644 index 000000000..47a302c09 --- /dev/null +++ b/app/Http/Resources/ResultResource.php @@ -0,0 +1,77 @@ +id = $result->id; + $this->ping = $result->ping; + $this->download = $result->download; + $this->upload = $result->upload; + $this->server_id = $result->server_id; + $this->server_host = $result->server_host; + $this->server_name = $result->server_name; + $this->url = $result->url; + $this->scheduled = $result->scheduled; + $this->successful = $result->successful; + $this->created_at = $result->created_at; + } + + + /** + * Transform the resource into an array. + * + * @return array + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'ping' => $this->ping, + 'download' => !blank($this->download) ? toBits(convertSize($this->download)) : null, + 'upload' => !blank($this->upload) ? toBits(convertSize($this->upload)) : null, + 'server_id' => $this->server_id, + 'server_host' => $this->server_host, + 'server_name' => $this->server_name, + 'url' => $this->url, + 'scheduled' => $this->scheduled, + 'failed' => !$this->successful, + 'created_at' => $this->created_at->toISOString(true), + 'updated_at' => $this->created_at->toISOString(true), // faking updated at to match legacy api payload + ]; + } + + +} diff --git a/app/Jobs/ExecSpeedtest.php b/app/Jobs/ExecSpeedtest.php index dd76f2032..212dad20e 100644 --- a/app/Jobs/ExecSpeedtest.php +++ b/app/Jobs/ExecSpeedtest.php @@ -2,6 +2,8 @@ namespace App\Jobs; +use App\Models\JobTracking; +use App\Models\JobTrackingStatusEnum; use App\Models\Result; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -22,9 +24,12 @@ class ExecSpeedtest implements ShouldQueue * @return void */ public function __construct( - public ?array $speedtest = null, - public bool $scheduled = false - ) { + public ?array $speedtest = null, + public ?string $tracking_key = '', + public bool $scheduled = false, + public bool $tracked = false, + ) + { } /** @@ -32,13 +37,14 @@ public function __construct( */ public function handle(): void { + $this->updateJobStatus(JobTrackingStatusEnum::Pending); $process = new Process( array_filter([ 'speedtest', '--accept-license', '--accept-gdpr', '--format=json', - optional($this->speedtest)['ookla_server_id'] ? '--server-id='.$this->speedtest['ookla_server_id'] : false, + optional($this->speedtest)['ookla_server_id'] ? '--server-id=' . $this->speedtest['ookla_server_id'] : false, ]) ); @@ -54,6 +60,7 @@ public function handle(): void 'successful' => false, 'data' => $message, ]); + $this->updateJobStatus(JobTrackingStatusEnum::Failed); return; } @@ -62,19 +69,34 @@ public function handle(): void $output = $process->getOutput(); $results = json_decode($output, true); - Result::create([ + $result = Result::create([ 'ping' => $results['ping']['latency'], 'download' => $results['download']['bandwidth'], 'upload' => $results['upload']['bandwidth'], 'server_id' => $results['server']['id'], 'server_name' => $results['server']['name'], - 'server_host' => $results['server']['host'].':'.$results['server']['port'], + 'server_host' => $results['server']['host'] . ':' . $results['server']['port'], 'url' => $results['result']['url'], 'scheduled' => $this->scheduled, 'data' => $output, ]); + $this->updateJobStatus(JobTrackingStatusEnum::Complete, $result->id); } catch (\Exception $e) { + $this->updateJobStatus(JobTrackingStatusEnum::Failed); Log::error($e->getMessage()); } } + + public function updateJobStatus(JobTrackingStatusEnum $status, $result_id = null): void + { + if ($this->tracked) { + JobTracking::where('tracking_key', $this->tracking_key)-> + update([ + 'status' => $status, + 'result_id' => $result_id + + ]); + + } + } } diff --git a/app/Models/JobTracking.php b/app/Models/JobTracking.php new file mode 100644 index 000000000..12a088c1c --- /dev/null +++ b/app/Models/JobTracking.php @@ -0,0 +1,21 @@ +subDays(30)) + ->orWhere('status', JobTrackingStatusEnum::Failed); + } +} diff --git a/app/Models/JobTrackingStatusEnum.php b/app/Models/JobTrackingStatusEnum.php new file mode 100644 index 000000000..95591ad4f --- /dev/null +++ b/app/Models/JobTrackingStatusEnum.php @@ -0,0 +1,12 @@ +id(); + $table->unsignedBigInteger('result_id')->nullable(); + $table->string('status')->default('queued'); + $table->uuid('tracking_key')->unique(); + $table->foreign('result_id')->references('id')->on('results')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_trackings'); + } +}; diff --git a/routes/api.php b/routes/api.php index 96a514837..e4b33338c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,6 @@ user(); }); -Route::get('/speedtest/latest', GetLatestController::class) +Route::get('/speedtest/latest', [MeasurementController::class, 'getLatest']) ->name('speedtest.latest'); + +Route::post('/speedtest', [MeasurementController::class, 'createNew']) + ->name('speedtest.create'); + +Route::get('speedtest/trackingid/{id}', [MeasurementController::class, 'getMeasurementByTrackingId']) + ->name('speedtest.getbytrackingid');