From e1bc7e3e623638e8d38a11c91409c0bb864d6d64 Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 8 Jun 2026 09:37:59 +0200 Subject: [PATCH 1/9] Added support for Hetzner Storage Box api. --- README.md | 2 +- .../storage_boxes/create_a_storage_box.php | 24 + .../create_a_storage_box_subaccount.php | 29 + .../get_all_storage_box_types.php | 7 + src/HetznerAPIClient.php | 54 ++ src/Models/StorageBoxTypes/StorageBoxType.php | 99 +++ .../StorageBoxTypes/StorageBoxTypePrice.php | 60 ++ .../StorageBoxTypes/StorageBoxTypes.php | 151 ++++ src/Models/StorageBoxes/StorageBox.php | 798 ++++++++++++++++++ .../StorageBoxes/StorageBoxAccessSettings.php | 85 ++ src/Models/StorageBoxes/StorageBoxAction.php | 46 + src/Models/StorageBoxes/StorageBoxActions.php | 133 +++ .../StorageBoxes/StorageBoxRequestOpts.php | 23 + .../StorageBoxes/StorageBoxSnapshot.php | 79 ++ .../StorageBoxSnapshotPlanRequest.php | 76 ++ src/Models/StorageBoxes/StorageBoxStats.php | 50 ++ .../StorageBoxes/StorageBoxSubaccount.php | 92 ++ .../StorageBoxSubaccountAccessSettings.php | 52 ++ src/Models/StorageBoxes/StorageBoxes.php | 202 +++++ .../StorageBoxTypes/StorageBoxTypeTest.php | 44 + .../StorageBoxTypes/StorageBoxTypesTest.php | 67 ++ .../fixtures/storage_box_type.json | 20 + .../fixtures/storage_box_types.json | 32 + .../Models/StorageBoxes/StorageBoxTest.php | 334 ++++++++ .../Models/StorageBoxes/StorageBoxesTest.php | 101 +++ .../Models/StorageBoxes/fixtures/action.json | 14 + .../Models/StorageBoxes/fixtures/actions.json | 26 + .../StorageBoxes/fixtures/snapshot.json | 17 + .../fixtures/snapshot_create.json | 19 + .../StorageBoxes/fixtures/snapshots.json | 19 + .../StorageBoxes/fixtures/storage_box.json | 58 ++ .../fixtures/storage_box_create.json | 68 ++ .../StorageBoxes/fixtures/storage_boxes.json | 70 ++ .../StorageBoxes/fixtures/subaccount.json | 22 + .../fixtures/subaccount_create.json | 19 + .../StorageBoxes/fixtures/subaccounts.json | 24 + 36 files changed, 3015 insertions(+), 1 deletion(-) create mode 100644 examples/storage_boxes/create_a_storage_box.php create mode 100644 examples/storage_boxes/create_a_storage_box_subaccount.php create mode 100644 examples/storage_boxes/get_all_storage_box_types.php create mode 100644 src/Models/StorageBoxTypes/StorageBoxType.php create mode 100644 src/Models/StorageBoxTypes/StorageBoxTypePrice.php create mode 100644 src/Models/StorageBoxTypes/StorageBoxTypes.php create mode 100644 src/Models/StorageBoxes/StorageBox.php create mode 100644 src/Models/StorageBoxes/StorageBoxAccessSettings.php create mode 100644 src/Models/StorageBoxes/StorageBoxAction.php create mode 100644 src/Models/StorageBoxes/StorageBoxActions.php create mode 100644 src/Models/StorageBoxes/StorageBoxRequestOpts.php create mode 100644 src/Models/StorageBoxes/StorageBoxSnapshot.php create mode 100644 src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php create mode 100644 src/Models/StorageBoxes/StorageBoxStats.php create mode 100644 src/Models/StorageBoxes/StorageBoxSubaccount.php create mode 100644 src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php create mode 100644 src/Models/StorageBoxes/StorageBoxes.php create mode 100644 tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php create mode 100644 tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php create mode 100644 tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json create mode 100644 tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json create mode 100644 tests/Unit/Models/StorageBoxes/StorageBoxTest.php create mode 100644 tests/Unit/Models/StorageBoxes/StorageBoxesTest.php create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/action.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/actions.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/snapshot.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/snapshots.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/storage_box.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/subaccount.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json create mode 100644 tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json diff --git a/README.md b/README.md index ae4f1270..e1422014 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Total Downloads](https://poser.pugx.org/lkdevelopment/hetzner-cloud-php-sdk/downloads)](https://packagist.org/packages/lkdevelopment/hetzner-cloud-php-sdk) [![Actions Status](https://github.com/lkdevelopment/hetzner-cloud-php-sdk/workflows/CI/badge.svg)](https://github.com/lkdevelopment/hetzner-cloud-php-sdk/actions) # Hetzner Cloud PHP SDK -A PHP SDK for the Hetzner Cloud API: https://docs.hetzner.cloud/ +A PHP SDK for both Hetzner Cloud API: https://docs.hetzner.cloud/ and Storage Box API: https://docs.hetzner.cloud/ ## Installation You can install the package via composer: diff --git a/examples/storage_boxes/create_a_storage_box.php b/examples/storage_boxes/create_a_storage_box.php new file mode 100644 index 00000000..278ff3fd --- /dev/null +++ b/examples/storage_boxes/create_a_storage_box.php @@ -0,0 +1,24 @@ +serverTypes()->get(1); +$location = $hetznerClient->locations()->getByName('fsn1'); +$type = $hetznerClient->storageBoxTypes()->get('bx11'); + +$response = $hetznerClient->storageBoxes()->create( + name: 'my-storage-box', + location: $location->name, + storageBoxType: $type->name, + password: '{my s3cr3t p@ssword}', + labels: ['type' => 'untest'], + accessSettings: new StorageBoxAccessSettings(reachable_externally:true), +); + +$response->getResponsePart('action')->waitUntilCompleted(); +$box = $response->getResponsePart('storage_box')->reload(); + +echo "Name: {$box->name}" . PHP_EOL; +echo "ID: {$box->id}" . PHP_EOL; diff --git a/examples/storage_boxes/create_a_storage_box_subaccount.php b/examples/storage_boxes/create_a_storage_box_subaccount.php new file mode 100644 index 00000000..92c02b9e --- /dev/null +++ b/examples/storage_boxes/create_a_storage_box_subaccount.php @@ -0,0 +1,29 @@ +storageBoxes()->get('some-existing-box'); + +$response = $box->createSubaccount( + 'my_home_dir', + 'MySecret!1234', + 'some_name', + new StorageBoxAccessSettings( + reachable_externally: true, + ssh_enabled: true, + ), + 'Description', + [ + 'some' => 'label', + ], +); +$response->getResponsePart('action')->waitUntilCompleted(); +$account = $response->getResponsePart('subaccount')->reload(); + +echo "Name: {$account->name}" . PHP_EOL; +echo "ID: {$account->id}" . PHP_EOL; +echo "HomeDir: {$account->home_directory}" . PHP_EOL; + + diff --git a/examples/storage_boxes/get_all_storage_box_types.php b/examples/storage_boxes/get_all_storage_box_types.php new file mode 100644 index 00000000..8895064a --- /dev/null +++ b/examples/storage_boxes/get_all_storage_box_types.php @@ -0,0 +1,7 @@ +storageBoxTypes()->all() as $type) { + echo "Name: {$type->name} - ID: {$type->id}" . PHP_EOL; +} diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index 0998f44b..3947847e 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -6,6 +6,9 @@ use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\Models\Actions\Actions; use LKDev\HetznerCloud\Models\Certificates\Certificates; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxActions; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxes; +use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxTypes; use LKDev\HetznerCloud\Models\Datacenters\Datacenters; use LKDev\HetznerCloud\Models\Firewalls\Firewalls; use LKDev\HetznerCloud\Models\FloatingIps\FloatingIps; @@ -63,6 +66,11 @@ class HetznerAPIClient */ protected GuzzleClient $httpClient; + /** + * @var \LKDev\HetznerCloud\Clients\GuzzleClient|null + */ + protected ?GuzzleClient $storageHttpClient = null; + /** * @param string $apiToken * @param string $baseUrl @@ -141,6 +149,28 @@ public function setHttpClient(GuzzleClient $client): self return $this; } + /** + * @return GuzzleClient + */ + public function getStorageHttpClient(): GuzzleClient + { + if ($this->storageHttpClient === null) { + $this->storageHttpClient = new GuzzleClient($this, ['base_uri' => 'https://api.hetzner.com/v1/']); + } + + return $this->storageHttpClient; + } + + /** + * @return $this + */ + public function setStorageHttpClient(GuzzleClient $client): self + { + $this->storageHttpClient = $client; + + return $this; + } + /** * @param \Psr\Http\Message\ResponseInterface $response * @@ -333,6 +363,30 @@ public function zones() return new Zones($this->httpClient); } + /** + * @return StorageBoxes + */ + public function storageBoxes(): StorageBoxes + { + return new StorageBoxes($this->getStorageHttpClient()); + } + + /** + * @return StorageBoxTypes + */ + public function storageBoxTypes(): StorageBoxTypes + { + return new StorageBoxTypes($this->getStorageHttpClient()); + } + + /** + * @return StorageBoxActions + */ + public function storageBoxActions(): StorageBoxActions + { + return new StorageBoxActions($this->getStorageHttpClient()); + } + /** * @return GuzzleClient */ diff --git a/src/Models/StorageBoxTypes/StorageBoxType.php b/src/Models/StorageBoxTypes/StorageBoxType.php new file mode 100644 index 00000000..795b7a43 --- /dev/null +++ b/src/Models/StorageBoxTypes/StorageBoxType.php @@ -0,0 +1,99 @@ +getStorageHttpClient() : null); + parent::__construct($storageClient); + } + + /** + * @param $data + * @return $this + */ + public function setAdditionalData($data) + { + $this->id = $data->id; + $this->name = $data->name; + $this->description = $data->description; + $this->snapshot_limit = $data->snapshot_limit ?? null; + $this->automatic_snapshot_limit = $data->automatic_snapshot_limit ?? null; + $this->subaccounts_limit = $data->subaccounts_limit; + $this->size = $data->size; + $this->prices = isset($data->prices) ? array_map(function ($price) { + return StorageBoxTypePrice::parse($price); + }, $data->prices) : null; + $this->deprecation = $data->deprecation ?? null; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self())->setAdditionalData($input); + } +} diff --git a/src/Models/StorageBoxTypes/StorageBoxTypePrice.php b/src/Models/StorageBoxTypes/StorageBoxTypePrice.php new file mode 100644 index 00000000..8a7c23e9 --- /dev/null +++ b/src/Models/StorageBoxTypes/StorageBoxTypePrice.php @@ -0,0 +1,60 @@ +location = $location; + $this->price_hourly = $priceHourly; + $this->price_monthly = $priceMonthly; + $this->setup_fee = $setupFee; + } + + /** + * @param $input + * @return self|null + */ + public static function parse($input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->location ?? '', + Price::parse($input->price_hourly ?? null), + Price::parse($input->price_monthly ?? null), + Price::parse($input->setup_fee ?? null) + ); + } +} diff --git a/src/Models/StorageBoxTypes/StorageBoxTypes.php b/src/Models/StorageBoxTypes/StorageBoxTypes.php new file mode 100644 index 00000000..96a1f11c --- /dev/null +++ b/src/Models/StorageBoxTypes/StorageBoxTypes.php @@ -0,0 +1,151 @@ +getStorageHttpClient() : null); + parent::__construct($storageClient); + } + + /** + * Returns all Storage Box type objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/list_storage_box_types + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * Returns a page of Storage Box type objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/list_storage_box_types + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + $response = $this->httpClient->get('storage_box_types'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + $this->_getKeys()['many'] => self::parse($resp->{$this->_getKeys()['many']})->{$this->_getKeys()['many']}, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific Storage Box type by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/get_storage_box_type + * + * @param int $id + * @return StorageBoxType|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $id): ?StorageBoxType + { + $response = $this->httpClient->get('storage_box_types/'.$id); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxType::parse(json_decode((string) $response->getBody())->storage_box_type); + } + + return null; + } + + /** + * Returns a specific Storage Box type by name. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/list_storage_box_types + * + * @param string $name + * @return StorageBoxType|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getByName(string $name): ?StorageBoxType + { + $types = $this->list(); + + foreach ($types->storage_box_types as $type) { + if ($type->name === $name) { + return $type; + } + } + + return null; + } + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->storage_box_types = array_map(function ($type) { + return StorageBoxType::parse($type); + }, $input); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'storage_box_type', 'many' => 'storage_box_types']; + } +} diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php new file mode 100644 index 00000000..f6a995df --- /dev/null +++ b/src/Models/StorageBoxes/StorageBox.php @@ -0,0 +1,798 @@ +id = $id; + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getStorageHttpClient() : null); + parent::__construct($storageClient); + } + + /** + * @param $data + * @return $this + */ + public function setAdditionalData($data) + { + $this->id = $data->id; + $this->name = $data->name; + $this->storage_box_type = $data->storage_box_type ? StorageBoxType::parse($data->storage_box_type) : null; + $this->location = $data->location ? Location::parse($data->location) : null; + $this->access_settings = $data->access_settings ? StorageBoxAccessSettings::parse($data->access_settings) : null; + $this->snapshot_plan = $data->snapshot_plan ?? null; + $this->protection = $data->protection ? Protection::parse($data->protection) : null; + $this->labels = isset($data->labels) ? get_object_vars($data->labels) : []; + $this->status = $data->status; + $this->username = $data->username ?? null; + $this->server = $data->server ?? null; + $this->system = $data->system ?? null; + $this->stats = $data->stats ? StorageBoxStats::parse($data->stats) : null; + $this->created = $data->created; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self($input->id))->setAdditionalData($input); + } + + /** + * Reload the data of the Storage Box. + * + * @return static + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function reload() + { + return HetznerAPIClient::$instance->storageBoxes()->getById($this->id); + } + + /** + * Deletes a Storage Box. This immediately removes the Storage Box and all its data. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/delete_storage_box + * + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function delete(): ?APIResponse + { + $response = $this->httpClient->delete('storage_boxes/'.$this->id); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Updates a Storage Box. Currently supports renaming and updating labels. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/update_storage_box + * + * @param array $data Keys: name (string), labels (object) + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function update(array $data): ?APIResponse + { + $response = $this->httpClient->put('storage_boxes/'.$this->id, ['json' => $data]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'storage_box' => self::parse(json_decode((string) $response->getBody())->storage_box), + ], $response->getHeaders()); + } + + return null; + } + + // --- Actions --- + + /** + * Changes the delete protection of the Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/change_storage_box_protection + * + * @param bool $delete Whether to enable delete protection + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeProtection(bool $delete): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/change_protection', [ + 'json' => ['delete' => $delete], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Upgrades or downgrades a Storage Box to a different type. + * Downgrading is only possible if current usage does not exceed the new type's capacity. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/change_storage_box_type + * + * @param string $storageBoxType ID or name of the target Storage Box type + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeType(string $storageBoxType): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/change_type', [ + 'json' => ['storage_box_type' => $storageBoxType], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Resets the password of the Storage Box. + * The password must comply with the password policy (12–128 chars, mixed case, digits, special chars). + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/reset_storage_box_password + * + * @param string $password New password + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function resetPassword( + #[SensitiveParameter] + string $password + ): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/reset_password', [ + 'json' => ['password' => $password], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Updates the access settings of the Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/update_storage_box_access_settings + * + * @param StorageBoxAccessSettings $settings + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateAccessSettings(StorageBoxAccessSettings $settings): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/update_access_settings', [ + 'json' => $settings->toArray(), + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Enables automatic snapshots on a schedule. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/enable_storage_box_snapshot_plan + * + * @param StorageBoxSnapshotPlanRequest $schedule + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function enableSnapshotPlan(StorageBoxSnapshotPlanRequest $schedule): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/enable_snapshot_plan', [ + 'json' => $schedule->toArray(), + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Disables the automatic snapshot plan. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/disable_storage_box_snapshot_plan + * + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function disableSnapshotPlan(): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/disable_snapshot_plan'); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Restores the Storage Box to the state of a given snapshot. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/rollback_storage_box_snapshot + * + * @param int $snapshotId ID of the snapshot to restore from + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function rollbackSnapshot(int $snapshotId): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/rollback_snapshot', [ + 'json' => ['snapshot_id' => $snapshotId], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + // --- Per-box action listing --- + + /** + * Returns all actions for this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_box_actions + * + * @return StorageBoxAction[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listActions(): array + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/actions'); + if (! HetznerAPIClient::hasError($response)) { + return array_map(function ($action) { + return StorageBoxAction::parse($action); + }, json_decode((string) $response->getBody())->actions); + } + + return []; + } + + /** + * Returns a specific action for this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/get_storage_box_action + * + * @param int $actionId + * @return StorageBoxAction|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getAction(int $actionId): ?StorageBoxAction + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/actions/'.$actionId); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxAction::parse(json_decode((string) $response->getBody())->action); + } + + return null; + } + + // --- Subaccounts --- + + /** + * Returns all subaccounts of this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/list_storage_box_subaccounts + * + * @return StorageBoxSubaccount[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listSubaccounts(): array + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/subaccounts'); + if (! HetznerAPIClient::hasError($response)) { + return array_map(function ($subaccount) { + return StorageBoxSubaccount::parse($subaccount); + }, json_decode((string) $response->getBody())->subaccounts); + } + + return []; + } + + /** + * Creates a new subaccount for this Storage Box. + * Subaccounts share the storage space of the parent Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/create_storage_box_subaccount + * + * @param string $homeDirectory Home directory of the subaccount (e.g. "backups/server01") + * @param string $password Password (must meet the password policy) + * @param string|null $name Display name + * @param StorageBoxSubaccountAccessSettings|null $accessSettings Access settings for the subaccount + * @param string|null $description Optional description + * @param array $labels User-defined labels + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function createSubaccount( + string $homeDirectory, + #[SensitiveParameter] + string $password, + ?string $name = null, + ?StorageBoxSubaccountAccessSettings $accessSettings = null, + ?string $description = null, + array $labels = [] + ): ?APIResponse { + $payload = [ + 'home_directory' => $homeDirectory, + 'password' => $password, + ]; + if ($name !== null) { + $payload['name'] = $name; + } + if ($accessSettings !== null) { + $payload['access_settings'] = $accessSettings->toArray(); + } + if ($description !== null) { + $payload['description'] = $description; + } + if (! empty($labels)) { + $payload['labels'] = $labels; + } + + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/subaccounts', ['json' => $payload]); + if (! HetznerAPIClient::hasError($response)) { + $data = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'subaccount' => $data->subaccount, + 'action' => StorageBoxAction::parse($data->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific subaccount by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/get_storage_box_subaccount + * + * @param int $subaccountId + * @return StorageBoxSubaccount|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getSubaccount(int $subaccountId): ?StorageBoxSubaccount + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxSubaccount::parse(json_decode((string) $response->getBody())->subaccount); + } + + return null; + } + + /** + * Updates a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/update_storage_box_subaccount + * + * @param int $subaccountId + * @param string|null $name Display name + * @param string|null $description Optional description + * @param array|null $labels User-defined labels + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateSubaccount( + int $subaccountId, + ?string $name = null, + ?string $description = null, + ?array $labels = null + ): ?APIResponse { + $payload = []; + if ($name !== null) { + $payload['name'] = $name; + } + if ($description !== null) { + $payload['description'] = $description; + } + if ($labels !== null) { + $payload['labels'] = $labels; + } + + $response = $this->httpClient->put('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId, ['json' => $payload]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'subaccount' => StorageBoxSubaccount::parse(json_decode((string) $response->getBody())->subaccount), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Deletes a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/delete_storage_box_subaccount + * + * @param int $subaccountId + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function deleteSubaccount(int $subaccountId): ?APIResponse + { + $response = $this->httpClient->delete('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Resets the password of a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/reset_storage_box_subaccount_password + * + * @param int $subaccountId + * @param string $password New password (must meet the password policy) + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function resetSubaccountPassword( + int $subaccountId, + #[SensitiveParameter] + string $password + ): ?APIResponse + { + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/reset_subaccount_password', + ['json' => ['password' => $password]] + ); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Changes the home directory of a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/change_storage_box_subaccount_home_directory + * + * @param int $subaccountId + * @param string $homeDirectory New home directory path + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeSubaccountHomeDirectory(int $subaccountId, string $homeDirectory): ?APIResponse + { + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/change_home_directory', + ['json' => ['home_directory' => $homeDirectory]] + ); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Updates the access settings of a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/update_storage_box_subaccount_access_settings + * + * @param int $subaccountId + * @param StorageBoxSubaccountAccessSettings $settings + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateSubaccountAccessSettings(int $subaccountId, StorageBoxSubaccountAccessSettings $settings): ?APIResponse + { + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/update_access_settings', + ['json' => $settings->toArray()] + ); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + // --- Snapshots --- + + /** + * Returns all snapshots of this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/list_storage_box_snapshots + * + * @return StorageBoxSnapshot[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listSnapshots(): array + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/snapshots'); + if (! HetznerAPIClient::hasError($response)) { + return array_map(function ($snapshot) { + return StorageBoxSnapshot::parse($snapshot); + }, json_decode((string) $response->getBody())->snapshots); + } + + return []; + } + + /** + * Creates a new snapshot of this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/create_storage_box_snapshot + * + * @param string|null $description Optional description + * @param array $labels User-defined labels + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function createSnapshot(?string $description = null, array $labels = []): ?APIResponse + { + $payload = []; + if ($description !== null) { + $payload['description'] = $description; + } + if (! empty($labels)) { + $payload['labels'] = $labels; + } + + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/snapshots', + empty($payload) ? [] : ['json' => $payload] + ); + if (! HetznerAPIClient::hasError($response)) { + $data = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'snapshot' => $data->snapshot, + 'action' => StorageBoxAction::parse($data->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific snapshot by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/get_storage_box_snapshot + * + * @param int $snapshotId + * @return StorageBoxSnapshot|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getSnapshot(int $snapshotId): ?StorageBoxSnapshot + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxSnapshot::parse(json_decode((string) $response->getBody())->snapshot); + } + + return null; + } + + /** + * Updates a snapshot's description or labels. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/update_storage_box_snapshot + * + * @param int $snapshotId + * @param array $data Keys: description (string), labels (object) + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateSnapshot(int $snapshotId, array $data): ?APIResponse + { + $response = $this->httpClient->put('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId, ['json' => $data]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'snapshot' => StorageBoxSnapshot::parse(json_decode((string) $response->getBody())->snapshot), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Deletes a snapshot. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/delete_storage_box_snapshot + * + * @param int $snapshotId + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function deleteSnapshot(int $snapshotId): ?APIResponse + { + $response = $this->httpClient->delete('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + // --- Folders --- + + /** + * Returns the list of folder names at the given path within the Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_box_folders + * + * @param string $path Directory path to list (default: ".", i.e. the root) + * @return string[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listFolders(string $path = '.'): array + { + $query = $path !== '.' ? '?path='.urlencode($path) : ''; + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/folders'.$query); + if (! HetznerAPIClient::hasError($response)) { + return json_decode((string) $response->getBody())->folders; + } + + return []; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxAccessSettings.php b/src/Models/StorageBoxes/StorageBoxAccessSettings.php new file mode 100644 index 00000000..370adaf0 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxAccessSettings.php @@ -0,0 +1,85 @@ +reachable_externally = $reachable_externally; + $this->samba_enabled = $samba_enabled; + $this->ssh_enabled = $ssh_enabled; + $this->webdav_enabled = $webdav_enabled; + $this->zfs_enabled = $zfs_enabled; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'reachable_externally' => $this->reachable_externally, + 'samba_enabled' => $this->samba_enabled, + 'ssh_enabled' => $this->ssh_enabled, + 'webdav_enabled' => $this->webdav_enabled, + 'zfs_enabled' => $this->zfs_enabled, + ]; + } + + /** + * @param object $input + * @return self|null + */ + public static function parse(object $input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->reachable_externally, + $input->samba_enabled, + $input->ssh_enabled, + $input->webdav_enabled, + $input->zfs_enabled + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxAction.php b/src/Models/StorageBoxes/StorageBoxAction.php new file mode 100644 index 00000000..e24529bc --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxAction.php @@ -0,0 +1,46 @@ +storageBoxActions()->getById($this->id); + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return new self( + $input->id, + $input->command, + $input->progress, + $input->status, + $input->started ?? null, + $input->finished ?? null, + $input->resources, + $input->error ?? null + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxActions.php b/src/Models/StorageBoxes/StorageBoxActions.php new file mode 100644 index 00000000..94efb6d6 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxActions.php @@ -0,0 +1,133 @@ +getStorageHttpClient() : null); + parent::__construct($storageClient); + } + + /** + * Returns all Storage Box action objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_boxes_actions + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * Returns a page of Storage Box action objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_boxes_actions + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + $response = $this->httpClient->get('storage_boxes/actions'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + 'actions' => self::parse($resp->actions)->actions, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific Storage Box action by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/get_storage_boxes_action + * + * @param int $actionId + * @return StorageBoxAction|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $actionId): ?StorageBoxAction + { + $response = $this->httpClient->get('storage_boxes/actions/'.$actionId); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxAction::parse(json_decode((string) $response->getBody())->action); + } + + return null; + } + + public function getByName(string $name) + { + throw new \BadMethodCallException('getByName is not possible on StorageBoxActions'); + } + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->actions = array_map(function ($action) { + return StorageBoxAction::parse($action); + }, $input); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'action', 'many' => 'actions']; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxRequestOpts.php b/src/Models/StorageBoxes/StorageBoxRequestOpts.php new file mode 100644 index 00000000..b7c67db2 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxRequestOpts.php @@ -0,0 +1,23 @@ +name = $name; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSnapshot.php b/src/Models/StorageBoxes/StorageBoxSnapshot.php new file mode 100644 index 00000000..b5485144 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSnapshot.php @@ -0,0 +1,79 @@ +id = $data->id; + $this->storage_box = $data->storage_box; + $this->name = $data->name ?? null; + $this->description = $data->description ?? null; + $this->labels = $data->labels ?? null; + $this->stats = $data->stats ?? null; + $this->is_automatic = $data->is_automatic ?? null; + $this->created = $data->created ?? null; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self())->setAdditionalData($input); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php b/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php new file mode 100644 index 00000000..4eaf8416 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php @@ -0,0 +1,76 @@ +max_snapshots = $max_snapshots; + $this->minute = $minute; + $this->hour = $hour; + $this->day_of_week = $day_of_week; + $this->day_of_month = $day_of_month; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'max_snapshots' => $this->max_snapshots, + 'minute' => $this->minute, + 'hour' => $this->hour, + 'day_of_week' => $this->day_of_week, + 'day_of_month' => $this->day_of_month, + ]; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxStats.php b/src/Models/StorageBoxes/StorageBoxStats.php new file mode 100644 index 00000000..a4b21b95 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxStats.php @@ -0,0 +1,50 @@ +size = $size; + $this->size_data = $size_data; + $this->size_snapshots = $size_snapshots; + } + + /** + * @param object $input + * @return self|null + */ + public static function parse(object $input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->size, + $input->size_data, + $input->size_snapshots + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSubaccount.php b/src/Models/StorageBoxes/StorageBoxSubaccount.php new file mode 100644 index 00000000..7e0b4311 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSubaccount.php @@ -0,0 +1,92 @@ +id = $data->id; + $this->storage_box = $data->storage_box; + $this->name = $data->name; + $this->home_directory = $data->home_directory; + $this->access_settings = isset($data->access_settings) ? StorageBoxSubaccountAccessSettings::parse($data->access_settings) : null; + $this->description = $data->description ?? null; + $this->labels = $data->labels ?? null; + $this->username = $data->username ?? null; + $this->server = $data->server ?? null; + $this->created = $data->created ?? null; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self())->setAdditionalData($input); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php b/src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php new file mode 100644 index 00000000..dd094c2b --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php @@ -0,0 +1,52 @@ +reachable_externally = $reachable_externally; + $this->readonly = $readonly; + $this->samba_enabled = $samba_enabled; + $this->ssh_enabled = $ssh_enabled; + $this->webdav_enabled = $webdav_enabled; + } + + public function toArray(): array + { + return [ + 'reachable_externally' => $this->reachable_externally, + 'readonly' => $this->readonly, + 'samba_enabled' => $this->samba_enabled, + 'ssh_enabled' => $this->ssh_enabled, + 'webdav_enabled' => $this->webdav_enabled, + ]; + } + + public static function parse(object $input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->reachable_externally, + $input->readonly, + $input->samba_enabled, + $input->ssh_enabled, + $input->webdav_enabled, + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxes.php b/src/Models/StorageBoxes/StorageBoxes.php new file mode 100644 index 00000000..0e9839e4 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxes.php @@ -0,0 +1,202 @@ +getStorageHttpClient() : null); + parent::__construct($storageClient); + } + + /** + * Returns all existing Storage Box objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_boxes + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * Returns a page of Storage Box objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_boxes + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new StorageBoxRequestOpts(); + } + $response = $this->httpClient->get('storage_boxes'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + $this->_getKeys()['many'] => self::parse($resp->{$this->_getKeys()['many']})->{$this->_getKeys()['many']}, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific Storage Box by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/get_storage_box + * + * @param int $id + * @return StorageBox|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $id): ?StorageBox + { + $response = $this->httpClient->get('storage_boxes/'.$id); + if (! HetznerAPIClient::hasError($response)) { + return StorageBox::parse(json_decode((string) $response->getBody())->storage_box); + } + + return null; + } + + /** + * Returns a specific Storage Box by name. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_boxes + * + * @param string $name + * @return StorageBox|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getByName(string $name): ?StorageBox + { + $boxes = $this->list(new StorageBoxRequestOpts($name)); + + return (count($boxes->storage_boxes) > 0) ? $boxes->storage_boxes[0] : null; + } + + /** + * Creates a new Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/create_storage_box + * + * @param string $name Name of the Storage Box + * @param string $location ID or name of the location + * @param string $storageBoxType ID or name of the Storage Box type + * @param string $password Initial password (must meet the password policy) + * @param array $labels User-defined labels + * @param array $sshKeys SSH public keys to authorize + * @param StorageBoxAccessSettings|null $accessSettings Initial access settings + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function create( + string $name, + string $location, + string $storageBoxType, + #[SensitiveParameter] + string $password, + array $labels = [], + array $sshKeys = [], + ?StorageBoxAccessSettings $accessSettings = null + ): ?APIResponse { + $payload = [ + 'name' => $name, + 'location' => $location, + 'storage_box_type' => $storageBoxType, + 'password' => $password, + ]; + if (! empty($labels)) { + $payload['labels'] = $labels; + } + if (! empty($sshKeys)) { + $payload['ssh_keys'] = $sshKeys; + } + if ($accessSettings !== null) { + $payload['access_settings'] = $accessSettings->toArray(); + } + + $response = $this->httpClient->post('storage_boxes', ['json' => $payload]); + if (! HetznerAPIClient::hasError($response)) { + $data = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'storage_box' => StorageBox::parse($data->storage_box), + 'action' => StorageBoxAction::parse($data->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->storage_boxes = array_map(function ($box) { + return StorageBox::parse($box); + }, $input); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'storage_box', 'many' => 'storage_boxes']; + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php new file mode 100644 index 00000000..b35945ed --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php @@ -0,0 +1,44 @@ +hetznerApi->setStorageHttpClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $tmp = new StorageBoxTypes($this->hetznerApi->getStorageHttpClient()); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_type.json'))); + $this->storageBoxType = $tmp->getById(1); + } + + public function testFields() + { + $this->assertEquals(1, $this->storageBoxType->id); + $this->assertEquals('bx11', $this->storageBoxType->name); + $this->assertEquals('BX11', $this->storageBoxType->description); + $this->assertEquals(10, $this->storageBoxType->snapshot_limit); + $this->assertEquals(10, $this->storageBoxType->automatic_snapshot_limit); + $this->assertEquals(200, $this->storageBoxType->subaccounts_limit); + $this->assertEquals(1073741824, $this->storageBoxType->size); + $this->assertIsArray($this->storageBoxType->prices); + $this->assertInstanceOf(StorageBoxTypePrice::class, $this->storageBoxType->prices[0]); + $this->assertEquals('fsn1', $this->storageBoxType->prices[0]->location); + $this->assertEquals('0.0051', $this->storageBoxType->prices[0]->price_hourly->net); + $this->assertEquals('3.2000', $this->storageBoxType->prices[0]->price_monthly->net); + $this->assertEquals('0.0000', $this->storageBoxType->prices[0]->setup_fee->net); + $this->assertNull($this->storageBoxType->deprecation); + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php new file mode 100644 index 00000000..1c5d35ca --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php @@ -0,0 +1,67 @@ +hetznerApi->setStorageHttpClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $this->storageBoxTypes = new StorageBoxTypes($this->hetznerApi->getStorageHttpClient()); + } + + public function testGetById() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_type.json'))); + $type = $this->storageBoxTypes->getById(1); + + $this->assertEquals(1, $type->id); + $this->assertEquals('bx11', $type->name); + $this->assertEquals(1073741824, $type->size); + $this->assertEquals(200, $type->subaccounts_limit); + + $this->assertLastRequestEquals('GET', '/storage_box_types/1'); + } + + public function testGetByName() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_types.json'))); + $type = $this->storageBoxTypes->getByName('bx11'); + + $this->assertEquals(1, $type->id); + $this->assertEquals('bx11', $type->name); + + $this->assertLastRequestEquals('GET', '/storage_box_types'); + } + + public function testList() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_types.json'))); + $resp = $this->storageBoxTypes->list(); + + $this->assertCount(1, $resp->storage_box_types); + $this->assertEquals('bx11', $resp->storage_box_types[0]->name); + $this->assertNotNull($resp->meta); + + $this->assertLastRequestEquals('GET', '/storage_box_types'); + } + + public function testAll() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_types.json'))); + $types = $this->storageBoxTypes->all(); + + $this->assertCount(1, $types); + $this->assertEquals('bx11', $types[0]->name); + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json new file mode 100644 index 00000000..c51031d4 --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json @@ -0,0 +1,20 @@ +{ + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json new file mode 100644 index 00000000..f20bc705 --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json @@ -0,0 +1,32 @@ +{ + "storage_box_types": [ + { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php new file mode 100644 index 00000000..276e1034 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php @@ -0,0 +1,334 @@ +hetznerApi->setStorageHttpClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $tmp = new StorageBoxes($this->hetznerApi->getStorageHttpClient()); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); + $this->storageBox = $tmp->getById(1); + } + + public function testDelete() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->delete(); + + $this->assertNotNull($resp->action); + $this->assertEquals('change_protection', $resp->action->command); + $this->assertLastRequestEquals('DELETE', '/storage_boxes/1'); + } + + public function testUpdate() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); + $resp = $this->storageBox->update(['name' => 'renamed-box']); + + $this->assertNotNull($resp->storage_box); + $this->assertLastRequestEquals('PUT', '/storage_boxes/1'); + $this->assertLastRequestBodyParametersEqual(['name' => 'renamed-box']); + } + + public function testChangeProtection() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->changeProtection(true); + + $this->assertNotNull($resp->action); + $this->assertEquals('change_protection', $resp->action->command); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/change_protection'); + $this->assertLastRequestBodyParametersEqual(['delete' => true]); + } + + public function testChangeType() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->changeType('bx21'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/change_type'); + $this->assertLastRequestBodyParametersEqual(['storage_box_type' => 'bx21']); + } + + public function testResetPassword() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->resetPassword('NewSecurePass123!'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/reset_password'); + $this->assertLastRequestBodyParametersEqual(['password' => 'NewSecurePass123!']); + } + + public function testUpdateAccessSettings() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $settings = new StorageBoxAccessSettings(false, false, true, false, false); + $resp = $this->storageBox->updateAccessSettings($settings); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/update_access_settings'); + $this->assertLastRequestBodyParametersEqual([ + 'reachable_externally' => false, + 'samba_enabled' => false, + 'ssh_enabled' => true, + 'webdav_enabled' => false, + 'zfs_enabled' => false, + ]); + } + + public function testEnableSnapshotPlan() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->enableSnapshotPlan( + new StorageBoxSnapshotPlanRequest(5, 0, 2) + ); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/enable_snapshot_plan'); + } + + public function testDisableSnapshotPlan() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->disableSnapshotPlan(); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/disable_snapshot_plan'); + } + + public function testRollbackSnapshot() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->rollbackSnapshot(20); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/rollback_snapshot'); + $this->assertLastRequestBodyParametersEqual(['snapshot_id' => 20]); + } + + public function testListActions() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/actions.json'))); + $actions = $this->storageBox->listActions(); + + $this->assertCount(1, $actions); + $this->assertEquals(101, $actions[0]->id); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/actions'); + } + + public function testGetAction() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $action = $this->storageBox->getAction(101); + + $this->assertEquals(101, $action->id); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/actions/101'); + } + + public function testListSubaccounts() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/subaccounts.json'))); + $subaccounts = $this->storageBox->listSubaccounts(); + + $this->assertCount(1, $subaccounts); + $this->assertEquals(10, $subaccounts[0]->id); + $this->assertEquals('my-name', $subaccounts[0]->name); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/subaccounts'); + } + + public function testCreateSubaccount() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/subaccount_create.json'))); + $resp = $this->storageBox->createSubaccount('backups/server01', 'SubPass123!', 'backup-user'); + + $this->assertNotNull($resp->action); + $this->assertEquals('create_subaccount', $resp->action->command); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts'); + $this->assertLastRequestBodyParametersEqual([ + 'home_directory' => 'backups/server01', + 'password' => 'SubPass123!', + 'name' => 'backup-user', + ]); + } + + public function testCreateSubaccountWithAccessSettings() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/subaccount_create.json'))); + $settings = new StorageBoxSubaccountAccessSettings(ssh_enabled: true); + $resp = $this->storageBox->createSubaccount('backups/server01', 'SubPass123!', 'backup-user', $settings); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts'); + $this->assertLastRequestBodyParametersEqual([ + 'home_directory' => 'backups/server01', + 'password' => 'SubPass123!', + 'name' => 'backup-user', + 'access_settings' => [ + 'reachable_externally' => false, + 'readonly' => false, + 'samba_enabled' => false, + 'ssh_enabled' => true, + 'webdav_enabled' => false, + ], + ]); + } + + public function testGetSubaccount() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/subaccount.json'))); + $sub = $this->storageBox->getSubaccount(10); + + $this->assertEquals(10, $sub->id); + $this->assertEquals('my-name', $sub->name); + $this->assertEquals('my_backups/host01.my.company', $sub->home_directory); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/subaccounts/10'); + } + + public function testUpdateSubaccount() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/subaccount.json'))); + $resp = $this->storageBox->updateSubaccount(10, name: 'renamed-user'); + + $this->assertNotNull($resp->subaccount); + $this->assertLastRequestEquals('PUT', '/storage_boxes/1/subaccounts/10'); + $this->assertLastRequestBodyParametersEqual(['name' => 'renamed-user']); + } + + public function testDeleteSubaccount() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->deleteSubaccount(10); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('DELETE', '/storage_boxes/1/subaccounts/10'); + } + + public function testResetSubaccountPassword() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->resetSubaccountPassword(10, 'NewSubPass123!'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts/10/actions/reset_subaccount_password'); + $this->assertLastRequestBodyParametersEqual(['password' => 'NewSubPass123!']); + } + + public function testChangeSubaccountHomeDirectory() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->changeSubaccountHomeDirectory(10, 'backups/server02'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts/10/actions/change_home_directory'); + $this->assertLastRequestBodyParametersEqual(['home_directory' => 'backups/server02']); + } + + public function testUpdateSubaccountAccessSettings() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $settings = new StorageBoxSubaccountAccessSettings(ssh_enabled: true, readonly: true); + $resp = $this->storageBox->updateSubaccountAccessSettings(10, $settings); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts/10/actions/update_access_settings'); + $this->assertLastRequestBodyParametersEqual([ + 'reachable_externally' => false, + 'readonly' => true, + 'samba_enabled' => false, + 'ssh_enabled' => true, + 'webdav_enabled' => false, + ]); + } + + public function testListSnapshots() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/snapshots.json'))); + $snapshots = $this->storageBox->listSnapshots(); + + $this->assertCount(1, $snapshots); + $this->assertEquals(20, $snapshots[0]->id); + $this->assertEquals('before-migration', $snapshots[0]->description); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/snapshots'); + } + + public function testCreateSnapshot() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/snapshot_create.json'))); + $resp = $this->storageBox->createSnapshot('before-migration'); + + $this->assertNotNull($resp->action); + $this->assertEquals('create_snapshot', $resp->action->command); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/snapshots'); + $this->assertLastRequestBodyParametersEqual(['description' => 'before-migration']); + } + + public function testGetSnapshot() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/snapshot.json'))); + $snap = $this->storageBox->getSnapshot(20); + + $this->assertEquals(20, $snap->id); + $this->assertEquals('before-migration', $snap->description); + $this->assertFalse($snap->is_automatic); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/snapshots/20'); + } + + public function testUpdateSnapshot() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/snapshot.json'))); + $resp = $this->storageBox->updateSnapshot(20, ['description' => 'updated-desc']); + + $this->assertNotNull($resp->snapshot); + $this->assertLastRequestEquals('PUT', '/storage_boxes/1/snapshots/20'); + $this->assertLastRequestBodyParametersEqual(['description' => 'updated-desc']); + } + + public function testDeleteSnapshot() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->deleteSnapshot(20); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('DELETE', '/storage_boxes/1/snapshots/20'); + } + + public function testListFolders() + { + $foldersJson = json_encode(['folders' => ['documents', 'backups', 'media']]); + $this->mockHandler->append(new Response(200, [], $foldersJson)); + $folders = $this->storageBox->listFolders(); + + $this->assertCount(3, $folders); + $this->assertEquals('documents', $folders[0]); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/folders'); + } + + public function testListFoldersWithPath() + { + $foldersJson = json_encode(['folders' => ['server01', 'server02']]); + $this->mockHandler->append(new Response(200, [], $foldersJson)); + $folders = $this->storageBox->listFolders('./backups'); + + $this->assertCount(2, $folders); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/folders'); + $this->assertLastRequestQueryParametersContains('path', './backups'); + } +} diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php new file mode 100644 index 00000000..2b34d33e --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php @@ -0,0 +1,101 @@ +hetznerApi->setStorageHttpClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $this->storageBoxes = new StorageBoxes($this->hetznerApi->getStorageHttpClient()); + } + + public function testCreate() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/storage_box_create.json'))); + $resp = $this->storageBoxes->create('new-storage-box', 'fsn1', 'bx11', 'SecurePass123!'); + + $box = $resp->getResponsePart('storage_box'); + $this->assertEquals(2, $box->id); + $this->assertEquals('my-resource', $box->name); + $this->assertEquals('initializing', $box->status); + + $this->assertNotNull($resp->action); + $this->assertEquals('create', $resp->action->command); + + $this->assertLastRequestEquals('POST', '/storage_boxes'); + $this->assertLastRequestBodyParametersEqual([ + 'name' => 'new-storage-box', + 'location' => 'fsn1', + 'storage_box_type' => 'bx11', + 'password' => 'SecurePass123!', + ]); + } + + public function testGetById() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); + $box = $this->storageBoxes->getById(1); + + $this->assertEquals(1, $box->id); + $this->assertEquals('my-storage-box', $box->name); + $this->assertEquals('active', $box->status); + $this->assertEquals('u1337', $box->username); + $this->assertNotNull($box->location); + $this->assertEquals('fsn1', $box->location->name); + $this->assertNotNull($box->storage_box_type); + $this->assertEquals('bx11', $box->storage_box_type->name); + $this->assertInstanceOf(StorageBoxAccessSettings::class, $box->access_settings); + $this->assertTrue($box->access_settings->ssh_enabled); + $this->assertFalse($box->access_settings->samba_enabled); + $this->assertInstanceOf(StorageBoxStats::class, $box->stats); + $this->assertEquals(0, $box->stats->size); + + $this->assertLastRequestEquals('GET', '/storage_boxes/1'); + } + + public function testGetByName() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_boxes.json'))); + $box = $this->storageBoxes->getByName('my-storage-box'); + + $this->assertEquals(1, $box->id); + $this->assertEquals('my-storage-box', $box->name); + + $this->assertLastRequestEquals('GET', '/storage_boxes'); + $this->assertLastRequestQueryParametersContains('name', 'my-storage-box'); + } + + public function testList() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_boxes.json'))); + $resp = $this->storageBoxes->list(); + + $this->assertCount(1, $resp->storage_boxes); + $this->assertEquals(1, $resp->storage_boxes[0]->id); + $this->assertNotNull($resp->meta); + + $this->assertLastRequestEquals('GET', '/storage_boxes'); + } + + public function testAll() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_boxes.json'))); + $boxes = $this->storageBoxes->all(); + + $this->assertCount(1, $boxes); + $this->assertEquals(1, $boxes[0]->id); + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/action.json b/tests/Unit/Models/StorageBoxes/fixtures/action.json new file mode 100644 index 00000000..c2755036 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/action.json @@ -0,0 +1,14 @@ +{ + "action": { + "id": 101, + "command": "change_protection", + "status": "success", + "progress": 100, + "started": "2016-01-30T23:50:00Z", + "finished": "2016-01-30T23:55:00Z", + "resources": [ + {"id": 1, "type": "storage_box"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/actions.json b/tests/Unit/Models/StorageBoxes/fixtures/actions.json new file mode 100644 index 00000000..094591e1 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/actions.json @@ -0,0 +1,26 @@ +{ + "actions": [ + { + "id": 101, + "command": "change_protection", + "status": "success", + "progress": 100, + "started": "2016-01-30T23:50:00Z", + "finished": "2016-01-30T23:55:00Z", + "resources": [ + {"id": 1, "type": "storage_box"} + ], + "error": null + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/snapshot.json b/tests/Unit/Models/StorageBoxes/fixtures/snapshot.json new file mode 100644 index 00000000..45f4b979 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/snapshot.json @@ -0,0 +1,17 @@ +{ + "snapshot": { + "id": 20, + "storage_box": 1, + "name": "2025-02-12T11-35-19", + "description": "before-migration", + "labels": { + "environment": "prod" + }, + "stats": { + "size": 2097152, + "size_filesystem": 1048576 + }, + "is_automatic": false, + "created": "2025-02-12T11:35:19Z" + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json b/tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json new file mode 100644 index 00000000..e22c3f23 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json @@ -0,0 +1,19 @@ +{ + "snapshot": { + "id": 20, + "storage_box": 1 + }, + "action": { + "id": 13, + "command": "create_snapshot", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00Z", + "finished": null, + "resources": [ + {"id": 1, "type": "storage_box"}, + {"id": 20, "type": "storage_box_snapshot"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/snapshots.json b/tests/Unit/Models/StorageBoxes/fixtures/snapshots.json new file mode 100644 index 00000000..8fccb463 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/snapshots.json @@ -0,0 +1,19 @@ +{ + "snapshots": [ + { + "id": 20, + "storage_box": 1, + "name": "2025-02-12T11-35-19", + "description": "before-migration", + "labels": { + "environment": "prod" + }, + "stats": { + "size": 2097152, + "size_filesystem": 1048576 + }, + "is_automatic": false, + "created": "2025-02-12T11:35:19Z" + } + ] +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/storage_box.json b/tests/Unit/Models/StorageBoxes/fixtures/storage_box.json new file mode 100644 index 00000000..644dc997 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/storage_box.json @@ -0,0 +1,58 @@ +{ + "storage_box": { + "id": 1, + "name": "my-storage-box", + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + }, + "location": { + "id": 1, + "name": "fsn1", + "description": "Falkenstein DC Park 1", + "country": "DE", + "city": "Falkenstein", + "latitude": 50.47612, + "longitude": 12.370071, + "network_zone": "eu-central" + }, + "access_settings": { + "reachable_externally": false, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "zfs_enabled": false + }, + "snapshot_plan": null, + "protection": { + "delete": false + }, + "labels": { + "environment": "prod" + }, + "status": "active", + "username": "u1337", + "server": "u1337.your-storagebox.de", + "system": "FSN1-BX355", + "stats": { + "size": 0, + "size_data": 0, + "size_snapshots": 0 + }, + "created": "2016-01-30T23:50:00Z" + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json b/tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json new file mode 100644 index 00000000..4225c353 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json @@ -0,0 +1,68 @@ +{ + "storage_box": { + "id": 2, + "name": "my-resource", + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + }, + "location": { + "id": 1, + "name": "fsn1", + "description": "Falkenstein DC Park 1", + "country": "DE", + "city": "Falkenstein", + "latitude": 50.47612, + "longitude": 12.370071, + "network_zone": "eu-central" + }, + "access_settings": { + "reachable_externally": false, + "samba_enabled": false, + "ssh_enabled": false, + "webdav_enabled": false, + "zfs_enabled": false + }, + "snapshot_plan": null, + "protection": { + "delete": false + }, + "labels": {}, + "status": "initializing", + "username": null, + "server": null, + "system": null, + "stats": { + "size": 0, + "size_data": 0, + "size_snapshots": 0 + }, + "created": "2016-01-30T23:50:00Z" + }, + "action": { + "id": 13, + "command": "create", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00Z", + "finished": null, + "resources": [ + {"id": 2, "type": "storage_box"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json b/tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json new file mode 100644 index 00000000..2d8a54ef --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json @@ -0,0 +1,70 @@ +{ + "storage_boxes": [ + { + "id": 1, + "name": "my-storage-box", + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + }, + "location": { + "id": 1, + "name": "fsn1", + "description": "Falkenstein DC Park 1", + "country": "DE", + "city": "Falkenstein", + "latitude": 50.47612, + "longitude": 12.370071, + "network_zone": "eu-central" + }, + "access_settings": { + "reachable_externally": false, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "zfs_enabled": false + }, + "snapshot_plan": null, + "protection": { + "delete": false + }, + "labels": { + "environment": "prod" + }, + "status": "active", + "username": "u1337", + "server": "u1337.your-storagebox.de", + "system": "FSN1-BX355", + "stats": { + "size": 0, + "size_data": 0, + "size_snapshots": 0 + }, + "created": "2016-01-30T23:50:00Z" + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/subaccount.json b/tests/Unit/Models/StorageBoxes/fixtures/subaccount.json new file mode 100644 index 00000000..2fa56d1a --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/subaccount.json @@ -0,0 +1,22 @@ +{ + "subaccount": { + "id": 10, + "storage_box": 1, + "name": "my-name", + "home_directory": "my_backups/host01.my.company", + "access_settings": { + "reachable_externally": true, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "readonly": false + }, + "description": "host01 backup", + "labels": { + "environment": "prod" + }, + "username": "u1337-sub1", + "server": "u1337-sub1.your-storagebox.de", + "created": "2025-02-22T00:02:00Z" + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json b/tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json new file mode 100644 index 00000000..85b553c1 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json @@ -0,0 +1,19 @@ +{ + "subaccount": { + "id": 10, + "storage_box": 1 + }, + "action": { + "id": 13, + "command": "create_subaccount", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00Z", + "finished": null, + "resources": [ + {"id": 1, "type": "storage_box"}, + {"id": 10, "type": "storage_box_subaccount"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json b/tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json new file mode 100644 index 00000000..867fecc7 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json @@ -0,0 +1,24 @@ +{ + "subaccounts": [ + { + "id": 10, + "storage_box": 1, + "name": "my-name", + "home_directory": "my_backups/host01.my.company", + "access_settings": { + "reachable_externally": true, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "readonly": false + }, + "description": "host01 backup", + "labels": { + "environment": "prod" + }, + "username": "u1337-sub1", + "server": "u1337-sub1.your-storagebox.de", + "created": "2025-02-22T00:02:00Z" + } + ] +} From 7f3c8f428291d2f95a1a53b5976f48ba4ce6570d Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 8 Jun 2026 09:48:16 +0200 Subject: [PATCH 2/9] Code Style fixes --- .../storage_boxes/create_a_storage_box.php | 4 +- .../create_a_storage_box_subaccount.php | 7 ++-- .../get_all_storage_box_types.php | 2 +- src/HetznerAPIClient.php | 6 +-- src/Models/StorageBoxTypes/StorageBoxType.php | 1 - .../StorageBoxTypes/StorageBoxTypePrice.php | 8 ++-- src/Models/StorageBoxes/StorageBox.php | 42 ++++++++----------- .../StorageBoxes/StorageBoxAccessSettings.php | 2 +- .../StorageBoxSnapshotPlanRequest.php | 6 +-- src/Models/StorageBoxes/StorageBoxStats.php | 2 +- .../StorageBoxes/StorageBoxSubaccount.php | 1 - src/Models/StorageBoxes/StorageBoxes.php | 15 ++++--- .../Models/StorageBoxes/StorageBoxTest.php | 2 +- 13 files changed, 44 insertions(+), 54 deletions(-) diff --git a/examples/storage_boxes/create_a_storage_box.php b/examples/storage_boxes/create_a_storage_box.php index 278ff3fd..c2ddf823 100644 --- a/examples/storage_boxes/create_a_storage_box.php +++ b/examples/storage_boxes/create_a_storage_box.php @@ -20,5 +20,5 @@ $response->getResponsePart('action')->waitUntilCompleted(); $box = $response->getResponsePart('storage_box')->reload(); -echo "Name: {$box->name}" . PHP_EOL; -echo "ID: {$box->id}" . PHP_EOL; +echo "Name: {$box->name}".PHP_EOL; +echo "ID: {$box->id}".PHP_EOL; diff --git a/examples/storage_boxes/create_a_storage_box_subaccount.php b/examples/storage_boxes/create_a_storage_box_subaccount.php index 92c02b9e..9109c9db 100644 --- a/examples/storage_boxes/create_a_storage_box_subaccount.php +++ b/examples/storage_boxes/create_a_storage_box_subaccount.php @@ -22,8 +22,7 @@ $response->getResponsePart('action')->waitUntilCompleted(); $account = $response->getResponsePart('subaccount')->reload(); -echo "Name: {$account->name}" . PHP_EOL; -echo "ID: {$account->id}" . PHP_EOL; -echo "HomeDir: {$account->home_directory}" . PHP_EOL; - +echo "Name: {$account->name}".PHP_EOL; +echo "ID: {$account->id}".PHP_EOL; +echo "HomeDir: {$account->home_directory}".PHP_EOL; diff --git a/examples/storage_boxes/get_all_storage_box_types.php b/examples/storage_boxes/get_all_storage_box_types.php index 8895064a..0413721a 100644 --- a/examples/storage_boxes/get_all_storage_box_types.php +++ b/examples/storage_boxes/get_all_storage_box_types.php @@ -3,5 +3,5 @@ require_once __DIR__.'/../bootstrap.php'; foreach ($hetznerClient->storageBoxTypes()->all() as $type) { - echo "Name: {$type->name} - ID: {$type->id}" . PHP_EOL; + echo "Name: {$type->name} - ID: {$type->id}".PHP_EOL; } diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index 3947847e..0818be19 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -6,9 +6,6 @@ use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\Models\Actions\Actions; use LKDev\HetznerCloud\Models\Certificates\Certificates; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxActions; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxes; -use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxTypes; use LKDev\HetznerCloud\Models\Datacenters\Datacenters; use LKDev\HetznerCloud\Models\Firewalls\Firewalls; use LKDev\HetznerCloud\Models\FloatingIps\FloatingIps; @@ -23,6 +20,9 @@ use LKDev\HetznerCloud\Models\Servers\Servers; use LKDev\HetznerCloud\Models\Servers\Types\ServerTypes; use LKDev\HetznerCloud\Models\SSHKeys\SSHKeys; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxActions; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxes; +use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxTypes; use LKDev\HetznerCloud\Models\Volumes\Volumes; use LKDev\HetznerCloud\Models\Zones\Zones; use Psr\Http\Message\ResponseInterface; diff --git a/src/Models/StorageBoxTypes/StorageBoxType.php b/src/Models/StorageBoxTypes/StorageBoxType.php index 795b7a43..c2f621d9 100644 --- a/src/Models/StorageBoxTypes/StorageBoxType.php +++ b/src/Models/StorageBoxTypes/StorageBoxType.php @@ -5,7 +5,6 @@ use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\HetznerAPIClient; use LKDev\HetznerCloud\Models\Model; -use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxTypePrice; class StorageBoxType extends Model { diff --git a/src/Models/StorageBoxTypes/StorageBoxTypePrice.php b/src/Models/StorageBoxTypes/StorageBoxTypePrice.php index 8a7c23e9..9737ae29 100644 --- a/src/Models/StorageBoxTypes/StorageBoxTypePrice.php +++ b/src/Models/StorageBoxTypes/StorageBoxTypePrice.php @@ -27,10 +27,10 @@ class StorageBoxTypePrice public ?Price $setup_fee; /** - * @param string $location - * @param Price|null $priceHourly - * @param Price|null $priceMonthly - * @param Price|null $setupFee + * @param string $location + * @param Price|null $priceHourly + * @param Price|null $priceMonthly + * @param Price|null $setupFee */ public function __construct(string $location, ?Price $priceHourly, ?Price $priceMonthly, ?Price $setupFee = null) { diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php index f6a995df..2437a9d7 100644 --- a/src/Models/StorageBoxes/StorageBox.php +++ b/src/Models/StorageBoxes/StorageBox.php @@ -9,10 +9,6 @@ use LKDev\HetznerCloud\Models\Locations\Location; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\Models\Protection; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxAccessSettings; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxSnapshotPlanRequest; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxSubaccountAccessSettings; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxStats; use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxType; use SensitiveParameter; @@ -89,7 +85,7 @@ class StorageBox extends Model implements Resource public ?string $created; /** - * @param int|null $id + * @param int|null $id * @param GuzzleClient|null $httpClient */ public function __construct(?int $id = null, ?GuzzleClient $httpClient = null) @@ -256,8 +252,7 @@ public function changeType(string $storageBoxType): ?APIResponse public function resetPassword( #[SensitiveParameter] string $password - ): ?APIResponse - { + ): ?APIResponse { $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/reset_password', [ 'json' => ['password' => $password], ]); @@ -435,12 +430,12 @@ public function listSubaccounts(): array * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/create_storage_box_subaccount * - * @param string $homeDirectory Home directory of the subaccount (e.g. "backups/server01") - * @param string $password Password (must meet the password policy) - * @param string|null $name Display name - * @param StorageBoxSubaccountAccessSettings|null $accessSettings Access settings for the subaccount - * @param string|null $description Optional description - * @param array $labels User-defined labels + * @param string $homeDirectory Home directory of the subaccount (e.g. "backups/server01") + * @param string $password Password (must meet the password policy) + * @param string|null $name Display name + * @param StorageBoxSubaccountAccessSettings|null $accessSettings Access settings for the subaccount + * @param string|null $description Optional description + * @param array $labels User-defined labels * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -509,10 +504,10 @@ public function getSubaccount(int $subaccountId): ?StorageBoxSubaccount * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/update_storage_box_subaccount * - * @param int $subaccountId - * @param string|null $name Display name + * @param int $subaccountId + * @param string|null $name Display name * @param string|null $description Optional description - * @param array|null $labels User-defined labels + * @param array|null $labels User-defined labels * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -571,7 +566,7 @@ public function deleteSubaccount(int $subaccountId): ?APIResponse * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/reset_storage_box_subaccount_password * - * @param int $subaccountId + * @param int $subaccountId * @param string $password New password (must meet the password policy) * @return APIResponse|null * @@ -581,8 +576,7 @@ public function resetSubaccountPassword( int $subaccountId, #[SensitiveParameter] string $password - ): ?APIResponse - { + ): ?APIResponse { $response = $this->httpClient->post( 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/reset_subaccount_password', ['json' => ['password' => $password]] @@ -601,7 +595,7 @@ public function resetSubaccountPassword( * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/change_storage_box_subaccount_home_directory * - * @param int $subaccountId + * @param int $subaccountId * @param string $homeDirectory New home directory path * @return APIResponse|null * @@ -627,8 +621,8 @@ public function changeSubaccountHomeDirectory(int $subaccountId, string $homeDir * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/update_storage_box_subaccount_access_settings * - * @param int $subaccountId - * @param StorageBoxSubaccountAccessSettings $settings + * @param int $subaccountId + * @param StorageBoxSubaccountAccessSettings $settings * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -677,7 +671,7 @@ public function listSnapshots(): array * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/create_storage_box_snapshot * * @param string|null $description Optional description - * @param array $labels User-defined labels + * @param array $labels User-defined labels * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException @@ -733,7 +727,7 @@ public function getSnapshot(int $snapshotId): ?StorageBoxSnapshot * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/update_storage_box_snapshot * - * @param int $snapshotId + * @param int $snapshotId * @param array $data Keys: description (string), labels (object) * @return APIResponse|null * diff --git a/src/Models/StorageBoxes/StorageBoxAccessSettings.php b/src/Models/StorageBoxes/StorageBoxAccessSettings.php index 370adaf0..3ea9a25e 100644 --- a/src/Models/StorageBoxes/StorageBoxAccessSettings.php +++ b/src/Models/StorageBoxes/StorageBoxAccessSettings.php @@ -65,7 +65,7 @@ public function toArray(): array } /** - * @param object $input + * @param object $input * @return self|null */ public static function parse(object $input): ?self diff --git a/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php b/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php index 4eaf8416..d9234f98 100644 --- a/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php +++ b/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php @@ -40,9 +40,9 @@ class StorageBoxSnapshotPlanRequest public ?int $day_of_month; /** - * @param int $max_snapshots - * @param int $minute - * @param int $hour + * @param int $max_snapshots + * @param int $minute + * @param int $hour * @param int|null $day_of_week * @param int|null $day_of_month */ diff --git a/src/Models/StorageBoxes/StorageBoxStats.php b/src/Models/StorageBoxes/StorageBoxStats.php index a4b21b95..7fcb7671 100644 --- a/src/Models/StorageBoxes/StorageBoxStats.php +++ b/src/Models/StorageBoxes/StorageBoxStats.php @@ -32,7 +32,7 @@ public function __construct(int $size, int $size_data, int $size_snapshots) } /** - * @param object $input + * @param object $input * @return self|null */ public static function parse(object $input): ?self diff --git a/src/Models/StorageBoxes/StorageBoxSubaccount.php b/src/Models/StorageBoxes/StorageBoxSubaccount.php index 7e0b4311..6688442b 100644 --- a/src/Models/StorageBoxes/StorageBoxSubaccount.php +++ b/src/Models/StorageBoxes/StorageBoxSubaccount.php @@ -3,7 +3,6 @@ namespace LKDev\HetznerCloud\Models\StorageBoxes; use LKDev\HetznerCloud\Models\Model; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxSubaccountAccessSettings; class StorageBoxSubaccount extends Model { diff --git a/src/Models/StorageBoxes/StorageBoxes.php b/src/Models/StorageBoxes/StorageBoxes.php index 0e9839e4..7d03c783 100644 --- a/src/Models/StorageBoxes/StorageBoxes.php +++ b/src/Models/StorageBoxes/StorageBoxes.php @@ -8,7 +8,6 @@ use LKDev\HetznerCloud\Models\Contracts\Resources; use LKDev\HetznerCloud\Models\Meta; use LKDev\HetznerCloud\Models\Model; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxAccessSettings; use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; use SensitiveParameter; @@ -120,13 +119,13 @@ public function getByName(string $name): ?StorageBox * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/create_storage_box * - * @param string $name Name of the Storage Box - * @param string $location ID or name of the location - * @param string $storageBoxType ID or name of the Storage Box type - * @param string $password Initial password (must meet the password policy) - * @param array $labels User-defined labels - * @param array $sshKeys SSH public keys to authorize - * @param StorageBoxAccessSettings|null $accessSettings Initial access settings + * @param string $name Name of the Storage Box + * @param string $location ID or name of the location + * @param string $storageBoxType ID or name of the Storage Box type + * @param string $password Initial password (must meet the password policy) + * @param array $labels User-defined labels + * @param array $sshKeys SSH public keys to authorize + * @param StorageBoxAccessSettings|null $accessSettings Initial access settings * @return APIResponse|null * * @throws \LKDev\HetznerCloud\APIException diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php index 276e1034..eada17a6 100644 --- a/tests/Unit/Models/StorageBoxes/StorageBoxTest.php +++ b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php @@ -6,9 +6,9 @@ use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\Models\StorageBoxes\StorageBox; use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxAccessSettings; -use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxSubaccountAccessSettings; use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxes; use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxSnapshotPlanRequest; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxSubaccountAccessSettings; use LKDev\Tests\TestCase; class StorageBoxTest extends TestCase From b23a4bbd37067e11ec614875e753336a8d7955cd Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 8 Jun 2026 09:48:58 +0200 Subject: [PATCH 3/9] Code Style fixes --- examples/storage_boxes/create_a_storage_box_subaccount.php | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/storage_boxes/create_a_storage_box_subaccount.php b/examples/storage_boxes/create_a_storage_box_subaccount.php index 9109c9db..0513dfb1 100644 --- a/examples/storage_boxes/create_a_storage_box_subaccount.php +++ b/examples/storage_boxes/create_a_storage_box_subaccount.php @@ -25,4 +25,3 @@ echo "Name: {$account->name}".PHP_EOL; echo "ID: {$account->id}".PHP_EOL; echo "HomeDir: {$account->home_directory}".PHP_EOL; - From f6eb4812c6105ede577c21ea7d1176e145322e16 Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 15 Jun 2026 11:22:23 +0200 Subject: [PATCH 4/9] Changed URL's to documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1422014..385ced72 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Total Downloads](https://poser.pugx.org/lkdevelopment/hetzner-cloud-php-sdk/downloads)](https://packagist.org/packages/lkdevelopment/hetzner-cloud-php-sdk) [![Actions Status](https://github.com/lkdevelopment/hetzner-cloud-php-sdk/workflows/CI/badge.svg)](https://github.com/lkdevelopment/hetzner-cloud-php-sdk/actions) # Hetzner Cloud PHP SDK -A PHP SDK for both Hetzner Cloud API: https://docs.hetzner.cloud/ and Storage Box API: https://docs.hetzner.cloud/ +A PHP SDK for both [Hetzner Cloud API](https://docs.hetzner.cloud/reference/cloud) and [Hetzner API](https://docs.hetzner.cloud/reference/hetzner) ## Installation You can install the package via composer: From d9eb4814fb23e3216f840f231781bb5f859ffa51 Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 15 Jun 2026 11:23:06 +0200 Subject: [PATCH 5/9] Renamed client name to be less storage box specific --- src/HetznerAPIClient.php | 20 +++++++++---------- src/Models/StorageBoxTypes/StorageBoxType.php | 2 +- .../StorageBoxTypes/StorageBoxTypes.php | 2 +- src/Models/StorageBoxes/StorageBox.php | 2 +- src/Models/StorageBoxes/StorageBoxActions.php | 2 +- src/Models/StorageBoxes/StorageBoxes.php | 2 +- .../StorageBoxTypes/StorageBoxTypeTest.php | 4 ++-- .../StorageBoxTypes/StorageBoxTypesTest.php | 4 ++-- .../Models/StorageBoxes/StorageBoxTest.php | 4 ++-- .../Models/StorageBoxes/StorageBoxesTest.php | 4 ++-- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index 0818be19..bcc782b6 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -69,7 +69,7 @@ class HetznerAPIClient /** * @var \LKDev\HetznerCloud\Clients\GuzzleClient|null */ - protected ?GuzzleClient $storageHttpClient = null; + protected ?GuzzleClient $apiHetznerComClient = null; /** * @param string $apiToken @@ -152,21 +152,21 @@ public function setHttpClient(GuzzleClient $client): self /** * @return GuzzleClient */ - public function getStorageHttpClient(): GuzzleClient + public function getApiHetznerComClient(): GuzzleClient { - if ($this->storageHttpClient === null) { - $this->storageHttpClient = new GuzzleClient($this, ['base_uri' => 'https://api.hetzner.com/v1/']); + if ($this->apiHetznerComClient === null) { + $this->apiHetznerComClient = new GuzzleClient($this, ['base_uri' => 'https://api.hetzner.com/v1/']); } - return $this->storageHttpClient; + return $this->apiHetznerComClient; } /** * @return $this */ - public function setStorageHttpClient(GuzzleClient $client): self + public function setApiHetznerComClient(GuzzleClient $client): self { - $this->storageHttpClient = $client; + $this->apiHetznerComClient = $client; return $this; } @@ -368,7 +368,7 @@ public function zones() */ public function storageBoxes(): StorageBoxes { - return new StorageBoxes($this->getStorageHttpClient()); + return new StorageBoxes($this->getApiHetznerComClient()); } /** @@ -376,7 +376,7 @@ public function storageBoxes(): StorageBoxes */ public function storageBoxTypes(): StorageBoxTypes { - return new StorageBoxTypes($this->getStorageHttpClient()); + return new StorageBoxTypes($this->getApiHetznerComClient()); } /** @@ -384,7 +384,7 @@ public function storageBoxTypes(): StorageBoxTypes */ public function storageBoxActions(): StorageBoxActions { - return new StorageBoxActions($this->getStorageHttpClient()); + return new StorageBoxActions($this->getApiHetznerComClient()); } /** diff --git a/src/Models/StorageBoxTypes/StorageBoxType.php b/src/Models/StorageBoxTypes/StorageBoxType.php index c2f621d9..054c50b1 100644 --- a/src/Models/StorageBoxTypes/StorageBoxType.php +++ b/src/Models/StorageBoxTypes/StorageBoxType.php @@ -58,7 +58,7 @@ class StorageBoxType extends Model */ public function __construct(?GuzzleClient $httpClient = null) { - $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getStorageHttpClient() : null); + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getApiHetznerComClient() : null); parent::__construct($storageClient); } diff --git a/src/Models/StorageBoxTypes/StorageBoxTypes.php b/src/Models/StorageBoxTypes/StorageBoxTypes.php index 96a1f11c..a84d6eae 100644 --- a/src/Models/StorageBoxTypes/StorageBoxTypes.php +++ b/src/Models/StorageBoxTypes/StorageBoxTypes.php @@ -25,7 +25,7 @@ class StorageBoxTypes extends Model implements Resources */ public function __construct(?GuzzleClient $httpClient = null) { - $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getStorageHttpClient() : null); + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getApiHetznerComClient() : null); parent::__construct($storageClient); } diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php index 2437a9d7..67e677c4 100644 --- a/src/Models/StorageBoxes/StorageBox.php +++ b/src/Models/StorageBoxes/StorageBox.php @@ -91,7 +91,7 @@ class StorageBox extends Model implements Resource public function __construct(?int $id = null, ?GuzzleClient $httpClient = null) { $this->id = $id; - $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getStorageHttpClient() : null); + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getApiHetznerComClient() : null); parent::__construct($storageClient); } diff --git a/src/Models/StorageBoxes/StorageBoxActions.php b/src/Models/StorageBoxes/StorageBoxActions.php index 94efb6d6..9d4582b6 100644 --- a/src/Models/StorageBoxes/StorageBoxActions.php +++ b/src/Models/StorageBoxes/StorageBoxActions.php @@ -25,7 +25,7 @@ class StorageBoxActions extends Model implements Resources */ public function __construct(?GuzzleClient $httpClient = null) { - $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getStorageHttpClient() : null); + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getApiHetznerComClient() : null); parent::__construct($storageClient); } diff --git a/src/Models/StorageBoxes/StorageBoxes.php b/src/Models/StorageBoxes/StorageBoxes.php index 7d03c783..02aa1a72 100644 --- a/src/Models/StorageBoxes/StorageBoxes.php +++ b/src/Models/StorageBoxes/StorageBoxes.php @@ -26,7 +26,7 @@ class StorageBoxes extends Model implements Resources */ public function __construct(?GuzzleClient $httpClient = null) { - $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getStorageHttpClient() : null); + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getApiHetznerComClient() : null); parent::__construct($storageClient); } diff --git a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php index b35945ed..eb4faecb 100644 --- a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php +++ b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php @@ -16,10 +16,10 @@ class StorageBoxTypeTest extends TestCase public function setUp(): void { parent::setUp(); - $this->hetznerApi->setStorageHttpClient( + $this->hetznerApi->setApiHetznerComClient( new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) ); - $tmp = new StorageBoxTypes($this->hetznerApi->getStorageHttpClient()); + $tmp = new StorageBoxTypes($this->hetznerApi->getApiHetznerComClient()); $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_type.json'))); $this->storageBoxType = $tmp->getById(1); } diff --git a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php index 1c5d35ca..cd9c9c79 100644 --- a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php +++ b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php @@ -14,10 +14,10 @@ class StorageBoxTypesTest extends TestCase public function setUp(): void { parent::setUp(); - $this->hetznerApi->setStorageHttpClient( + $this->hetznerApi->setApiHetznerComClient( new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) ); - $this->storageBoxTypes = new StorageBoxTypes($this->hetznerApi->getStorageHttpClient()); + $this->storageBoxTypes = new StorageBoxTypes($this->hetznerApi->getApiHetznerComClient()); } public function testGetById() diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php index eada17a6..62e07227 100644 --- a/tests/Unit/Models/StorageBoxes/StorageBoxTest.php +++ b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php @@ -18,10 +18,10 @@ class StorageBoxTest extends TestCase public function setUp(): void { parent::setUp(); - $this->hetznerApi->setStorageHttpClient( + $this->hetznerApi->setApiHetznerComClient( new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) ); - $tmp = new StorageBoxes($this->hetznerApi->getStorageHttpClient()); + $tmp = new StorageBoxes($this->hetznerApi->getApiHetznerComClient()); $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); $this->storageBox = $tmp->getById(1); } diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php index 2b34d33e..b2d7cf67 100644 --- a/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php +++ b/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php @@ -16,10 +16,10 @@ class StorageBoxesTest extends TestCase public function setUp(): void { parent::setUp(); - $this->hetznerApi->setStorageHttpClient( + $this->hetznerApi->setApiHetznerComClient( new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) ); - $this->storageBoxes = new StorageBoxes($this->hetznerApi->getStorageHttpClient()); + $this->storageBoxes = new StorageBoxes($this->hetznerApi->getApiHetznerComClient()); } public function testCreate() From 1f0c43a1c7b7550d9f6355e4a2d43fd35ca60dc6 Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 15 Jun 2026 11:37:01 +0200 Subject: [PATCH 6/9] Refactored StorageBoxAction to use the base Action class --- src/Models/StorageBoxes/StorageBox.php | 41 +++++++++-------- src/Models/StorageBoxes/StorageBoxAction.php | 46 ------------------- src/Models/StorageBoxes/StorageBoxActions.php | 13 +++--- src/Models/StorageBoxes/StorageBoxes.php | 3 +- 4 files changed, 30 insertions(+), 73 deletions(-) delete mode 100644 src/Models/StorageBoxes/StorageBoxAction.php diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php index 67e677c4..cb6f1046 100644 --- a/src/Models/StorageBoxes/StorageBox.php +++ b/src/Models/StorageBoxes/StorageBox.php @@ -9,6 +9,7 @@ use LKDev\HetznerCloud\Models\Locations\Location; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\Models\Protection; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxType; use SensitiveParameter; @@ -158,7 +159,7 @@ public function delete(): ?APIResponse $response = $this->httpClient->delete('storage_boxes/'.$this->id); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -206,7 +207,7 @@ public function changeProtection(bool $delete): ?APIResponse ]); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -231,7 +232,7 @@ public function changeType(string $storageBoxType): ?APIResponse ]); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -258,7 +259,7 @@ public function resetPassword( ]); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -282,7 +283,7 @@ public function updateAccessSettings(StorageBoxAccessSettings $settings): ?APIRe ]); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -306,7 +307,7 @@ public function enableSnapshotPlan(StorageBoxSnapshotPlanRequest $schedule): ?AP ]); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -327,7 +328,7 @@ public function disableSnapshotPlan(): ?APIResponse $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/disable_snapshot_plan'); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -351,7 +352,7 @@ public function rollbackSnapshot(int $snapshotId): ?APIResponse ]); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -365,7 +366,7 @@ public function rollbackSnapshot(int $snapshotId): ?APIResponse * * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_box_actions * - * @return StorageBoxAction[] + * @return Action[] * * @throws \LKDev\HetznerCloud\APIException */ @@ -374,7 +375,7 @@ public function listActions(): array $response = $this->httpClient->get('storage_boxes/'.$this->id.'/actions'); if (! HetznerAPIClient::hasError($response)) { return array_map(function ($action) { - return StorageBoxAction::parse($action); + return Action::parse($action); }, json_decode((string) $response->getBody())->actions); } @@ -387,15 +388,15 @@ public function listActions(): array * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/get_storage_box_action * * @param int $actionId - * @return StorageBoxAction|null + * @return Action|null * * @throws \LKDev\HetznerCloud\APIException */ - public function getAction(int $actionId): ?StorageBoxAction + public function getAction(int $actionId): ?Action { $response = $this->httpClient->get('storage_boxes/'.$this->id.'/actions/'.$actionId); if (! HetznerAPIClient::hasError($response)) { - return StorageBoxAction::parse(json_decode((string) $response->getBody())->action); + return Action::parse(json_decode((string) $response->getBody())->action); } return null; @@ -472,7 +473,7 @@ public function createSubaccount( return APIResponse::create([ 'subaccount' => $data->subaccount, - 'action' => StorageBoxAction::parse($data->action), + 'action' => Action::parse($data->action), ], $response->getHeaders()); } @@ -554,7 +555,7 @@ public function deleteSubaccount(int $subaccountId): ?APIResponse $response = $this->httpClient->delete('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -583,7 +584,7 @@ public function resetSubaccountPassword( ); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -609,7 +610,7 @@ public function changeSubaccountHomeDirectory(int $subaccountId, string $homeDir ); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -635,7 +636,7 @@ public function updateSubaccountAccessSettings(int $subaccountId, StorageBoxSuba ); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } @@ -695,7 +696,7 @@ public function createSnapshot(?string $description = null, array $labels = []): return APIResponse::create([ 'snapshot' => $data->snapshot, - 'action' => StorageBoxAction::parse($data->action), + 'action' => Action::parse($data->action), ], $response->getHeaders()); } @@ -760,7 +761,7 @@ public function deleteSnapshot(int $snapshotId): ?APIResponse $response = $this->httpClient->delete('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId); if (! HetznerAPIClient::hasError($response)) { return APIResponse::create([ - 'action' => StorageBoxAction::parse(json_decode((string) $response->getBody())->action), + 'action' => Action::parse(json_decode((string) $response->getBody())->action), ], $response->getHeaders()); } diff --git a/src/Models/StorageBoxes/StorageBoxAction.php b/src/Models/StorageBoxes/StorageBoxAction.php deleted file mode 100644 index e24529bc..00000000 --- a/src/Models/StorageBoxes/StorageBoxAction.php +++ /dev/null @@ -1,46 +0,0 @@ -storageBoxActions()->getById($this->id); - } - - /** - * @param $input - * @return static|null - */ - public static function parse($input) - { - if ($input == null) { - return; - } - - return new self( - $input->id, - $input->command, - $input->progress, - $input->status, - $input->started ?? null, - $input->finished ?? null, - $input->resources, - $input->error ?? null - ); - } -} diff --git a/src/Models/StorageBoxes/StorageBoxActions.php b/src/Models/StorageBoxes/StorageBoxActions.php index 9d4582b6..ecc062c7 100644 --- a/src/Models/StorageBoxes/StorageBoxActions.php +++ b/src/Models/StorageBoxes/StorageBoxActions.php @@ -7,11 +7,12 @@ use LKDev\HetznerCloud\HetznerAPIClient; use LKDev\HetznerCloud\Models\Contracts\Resources; use LKDev\HetznerCloud\Models\Meta; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; -class StorageBoxActions extends Model implements Resources +class Actions extends Model implements Resources { use GetFunctionTrait; @@ -82,15 +83,15 @@ public function list(?RequestOpts $requestOpts = null): ?APIResponse * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/get_storage_boxes_action * * @param int $actionId - * @return StorageBoxAction|null + * @return Action|null * * @throws \LKDev\HetznerCloud\APIException */ - public function getById(int $actionId): ?StorageBoxAction + public function getById(int $actionId): ?Action { $response = $this->httpClient->get('storage_boxes/actions/'.$actionId); if (! HetznerAPIClient::hasError($response)) { - return StorageBoxAction::parse(json_decode((string) $response->getBody())->action); + return Action::parse(json_decode((string) $response->getBody())->action); } return null; @@ -98,7 +99,7 @@ public function getById(int $actionId): ?StorageBoxAction public function getByName(string $name) { - throw new \BadMethodCallException('getByName is not possible on StorageBoxActions'); + throw new \BadMethodCallException('getByName is not possible on Actions'); } /** @@ -108,7 +109,7 @@ public function getByName(string $name) public function setAdditionalData($input) { $this->actions = array_map(function ($action) { - return StorageBoxAction::parse($action); + return Action::parse($action); }, $input); return $this; diff --git a/src/Models/StorageBoxes/StorageBoxes.php b/src/Models/StorageBoxes/StorageBoxes.php index 02aa1a72..3efcd923 100644 --- a/src/Models/StorageBoxes/StorageBoxes.php +++ b/src/Models/StorageBoxes/StorageBoxes.php @@ -7,6 +7,7 @@ use LKDev\HetznerCloud\HetznerAPIClient; use LKDev\HetznerCloud\Models\Contracts\Resources; use LKDev\HetznerCloud\Models\Meta; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; @@ -162,7 +163,7 @@ public function create( return APIResponse::create([ 'storage_box' => StorageBox::parse($data->storage_box), - 'action' => StorageBoxAction::parse($data->action), + 'action' => Action::parse($data->action), ], $response->getHeaders()); } From fb0cb0a87b4199705da93b05b159509122f816d1 Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 15 Jun 2026 11:40:35 +0200 Subject: [PATCH 7/9] Style-ci fixes --- src/Models/StorageBoxes/StorageBox.php | 2 +- src/Models/StorageBoxes/StorageBoxActions.php | 2 +- src/Models/StorageBoxes/StorageBoxes.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php index cb6f1046..c32f84a4 100644 --- a/src/Models/StorageBoxes/StorageBox.php +++ b/src/Models/StorageBoxes/StorageBox.php @@ -8,8 +8,8 @@ use LKDev\HetznerCloud\Models\Contracts\Resource; use LKDev\HetznerCloud\Models\Locations\Location; use LKDev\HetznerCloud\Models\Model; -use LKDev\HetznerCloud\Models\Protection; use LKDev\HetznerCloud\Models\Actions\Action; +use LKDev\HetznerCloud\Models\Protection; use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxType; use SensitiveParameter; diff --git a/src/Models/StorageBoxes/StorageBoxActions.php b/src/Models/StorageBoxes/StorageBoxActions.php index ecc062c7..2201264e 100644 --- a/src/Models/StorageBoxes/StorageBoxActions.php +++ b/src/Models/StorageBoxes/StorageBoxActions.php @@ -5,9 +5,9 @@ use LKDev\HetznerCloud\APIResponse; use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\HetznerAPIClient; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Contracts\Resources; use LKDev\HetznerCloud\Models\Meta; -use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; diff --git a/src/Models/StorageBoxes/StorageBoxes.php b/src/Models/StorageBoxes/StorageBoxes.php index 3efcd923..286621d9 100644 --- a/src/Models/StorageBoxes/StorageBoxes.php +++ b/src/Models/StorageBoxes/StorageBoxes.php @@ -5,9 +5,9 @@ use LKDev\HetznerCloud\APIResponse; use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\HetznerAPIClient; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Contracts\Resources; use LKDev\HetznerCloud\Models\Meta; -use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Model; use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; From 4074e4b31e361b46ebc9c725b6ba4aee5c69884e Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 15 Jun 2026 11:42:34 +0200 Subject: [PATCH 8/9] Style-ci fixes, missed one --- src/Models/StorageBoxes/StorageBox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php index c32f84a4..00529277 100644 --- a/src/Models/StorageBoxes/StorageBox.php +++ b/src/Models/StorageBoxes/StorageBox.php @@ -5,10 +5,10 @@ use LKDev\HetznerCloud\APIResponse; use LKDev\HetznerCloud\Clients\GuzzleClient; use LKDev\HetznerCloud\HetznerAPIClient; +use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Contracts\Resource; use LKDev\HetznerCloud\Models\Locations\Location; use LKDev\HetznerCloud\Models\Model; -use LKDev\HetznerCloud\Models\Actions\Action; use LKDev\HetznerCloud\Models\Protection; use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxType; use SensitiveParameter; From 2392e31fec5de6283680421d98d9a8697d62d639 Mon Sep 17 00:00:00 2001 From: Ulrik Nielsen Date: Mon, 15 Jun 2026 11:50:53 +0200 Subject: [PATCH 9/9] Fixed naming issue --- src/Models/StorageBoxes/StorageBoxActions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/StorageBoxes/StorageBoxActions.php b/src/Models/StorageBoxes/StorageBoxActions.php index 2201264e..b0e074eb 100644 --- a/src/Models/StorageBoxes/StorageBoxActions.php +++ b/src/Models/StorageBoxes/StorageBoxActions.php @@ -12,7 +12,7 @@ use LKDev\HetznerCloud\RequestOpts; use LKDev\HetznerCloud\Traits\GetFunctionTrait; -class Actions extends Model implements Resources +class StorageBoxActions extends Model implements Resources { use GetFunctionTrait; @@ -99,7 +99,7 @@ public function getById(int $actionId): ?Action public function getByName(string $name) { - throw new \BadMethodCallException('getByName is not possible on Actions'); + throw new \BadMethodCallException('getByName is not possible on StorageBoxActions'); } /**