Files
ladill-hosting/app/Services/Hosting/ServerTaskResultService.php
T
isaaccladandCursor e251a4cf60
Deploy Ladill Hosting / deploy (push) Failing after 17s
Initial Ladill Hosting app with Gitea deploy pipeline.
Shared web hosting extracted from the platform monolith, with CI deploy
to /var/www/ladill-hosting matching Bird/Domains/Email.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 16:24:20 +00:00

558 lines
20 KiB
PHP

<?php
namespace App\Services\Hosting;
use App\Models\Domain;
use App\Models\ServerDatabase;
use App\Models\ServerFile;
use App\Models\ServerSite;
use App\Models\ServerTask;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class ServerTaskResultService
{
public function apply(ServerTask $task): ServerTask
{
$task->loadMissing('order', 'agent');
$references = match ($task->type) {
ServerTask::TYPE_DOMAIN_ADD => $this->persistDomainTask($task),
ServerTask::TYPE_SITE_DELETE => $this->persistSiteDeleteTask($task),
ServerTask::TYPE_SSL_REQUEST => $this->persistSslTask($task),
ServerTask::TYPE_SSL_RENEW => $this->persistSslRenewTask($task),
ServerTask::TYPE_DATABASE_CREATE => $this->persistDatabaseTask($task),
ServerTask::TYPE_PHP_CONFIGURE => $this->persistPhpTask($task),
ServerTask::TYPE_CRON_LIST,
ServerTask::TYPE_CRON_UPSERT,
ServerTask::TYPE_CRON_REMOVE => $this->persistCronTask($task),
ServerTask::TYPE_LOG_READ => $this->persistLogTask($task),
ServerTask::TYPE_FILE_LIST,
ServerTask::TYPE_FILE_READ,
ServerTask::TYPE_FILE_WRITE => $this->persistFileTask($task),
default => [],
};
if ($references !== []) {
$result = (array) ($task->result ?? []);
$result['model_refs'] = array_merge((array) ($result['model_refs'] ?? []), $references);
$task->forceFill(['result' => $result])->save();
}
return $task->fresh();
}
/**
* @return array<string, mixed>
*/
private function persistDomainTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
if ($domainName === '') {
return [];
}
$order = $task->order()->first();
$domain = Domain::query()->where('host', $domainName)->first();
if ($domain === null && $order !== null) {
$domain = Domain::create([
'user_id' => $order->user_id,
'host' => $domainName,
'type' => 'server',
'source' => 'server_manager',
'status' => 'pending',
'onboarding_mode' => Domain::MODE_MANUAL_DNS,
'onboarding_state' => Domain::STATE_CONNECTED,
'dns_mode' => 'manual',
'connected_at' => now(),
'verification_meta' => [
'source' => 'server_agent_task',
'customer_hosting_order_id' => $task->customer_hosting_order_id,
],
]);
} elseif ($domain !== null) {
$verificationMeta = array_merge((array) ($domain->verification_meta ?? []), [
'server_agent_task_id' => $task->id,
'customer_hosting_order_id' => $task->customer_hosting_order_id,
]);
$domain->fill([
'user_id' => $domain->user_id ?: $order?->user_id,
'source' => $domain->source ?: 'server_manager',
'connected_at' => $domain->connected_at ?? now(),
'verification_meta' => $verificationMeta,
])->save();
}
$site = ServerSite::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'domain' => $domainName,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'domain_id' => $domain?->id,
'document_root' => (string) ($result['document_root'] ?? data_get($task->payload, 'document_root', '')) ?: null,
'php_version' => (string) data_get($task->payload, 'php_version', '') ?: null,
'php_socket' => (string) ($result['php_socket'] ?? '') ?: null,
'status' => 'active',
'metadata' => array_filter([
'task_type' => $task->type,
'server_task_id' => $task->id,
'payload' => $task->payload,
'result' => Arr::except($result, ['message', 'model_refs']),
], static fn ($value): bool => $value !== null && $value !== []),
]
);
return array_filter([
'domain_id' => $domain?->id,
'server_site_id' => $site->id,
]);
}
/**
* @return array<string, mixed>
*/
private function persistSiteDeleteTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
if ($domainName === '') {
return [];
}
$site = ServerSite::query()
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
->where('domain', $domainName)
->first();
if ($site === null) {
return [];
}
$metadata = array_merge((array) ($site->metadata ?? []), [
'removed_at' => now()->toIso8601String(),
'removed_by_task_id' => $task->id,
]);
$site->fill([
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'status' => 'removed',
'ssl_status' => null,
'metadata' => $metadata,
])->save();
return [
'server_site_id' => $site->id,
];
}
/**
* @return array<string, mixed>
*/
private function persistSslTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
if ($domainName === '') {
return [];
}
$site = ServerSite::query()
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
->where('domain', $domainName)
->first();
if ($site === null) {
$site = ServerSite::create([
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'domain' => $domainName,
'status' => 'active',
'ssl_status' => (string) ($result['ssl'] ?? 'issued'),
'ssl_provisioned_at' => now(),
'metadata' => [
'created_from' => 'ssl.request',
'server_task_id' => $task->id,
],
]);
} else {
$site->fill([
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'ssl_status' => (string) ($result['ssl'] ?? 'issued'),
'ssl_provisioned_at' => now(),
])->save();
}
if ($site->domain_id) {
$domain = Domain::query()->find($site->domain_id);
} else {
$domain = Domain::query()->where('host', $domainName)->first();
}
if ($domain !== null) {
$domain->fill([
'ssl_status' => (string) ($result['ssl'] ?? 'issued'),
'ssl_provisioned_at' => now(),
])->save();
if ($site->domain_id === null) {
$site->forceFill(['domain_id' => $domain->id])->save();
}
}
return array_filter([
'domain_id' => $domain->id ?? null,
'server_site_id' => $site->id,
]);
}
/**
* @return array<string, mixed>
*/
private function persistSslRenewTask(ServerTask $task): array
{
$expiresAt = now()->addDays(90);
ServerSite::query()
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
->whereIn('ssl_status', ['issued', 'active'])
->update([
'ssl_expires_at' => $expiresAt,
'ssl_provisioned_at' => now(),
]);
$domainIds = ServerSite::query()
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
->whereNotNull('domain_id')
->pluck('domain_id');
if ($domainIds->isNotEmpty()) {
Domain::query()
->whereIn('id', $domainIds)
->where('ssl_status', 'active')
->update(['ssl_expires_at' => $expiresAt]);
}
return [];
}
/**
* @return array<string, mixed>
*/
private function persistDatabaseTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
$databaseName = (string) ($result['database'] ?? data_get($task->payload, 'name', ''));
if ($databaseName === '') {
return [];
}
$database = ServerDatabase::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'name' => $databaseName,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'username' => (string) ($result['username'] ?? $databaseName) ?: null,
'password_encrypted' => isset($result['password']) && $result['password'] !== '' ? encrypt((string) $result['password']) : null,
'engine' => 'mariadb',
'status' => 'active',
'metadata' => array_filter([
'server_task_id' => $task->id,
'payload' => $task->payload,
'result' => Arr::except($result, ['message', 'model_refs', 'password']),
], static fn ($value): bool => $value !== null && $value !== []),
]
);
return [
'server_database_id' => $database->id,
];
}
/**
* @return array<string, mixed>
*/
private function persistPhpTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
if ($domainName === '') {
return [];
}
$site = ServerSite::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'domain' => $domainName,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'php_version' => (string) ($result['php_version'] ?? data_get($task->payload, 'php_version', '')) ?: null,
'php_socket' => (string) ($result['php_socket'] ?? '') ?: null,
'status' => 'active',
'metadata' => array_filter([
'php_configured_at' => now()->toIso8601String(),
'server_task_id' => $task->id,
'result' => Arr::except($result, ['message', 'model_refs']),
], static fn ($value): bool => $value !== null && $value !== []),
]
);
return [
'server_site_id' => $site->id,
];
}
/**
* @return array<string, mixed>
*/
private function persistCronTask(ServerTask $task): array
{
$entries = array_values(array_filter((array) data_get($task->result, 'entries', []), 'is_array'));
$order = $task->order()->first();
if ($order !== null) {
$meta = (array) ($order->meta ?? []);
$panelState = (array) ($meta['server_panel'] ?? []);
$panelState['managed_cron_entries'] = $entries;
$panelState['managed_cron_synced_at'] = now()->toIso8601String();
$meta['server_panel'] = $panelState;
$order->update(['meta' => $meta]);
}
return [
'cron_entry_count' => count($entries),
];
}
/**
* @return array<string, mixed>
*/
private function persistLogTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
$contentBase64 = (string) ($result['content_base64'] ?? '');
$content = $contentBase64 !== '' ? base64_decode($contentBase64, true) : false;
$content = is_string($content) ? $content : '';
$target = (string) ($result['target'] ?? data_get($task->payload, 'target', 'log'));
$domain = (string) ($result['domain'] ?? data_get($task->payload, 'domain', ''));
$snapshotKey = $target.($domain !== '' ? ':'.$domain : '');
$order = $task->order()->first();
if ($order !== null) {
$meta = (array) ($order->meta ?? []);
$panelState = (array) ($meta['server_panel'] ?? []);
$panelState['log_snapshots'] = array_merge((array) ($panelState['log_snapshots'] ?? []), [
$snapshotKey => [
'target' => $target,
'domain' => $domain !== '' ? $domain : null,
'lines' => (int) ($result['lines'] ?? data_get($task->payload, 'lines', 0)),
'path' => $result['path'] ?? null,
'read_at' => now()->toIso8601String(),
'content_preview' => $this->previewForContent($content),
],
]);
$meta['server_panel'] = $panelState;
$order->update(['meta' => $meta]);
}
return [
'log_snapshot_key' => $snapshotKey,
];
}
/**
* @return array<string, mixed>
*/
private function persistFileTask(ServerTask $task): array
{
$result = (array) ($task->result ?? []);
return match ($task->type) {
ServerTask::TYPE_FILE_LIST => $this->persistFileListTask($task, $result),
ServerTask::TYPE_FILE_READ => $this->persistFileReadTask($task, $result),
ServerTask::TYPE_FILE_WRITE => $this->persistFileWriteTask($task, $result),
default => [],
};
}
/**
* @param array<string, mixed> $result
* @return array<string, mixed>
*/
private function persistFileListTask(ServerTask $task, array $result): array
{
$basePath = (string) data_get($task->payload, 'path', '');
$entries = array_values(array_filter((array) ($result['entries'] ?? []), 'is_array'));
$recordIds = [];
if ($basePath !== '') {
$rootRecord = ServerFile::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'path' => $basePath,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'kind' => 'directory',
'exists' => true,
'metadata' => [
'entry_count' => count($entries),
'server_task_id' => $task->id,
],
'last_synced_at' => now(),
]
);
$recordIds[] = $rootRecord->id;
}
foreach ($entries as $entry) {
$path = (string) ($entry['path'] ?? '');
if ($path === '') {
continue;
}
$record = ServerFile::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'path' => $path,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'kind' => $this->normalizeFileKind((string) ($entry['type'] ?? 'f')),
'exists' => true,
'size_bytes' => is_numeric($entry['size'] ?? null) ? (int) $entry['size'] : null,
'metadata' => [
'listed_name' => $entry['name'] ?? basename($path),
'server_task_id' => $task->id,
],
'last_synced_at' => now(),
]
);
$recordIds[] = $record->id;
}
return [
'server_file_ids' => array_values(array_unique($recordIds)),
];
}
/**
* @param array<string, mixed> $result
* @return array<string, mixed>
*/
private function persistFileReadTask(ServerTask $task, array $result): array
{
$path = (string) ($result['path'] ?? data_get($task->payload, 'path', ''));
if ($path === '') {
return [];
}
$contentBase64 = (string) ($result['content_base64'] ?? '');
$content = $contentBase64 !== '' ? base64_decode($contentBase64, true) : false;
$content = is_string($content) ? $content : '';
$file = ServerFile::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'path' => $path,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'kind' => 'file',
'exists' => true,
'size_bytes' => strlen($content),
'content_hash' => $content !== '' ? hash('sha256', $content) : null,
'content_preview' => $this->previewForContent($content),
'metadata' => [
'encoding' => 'base64',
'server_task_id' => $task->id,
],
'last_synced_at' => now(),
]
);
return [
'server_file_id' => $file->id,
];
}
/**
* @param array<string, mixed> $result
* @return array<string, mixed>
*/
private function persistFileWriteTask(ServerTask $task, array $result): array
{
$path = (string) ($result['path'] ?? data_get($task->payload, 'path', ''));
if ($path === '') {
return [];
}
$content = (string) data_get($task->payload, 'content', '');
$file = ServerFile::query()->updateOrCreate(
[
'customer_hosting_order_id' => $task->customer_hosting_order_id,
'path' => $path,
],
[
'server_agent_id' => $task->server_agent_id,
'latest_server_task_id' => $task->id,
'kind' => 'file',
'exists' => true,
'size_bytes' => strlen($content),
'content_hash' => $content !== '' ? hash('sha256', $content) : null,
'content_preview' => $this->previewForContent($content),
'metadata' => [
'server_task_id' => $task->id,
'write_source' => 'server_agent',
],
'last_synced_at' => now(),
]
);
return [
'server_file_id' => $file->id,
];
}
private function normalizeFileKind(string $type): string
{
return match ($type) {
'd' => 'directory',
'l' => 'symlink',
default => 'file',
};
}
private function previewForContent(string $content): ?string
{
if ($content === '') {
return null;
}
if (preg_match('//u', $content) !== 1) {
return Str::limit(base64_encode(substr($content, 0, 2000)), 4000, '');
}
return (string) Str::of($content)
->replaceMatches('/[^\P{C}\t\r\n]/u', '')
->limit(4000, '');
}
}