diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..749aa1e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..246a71c --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# VanguardBackup PHP SDK + +## Overview + +The VanguardBackup PHP SDK offers a fluent interface for interacting with the VanguardBackup API, enabling efficient management of your backup operations. + +## API Documentation + +While this SDK provides a convenient way to interact with the VanguardBackup API, you may sometimes need more detailed information about the specific parameters and responses for each endpoint. For comprehensive API documentation, including request/response schemas and example payloads, please refer to our official API documentation: + +[VanguardBackup API Documentation](https://docs.vanguardbackup.com/api/introduction) + +This resource will be invaluable when constructing requests or handling responses, especially for more complex operations not fully abstracted by the SDK. + +## Getting Started + +### Installation + +Add the VanguardBackup SDK to your project using Composer: + +```bash +composer require vanguardbackup/vanguard-sdk +``` + +### Initializing the SDK + +Create a new instance of the VanguardBackup client: + +```php +$vanguard = new VanguardBackup\Vanguard\VanguardClient('YOUR_API_KEY'); +``` + +For custom VanguardBackup installations, specify the API base URL: + +```php +$vanguard = new VanguardBackup\Vanguard\VanguardClient('YOUR_API_KEY', 'https://your-vanguard-instance.com/api'); +``` + +## Core Functionalities + +### User Authentication + +Retrieve the authenticated user's information: + +```php +$user = $vanguard->user(); +``` + +### Backup Task Management + +Backup tasks are central to VanguardBackup operations. Here's how to interact with them: + +```php +// List all backup tasks +$tasks = $vanguard->backupTasks(); + +// Get a specific backup task +$task = $vanguard->backupTask($taskId); + +// Create a new backup task +$newTask = $vanguard->createBackupTask([ + 'label' => 'Daily Database Backup', + 'source_type' => 'database', + // ... other task parameters +]); + +// Update an existing backup task +$updatedTask = $vanguard->updateBackupTask($taskId, [ + 'frequency' => 'daily', + // ... other parameters to update +]); + +// Delete a backup task +$vanguard->deleteBackupTask($taskId); + +// Get the status of a backup task +$status = $vanguard->getBackupTaskStatus($taskId); + +// Retrieve the latest log for a backup task +$log = $vanguard->getLatestBackupTaskLog($taskId); + +// Manually trigger a backup task +$vanguard->runBackupTask($taskId); +``` + +Individual `BackupTask` instances also provide methods for common operations: + +```php +$task->update(['label' => 'Weekly Full Backup']); +$task->delete(); +$task->getStatus(); +$task->getLatestLog(); +$task->run(); +``` + +### Remote Server Management + +Manage the servers from which you're backing up data: + +```php +// List all remote servers +$servers = $vanguard->remoteServers(); + +// Get a specific remote server +$server = $vanguard->remoteServer($serverId); + +// Add a new remote server +$newServer = $vanguard->createRemoteServer([ + 'label' => 'Production DB Server', + 'ip_address' => '192.168.1.100', + // ... other server details +]); + +// Update a remote server +$updatedServer = $vanguard->updateRemoteServer($serverId, [ + 'label' => 'Updated Production DB Server', +]); + +// Remove a remote server +$vanguard->deleteRemoteServer($serverId); +``` + +### Backup Destination Management + +Control where your backups are stored: + +```php +// List all backup destinations +$destinations = $vanguard->backupDestinations(); + +// Get a specific backup destination +$destination = $vanguard->backupDestination($destinationId); + +// Create a new backup destination +$newDestination = $vanguard->createBackupDestination([ + 'type' => 's3', + 'bucket' => 'my-backups', + // ... other destination details +]); + +// Update a backup destination +$updatedDestination = $vanguard->updateBackupDestination($destinationId, [ + 'bucket' => 'new-backup-bucket', +]); + +// Remove a backup destination +$vanguard->deleteBackupDestination($destinationId); +``` + +### Tag Management + +Organize your backup resources with tags: + +```php +// List all tags +$tags = $vanguard->tags(); + +// Get a specific tag +$tag = $vanguard->tag($tagId); + +// Create a new tag +$newTag = $vanguard->createTag(['label' => 'Production']); + +// Update a tag +$updatedTag = $vanguard->updateTag($tagId, ['label' => 'Staging']); + +// Delete a tag +$vanguard->deleteTag($tagId); +``` + +### Notification Stream Management + +Configure how you receive alerts about your backups: + +```php +// List all notification streams +$streams = $vanguard->notificationStreams(); + +// Get a specific notification stream +$stream = $vanguard->notificationStream($streamId); + +// Create a new notification stream +$newStream = $vanguard->createNotificationStream([ + 'type' => 'slack', + 'webhook_url' => 'https://hooks.slack.com/services/...', +]); + +// Update a notification stream +$updatedStream = $vanguard->updateNotificationStream($streamId, [ + 'events' => ['backup_failed', 'backup_successful'], +]); + +// Delete a notification stream +$vanguard->deleteNotificationStream($streamId); +``` + +## Security + +For reporting security vulnerabilities, please refer to our [security policy](https://github.com/vanguardbackup/vanguard/security/policy). + +## License + +The VanguardBackup PHP SDK is open-source software, released under the [MIT licence](LICENSE.md). + +## Acknowledgments + +We'd like to express our gratitude to the Laravel Forge PHP SDK, which served as an inspiration for the structure and design of this SDK. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b889508 --- /dev/null +++ b/composer.json @@ -0,0 +1,54 @@ +{ + "name": "vanguardbackup/vanguard-php-sdk", + "description": "The official VanguardBackup PHP SDK.", + "keywords": ["vanguard", "backup", "sdk", "php"], + "license": "MIT", + "support": { + "issues": "https://github.com/vanguardbackup/vanguard-php-sdk/issues", + "source": "https://github.com/vanguardbackup/vanguard-php-sdk" + }, + "authors": [ + { + "name": "Lewis Larsen", + "email": "lewis@larsens.dev" + } + ], + "require": { + "php": "^7.2|^8.0", + "ext-json": "*", + "guzzlehttp/guzzle": "^6.3.1|^7.0" + }, + "require-dev": { + "roave/security-advisories": "dev-latest" + , + "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0", + "mockery/mockery": "^1.3.1", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.4|^9.0|^10.4" + }, + "autoload": { + "psr-4": { + "VanguardBackup\\Vanguard\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "VanguardBackup\\Vanguard\\Tests\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + }, + "laravel": { + "providers": [ + "VanguardBackup\\Vanguard\\VanguardServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..fc71907 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + paths: + - src + + level: 0 + + ignoreErrors: + - "#\\(void\\) is used.#" diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..913ec1c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + ./tests + + + diff --git a/src/Actions/ManagesBackupDestinations.php b/src/Actions/ManagesBackupDestinations.php new file mode 100644 index 0000000..e9d7026 --- /dev/null +++ b/src/Actions/ManagesBackupDestinations.php @@ -0,0 +1,65 @@ +transformCollection( + $this->get('backup-destinations')['data'], BackupDestination::class + ); + } + + /** + * Get a backup destination instance. + * + * @param string $destinationId + * @return BackupDestination + */ + public function backupDestination(string $destinationId): BackupDestination + { + return new BackupDestination($this->get("backup-destinations/{$destinationId}")['data'], $this); + } + + /** + * Create a new backup destination. + * + * @param array $data + * @return BackupDestination + */ + public function createBackupDestination(array $data): BackupDestination + { + return new BackupDestination($this->post('backup-destinations', $data)['data'], $this); + } + + /** + * Update the given backup destination. + * + * @param string $destinationId + * @param array $data + * @return BackupDestination + */ + public function updateBackupDestination(string $destinationId, array $data): BackupDestination + { + return new BackupDestination($this->put("backup-destinations/{$destinationId}", $data)['data'], $this); + } + + /** + * Delete the given backup destination. + * + * @param string $destinationId + * @return void + */ + public function deleteBackupDestination(string $destinationId): void + { + $this->delete("backup-destinations/{$destinationId}"); + } +} \ No newline at end of file diff --git a/src/Actions/ManagesBackupTasks.php b/src/Actions/ManagesBackupTasks.php new file mode 100644 index 0000000..13cc2b2 --- /dev/null +++ b/src/Actions/ManagesBackupTasks.php @@ -0,0 +1,98 @@ +transformCollection( + $this->get('backup-tasks')['data'], BackupTask::class + ); + } + + /** + * Get a backup task instance. + * + * @param string $taskId + * @return BackupTask + */ + public function backupTask(string $taskId): BackupTask + { + return new BackupTask($this->get("backup-tasks/{$taskId}")['data'], $this); + } + + /** + * Create a new backup task. + * + * @param array $data + * @return BackupTask + */ + public function createBackupTask(array $data): BackupTask + { + return new BackupTask($this->post('backup-tasks', $data)['data'], $this); + } + + /** + * Update the given backup task. + * + * @param string $taskId + * @param array $data + * @return BackupTask + */ + public function updateBackupTask(string $taskId, array $data): BackupTask + { + return new BackupTask($this->put("backup-tasks/{$taskId}", $data)['data'], $this); + } + + /** + * Delete the given backup task. + * + * @param string $taskId + * @return void + */ + public function deleteBackupTask(string $taskId): void + { + $this->delete("backup-tasks/{$taskId}"); + } + + /** + * Get the status of a backup task. + * + * @param string $taskId + * @return array + */ + public function getBackupTaskStatus(string $taskId): array + { + return $this->get("backup-tasks/{$taskId}/status")['data']; + } + + /** + * Get the latest log for a backup task. + * + * @param string $taskId + * @return array + */ + public function getLatestBackupTaskLog(string $taskId): array + { + return $this->get("backup-tasks/{$taskId}/latest-log")['data']; + } + + /** + * Run a backup task. + * + * @param string $taskId + * @return array + */ + public function runBackupTask(string $taskId): array + { + return $this->post("backup-tasks/{$taskId}/run"); + } +} \ No newline at end of file diff --git a/src/Actions/ManagesNotificationStreams.php b/src/Actions/ManagesNotificationStreams.php new file mode 100644 index 0000000..c9d0283 --- /dev/null +++ b/src/Actions/ManagesNotificationStreams.php @@ -0,0 +1,65 @@ +transformCollection( + $this->get('notification-streams')['data'], NotificationStream::class + ); + } + + /** + * Get a notification stream instance. + * + * @param string $streamId + * @return NotificationStream + */ + public function notificationStream(string $streamId): NotificationStream + { + return new NotificationStream($this->get("notification-streams/{$streamId}")['data'], $this); + } + + /** + * Create a new notification stream. + * + * @param array $data + * @return NotificationStream + */ + public function createNotificationStream(array $data): NotificationStream + { + return new NotificationStream($this->post('notification-streams', $data)['data'], $this); + } + + /** + * Update the given notification stream. + * + * @param string $streamId + * @param array $data + * @return NotificationStream + */ + public function updateNotificationStream(string $streamId, array $data): NotificationStream + { + return new NotificationStream($this->put("notification-streams/{$streamId}", $data)['data'], $this); + } + + /** + * Delete the given notification stream. + * + * @param string $streamId + * @return void + */ + public function deleteNotificationStream(string $streamId): void + { + $this->delete("notification-streams/{$streamId}"); + } +} \ No newline at end of file diff --git a/src/Actions/ManagesRemoteServers.php b/src/Actions/ManagesRemoteServers.php new file mode 100644 index 0000000..d465890 --- /dev/null +++ b/src/Actions/ManagesRemoteServers.php @@ -0,0 +1,65 @@ +transformCollection( + $this->get('remote-servers')['data'], RemoteServer::class + ); + } + + /** + * Get a remote server instance. + * + * @param string $serverId + * @return RemoteServer + */ + public function remoteServer(string $serverId): RemoteServer + { + return new RemoteServer($this->get("remote-servers/{$serverId}")['data'], $this); + } + + /** + * Create a new remote server. + * + * @param array $data + * @return RemoteServer + */ + public function createRemoteServer(array $data): RemoteServer + { + return new RemoteServer($this->post('remote-servers', $data)['data'], $this); + } + + /** + * Update the given remote server. + * + * @param string $serverId + * @param array $data + * @return RemoteServer + */ + public function updateRemoteServer(string $serverId, array $data): RemoteServer + { + return new RemoteServer($this->put("remote-servers/{$serverId}", $data)['data'], $this); + } + + /** + * Delete the given remote server. + * + * @param string $serverId + * @return void + */ + public function deleteRemoteServer(string $serverId): void + { + $this->delete("remote-servers/{$serverId}"); + } +} \ No newline at end of file diff --git a/src/Actions/ManagesTags.php b/src/Actions/ManagesTags.php new file mode 100644 index 0000000..d471567 --- /dev/null +++ b/src/Actions/ManagesTags.php @@ -0,0 +1,65 @@ +transformCollection( + $this->get('tags')['tags'], Tag::class + ); + } + + /** + * Get a tag instance. + * + * @param string $tagId + * @return Tag + */ + public function tag(string $tagId): Tag + { + return new Tag($this->get("tags/{$tagId}")['tag'], $this); + } + + /** + * Create a new tag. + * + * @param array $data + * @return Tag + */ + public function createTag(array $data): Tag + { + return new Tag($this->post('tags', $data)['tag'], $this); + } + + /** + * Update the given tag. + * + * @param string $tagId + * @param array $data + * @return Tag + */ + public function updateTag(string $tagId, array $data): Tag + { + return new Tag($this->put("tags/{$tagId}", $data)['tag'], $this); + } + + /** + * Delete the given tag. + * + * @param string $tagId + * @return void + */ + public function deleteTag(string $tagId): void + { + $this->delete("tags/{$tagId}"); + } +} \ No newline at end of file diff --git a/src/Exceptions/NotFoundException.php b/src/Exceptions/NotFoundException.php new file mode 100644 index 0000000..e9e96e1 --- /dev/null +++ b/src/Exceptions/NotFoundException.php @@ -0,0 +1,18 @@ +rateLimitResetsAt = $rateLimitReset; + } + + /** + * Get the timestamp when the rate limit will be reset. + * + * @return int|null + */ + public function getRateLimitResetsAt(): ?int + { + return $this->rateLimitResetsAt; + } +} \ No newline at end of file diff --git a/src/Exceptions/ValidationException.php b/src/Exceptions/ValidationException.php new file mode 100644 index 0000000..3cf89cb --- /dev/null +++ b/src/Exceptions/ValidationException.php @@ -0,0 +1,38 @@ +errors = $errors; + } + + /** + * Get the validation errors. + * + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } +} \ No newline at end of file diff --git a/src/Facades/Vanguard.php b/src/Facades/Vanguard.php new file mode 100644 index 0000000..52b274f --- /dev/null +++ b/src/Facades/Vanguard.php @@ -0,0 +1,65 @@ +request('GET', $uri); + } + + /** + * Send a POST request to VanguardBackup API and return the response. + * + * @param string $uri + * @param array $payload + * @return mixed + * @throws Exception + */ + public function post($uri, array $payload = []): mixed + { + return $this->request('POST', $uri, $payload); + } + + /** + * Send a PUT request to VanguardBackup API and return the response. + * + * @param string $uri + * @param array $payload + * @return mixed + * @throws Exception + */ + public function put($uri, array $payload = []): mixed + { + return $this->request('PUT', $uri, $payload); + } + + /** + * Send a DELETE request to VanguardBackup API and return the response. + * + * @param string $uri + * @param array $payload + * @return mixed + * @throws Exception + */ + public function delete($uri, array $payload = []): mixed + { + return $this->request('DELETE', $uri, $payload); + } + + /** + * Send a request to VanguardBackup API and return the response. + * + * @param string $method + * @param string $uri + * @param array $payload + * @return mixed + * @throws Exception + */ + protected function request($method, $uri, array $payload = []): mixed + { + $options = $this->prepareRequestPayload($payload); + + $response = $this->httpClient->request($method, $uri, $options); + + return $this->handleResponse($response); + } + + /** + * Prepare the payload for the request. + * + * @param array $payload + * @return array + */ + protected function prepareRequestPayload(array $payload): array + { + if (isset($payload['json'])) { + return ['json' => $payload['json']]; + } + + return empty($payload) ? [] : ['form_params' => $payload]; + } + + /** + * Handle the API response. + * + * @param ResponseInterface $response + * @return mixed + * @throws Exception + */ + protected function handleResponse(ResponseInterface $response): mixed + { + $statusCode = $response->getStatusCode(); + + if ($statusCode < 200 || $statusCode > 299) { + $this->handleRequestError($response); + } + + $responseBody = (string) $response->getBody(); + + return json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR) ?: $responseBody; + } + + /** + * Handle request errors. + * + * @param ResponseInterface $response + * @throws NotFoundException + * @throws ValidationException + * @throws RateLimitExceededException + * @throws Exception + */ + protected function handleRequestError(ResponseInterface $response): void + { + $statusCode = $response->getStatusCode(); + $body = (string) $response->getBody(); + + throw match ($statusCode) { + 422 => new ValidationException(json_decode($body, true, 512, JSON_THROW_ON_ERROR)), + 404 => new NotFoundException(), + 429 => new RateLimitExceededException( + $response->hasHeader('x-ratelimit-reset') + ? (int)$response->getHeader('x-ratelimit-reset')[0] + : null + ), + default => new RuntimeException($body), + }; + } +} \ No newline at end of file diff --git a/src/Resources/BackupDestination.php b/src/Resources/BackupDestination.php new file mode 100644 index 0000000..69c02c1 --- /dev/null +++ b/src/Resources/BackupDestination.php @@ -0,0 +1,135 @@ +client->updateBackupDestination($this->id, $data); + } + + /** + * Delete the given backup destination. + * + * @return void + */ + public function delete() + { + $this->client->deleteBackupDestination($this->id); + } + + /** + * Check if the backup destination is of type S3. + * + * @return bool + */ + public function isS3(): bool + { + return $this->type === 's3'; + } + + /** + * Check if the backup destination is of type custom S3. + * + * @return bool + */ + public function isCustomS3(): bool + { + return $this->type === 'custom_s3'; + } + + /** + * Get the S3 endpoint URL. + * + * @return string|null + */ + public function getS3EndpointUrl(): ?string + { + if ($this->isCustomS3()) { + return $this->s3Endpoint; + } + + if ($this->isS3() && $this->s3Region) { + return "https://s3.{$this->s3Region}.amazonaws.com"; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Resources/BackupTask.php b/src/Resources/BackupTask.php new file mode 100644 index 0000000..57016a5 --- /dev/null +++ b/src/Resources/BackupTask.php @@ -0,0 +1,188 @@ +client->updateBackupTask($this->id, $data); + } + + /** + * Delete the given backup task. + * + * @return void + */ + public function delete() + { + $this->client->deleteBackupTask($this->id); + } + + /** + * Get the status of the backup task. + * + * @return array + */ + public function getStatus(): array + { + return $this->client->getBackupTaskStatus($this->id); + } + + /** + * Get the latest log for the backup task. + * + * @return array + */ + public function getLatestLog(): array + { + return $this->client->getLatestBackupTaskLog($this->id); + } + + /** + * Run the backup task. + * + * @return array + */ + public function run(): array + { + return $this->client->runBackupTask($this->id); + } + + /** + * Check if the backup task is for a database. + * + * @return bool + */ + public function isDatabaseBackup(): bool + { + return $this->source['type'] === 'database'; + } + + /** + * Check if the backup task is for files. + * + * @return bool + */ + public function isFileBackup(): bool + { + return $this->source['type'] === 'files'; + } + + /** + * Get the frequency of the backup task. + * + * @return string + */ + public function getFrequency(): string + { + return $this->schedule['frequency']; + } + + /** + * Get the time the backup task is scheduled to run. + * + * @return string + */ + public function getScheduledTime(): string + { + return $this->schedule['time']; + } +} \ No newline at end of file diff --git a/src/Resources/NotificationStream.php b/src/Resources/NotificationStream.php new file mode 100644 index 0000000..cd8cc48 --- /dev/null +++ b/src/Resources/NotificationStream.php @@ -0,0 +1,96 @@ +client->updateNotificationStream($this->id, $data); + } + + /** + * Delete the given notification stream. + * + * @return void + */ + public function delete(): void + { + $this->client->deleteNotificationStream($this->id); + } + + /** + * Check if the notification stream sends notifications on successful backups. + * + * @return bool + */ + public function notifiesOnSuccess(): bool + { + return $this->notifications['on_success'] ?? false; + } + + /** + * Check if the notification stream sends notifications on failed backups. + * + * @return bool + */ + public function notifiesOnFailure(): bool + { + return $this->notifications['on_failure'] ?? false; + } +} \ No newline at end of file diff --git a/src/Resources/RemoteServer.php b/src/Resources/RemoteServer.php new file mode 100644 index 0000000..30428f6 --- /dev/null +++ b/src/Resources/RemoteServer.php @@ -0,0 +1,146 @@ +client->updateRemoteServer($this->id, $data); + } + + /** + * Delete the given remote server. + * + * @return void + */ + public function delete() + { + $this->client->deleteRemoteServer($this->id); + } + + /** + * Get the IP address of the remote server. + * + * @return string + */ + public function getIpAddress(): string + { + return $this->connection['ip_address']; + } + + /** + * Get the username for the remote server connection. + * + * @return string + */ + public function getUsername(): string + { + return $this->connection['username']; + } + + /** + * Get the port for the remote server connection. + * + * @return int + */ + public function getPort(): int + { + return $this->connection['port']; + } + + /** + * Check if a database password is set for the remote server. + * + * @return bool + */ + public function isDatabasePasswordSet(): bool + { + return $this->connection['is_database_password_set']; + } + + /** + * Get the connectivity status of the remote server. + * + * @return string + */ + public function getConnectivityStatus(): string + { + return $this->status['connectivity']; + } + + /** + * Get the last connected timestamp of the remote server. + * + * @return string|null + */ + public function getLastConnectedAt(): ?string + { + return $this->status['last_connected_at']; + } + + /** + * Check if the remote server is currently connected. + * + * @return bool + */ + public function isConnected(): bool + { + return $this->getConnectivityStatus() === 'connected'; + } +} \ No newline at end of file diff --git a/src/Resources/Resource.php b/src/Resources/Resource.php new file mode 100644 index 0000000..4d98c37 --- /dev/null +++ b/src/Resources/Resource.php @@ -0,0 +1,111 @@ +attributes = $attributes; + $this->client = $client; + + $this->fill(); + } + + /** + * Fill the resource with the array of attributes. + * + * @return void + */ + protected function fill(): void + { + foreach ($this->attributes as $key => $value) { + $key = $this->camelCase($key); + + $this->{$key} = $value; + } + } + + /** + * Convert the key name to camel case. + * + * @param string $key + * @return string + */ + protected function camelCase(string $key): string + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + } + + /** + * Transform the items of the collection to the given class. + * + * @param array $collection + * @param string $class + * @param array $extraData + * @return array + */ + protected function transformCollection(array $collection, string $class, array $extraData = []): array + { + return array_map(function ($data) use ($class, $extraData) { + return new $class($data + $extraData, $this->client); + }, $collection); + } + + /** + * Transform the collection of tags to a string. + * + * @param array $tags + * @param string $separator + * @return string + */ + protected function transformTags(array $tags, string $separator = ', '): string + { + return implode($separator, array_column($tags ?? [], 'name')); + } + + /** + * Get an attribute from the resource. + * + * @param string $key + * @return mixed + */ + public function getAttribute(string $key): mixed + { + return $this->attributes[$key] ?? null; + } + + /** + * Get all attributes from the resource. + * + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } +} \ No newline at end of file diff --git a/src/Resources/Tag.php b/src/Resources/Tag.php new file mode 100644 index 0000000..f7e34a5 --- /dev/null +++ b/src/Resources/Tag.php @@ -0,0 +1,69 @@ +client->updateTag($this->id, $data); + } + + /** + * Delete the given tag. + * + * @return void + */ + public function delete(): void + { + $this->client->deleteTag($this->id); + } +} \ No newline at end of file diff --git a/src/Resources/User.php b/src/Resources/User.php new file mode 100644 index 0000000..c212a27 --- /dev/null +++ b/src/Resources/User.php @@ -0,0 +1,181 @@ +id = $attributes['id']; + $this->name = $attributes['personal_info']['name']; + $this->firstName = $attributes['personal_info']['first_name']; + $this->lastName = $attributes['personal_info']['last_name']; + $this->email = $attributes['personal_info']['email']; + $this->avatarUrl = $attributes['personal_info']['avatar_url']; + $this->timezone = $attributes['account_settings']['timezone']; + $this->language = $attributes['account_settings']['language']; + $this->isAdmin = $attributes['account_settings']['is_admin']; + $this->githubLoginEnabled = $attributes['account_settings']['github_login_enabled']; + $this->weeklySummaryEnabled = $attributes['account_settings']['weekly_summary_enabled']; + $this->totalBackupTasks = $attributes['backup_tasks']['total']; + $this->activeBackupTasks = $attributes['backup_tasks']['active']; + $this->totalBackupLogs = $attributes['backup_tasks']['logs']['total']; + $this->todayBackupLogs = $attributes['backup_tasks']['logs']['today']; + $this->remoteServers = $attributes['related_entities']['remote_servers']; + $this->backupDestinations = $attributes['related_entities']['backup_destinations']; + $this->tags = $attributes['related_entities']['tags']; + $this->notificationStreams = $attributes['related_entities']['notification_streams']; + $this->accountCreated = $attributes['timestamps']['account_created']; + } +} \ No newline at end of file diff --git a/src/VanguardClient.php b/src/VanguardClient.php new file mode 100644 index 0000000..ee42390 --- /dev/null +++ b/src/VanguardClient.php @@ -0,0 +1,132 @@ +setBaseUrl($baseUrl); + } + + if (! is_null($apiKey)) { + $this->setApiKey($apiKey, $httpClient); + } + + if (! is_null($httpClient)) { + $this->httpClient = $httpClient; + } + } + + /** + * Transform the items of the collection to the given class. + * + * @param array $collection + * @param string $class + * @param array $extraData + * @return array + */ + protected function transformCollection($collection, $class, $extraData = []) + { + return array_map(function ($data) use ($class, $extraData) { + return new $class($data + $extraData, $this); + }, $collection); + } + + /** + * Set the API key and set up the HTTP client. + * + * @param string $apiKey + * @param \GuzzleHttp\Client|null $httpClient + * @return $this + */ + public function setApiKey($apiKey, $httpClient = null): static + { + $this->apiKey = $apiKey; + + $this->httpClient = $httpClient ?: new HttpClient([ + 'base_uri' => $this->baseUrl, + 'http_errors' => false, + 'headers' => [ + 'Authorization' => 'Bearer '.$this->apiKey, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + + return $this; + } + + /** + * Set the base URL for the VanguardBackup API. + * + * @param string $url + * @return $this + */ + public function setBaseUrl(string $url): static + { + $this->baseUrl = rtrim($url, '/'); + + return $this; + } + + /** + * Get the base URL for the VanguardBackup API. + * + * @return string + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + /** + * Get an authenticated user instance. + * + * @return User + */ + public function user(): User + { + return new User($this->get('user')['data']); + } +} \ No newline at end of file diff --git a/src/VanguardManager.php b/src/VanguardManager.php new file mode 100644 index 0000000..f4aecc8 --- /dev/null +++ b/src/VanguardManager.php @@ -0,0 +1,78 @@ +client = new VanguardClient($apiKey, $baseUrl, $httpClient); + } + + /** + * Dynamically forward method calls to the VanguardClient instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call(string $method, array $parameters) + { + return $this->forwardCallTo($this->client, $method, $parameters); + } + + /** + * Get the underlying VanguardClient instance. + * + * @return VanguardClient + */ + public function getClient(): VanguardClient + { + return $this->client; + } + + /** + * Set the base URL for the VanguardBackup API. + * + * @param string $url + * @return $this + */ + public function setBaseUrl(string $url): self + { + $this->client->setBaseUrl($url); + + return $this; + } + + /** + * Get the base URL for the VanguardBackup API. + * + * @return string + */ + public function getBaseUrl(): string + { + return $this->client->getBaseUrl(); + } +} \ No newline at end of file diff --git a/src/VanguardServiceProvider.php b/src/VanguardServiceProvider.php new file mode 100644 index 0000000..8a5aa6d --- /dev/null +++ b/src/VanguardServiceProvider.php @@ -0,0 +1,19 @@ +app->singleton(VanguardManager::class, function ($app) { + return new VanguardManager($app['config']->get('services.vanguard.token')); + }); + } +} \ No newline at end of file diff --git a/tests/VanguardTest.php b/tests/VanguardTest.php new file mode 100644 index 0000000..695037b --- /dev/null +++ b/tests/VanguardTest.php @@ -0,0 +1,121 @@ +shouldReceive('request')->once()->with('GET', 'tags', [])->andReturn( + new Response(200, [], '{"data": [{"id": 1, "label": "Test Tag"}]}') + ); + + $this->assertCount(1, $vanguard->tags()); + } + + public function test_update_backup_task(): void + { + $vanguard = new VanguardClient('123', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request')->once()->with('PUT', 'backup-tasks/456', [ + 'json' => ['label' => 'Updated Backup Task'], + ])->andReturn( + new Response(200, [], '{"data": {"id": 456, "label": "Updated Backup Task"}}') + ); + + $this->assertSame('Updated Backup Task', $vanguard->updateBackupTask('456', [ + 'label' => 'Updated Backup Task', + ])->label); + } + + public function test_handling_validation_errors(): void + { + $vanguard = new VanguardClient('123', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request')->once()->with('GET', 'tags', [])->andReturn( + new Response(422, [], '{"error": "Validation Error", "message": "The given data was invalid.", "errors": {"label": ["The label field is required."]}}') + ); + + try { + $vanguard->tags(); + } catch (ValidationException $e) { + $this->assertEquals(['label' => ['The label field is required.']], $e->getErrors()); + } + } + + public function test_handling_404_errors(): void + { + $this->expectException(NotFoundException::class); + + $vanguard = new VanguardClient('123', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request')->once()->with('GET', 'tags', [])->andReturn( + new Response(404) + ); + + $vanguard->tags(); + } + + public function testRateLimitExceededWithHeaderSet(): void + { + $vanguard = new VanguardClient('123', $http = Mockery::mock(Client::class)); + + $timestamp = strtotime(date('Y-m-d H:i:s')); + + $http->shouldReceive('request')->once()->with('GET', 'tags', [])->andReturn( + new Response(429, [ + 'x-ratelimit-reset' => $timestamp, + ], 'Too Many Attempts.') + ); + + try { + $vanguard->tags(); + } catch (RateLimitExceededException $e) { + $this->assertSame($timestamp, $e->getRateLimitResetsAt()); + } + } + + public function testRateLimitExceededWithHeaderNotAvailable(): void + { + $vanguard = new VanguardClient('123', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request')->once()->with('GET', 'tags', [])->andReturn( + new Response(429, [], 'Too Many Attempts.') + ); + + try { + $vanguard->tags(); + } catch (RateLimitExceededException $e) { + $this->assertNull($e->getRateLimitResetsAt()); + } + } + + public function test_run_backup_task(): void + { + $vanguard = new VanguardClient('123', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request')->once()->with('POST', 'backup-tasks/789/run', [])->andReturn( + new Response(200, [], '{"message": "Backup task initiated successfully."}') + ); + + $response = $vanguard->runBackupTask('789'); + $this->assertArrayHasKey('message', $response); + $this->assertEquals('Backup task initiated successfully.', $response['message']); + } +} \ No newline at end of file