Backup Storage

9 Aufrufe Markdown

The nine endpoints that set up, test and remove the storage targets backups go to.

Overview

A storage target defines where backups get sent. These nine endpoints set targets up, test them and take them away. When backups run is a separate matter.

Providers fall into two groups. Server-type ones are set up with an address and a password. Cloud-type ones wait for the user to grant access in a browser. That path has three steps: get the consent address, send the user, then create the target with the returned token.

A target is actually connected to before it is saved. A wrong password surfaces the moment you write the target, not on the night of the backup.

Reference

Listing the Providers

get/api/v1/admin/settings/backup/storage/providers
Settings/GetBackupStorageProviders admin

Returns the storage providers installed for backups to be sent to.

Response fields data[] — 5
keystringThe provider key. This is what creating a target takes.
namestringThe provider name.
descriptionstringWhat it does.
oauthboolWhether it needs the user to grant access. When true, writing settings is not enough on its own.
fieldsarrayThe definition of the settings the provider wants.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage/providers' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/providers', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/providers');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A server-type provider is set up with settings; a cloud one needs the USER TO GRANT ACCESS.
$providers = Api::Settings()->GetBackupStorageProviders()['data'];
$simple    = array_filter($providers, fn ($p) => ! $p['oauth']);

Testing a Connection

post/api/v1/admin/settings/backup/storage/validate
Settings/ValidateBackupStorage admin

Runs a configuration through a connection test without saving it.

Body 3
typestringreqThe provider to test. Required unless you give a target id.
idintThe id of an existing target. Its settings get used.
configobjectThe provider settings. Secret values stay protected behind the mask.
Response fields data — 2
validatedboolWhether the connection worked.
messagestringWhat the result means.
Errors 3
unknown_provider422The provider is not recognised.
module_not_loadable422The provider could not be loaded.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage/validate' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"type":"FTP","config":{"host":"ftp.example.com","username":"backup","password":"secret"}}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/validate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'FTP',
    config: { host: 'ftp.example.com', username: 'backup', password: secret },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/validate');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'type'   => 'FTP',
        'config' => ['host' => 'ftp.example.com', 'username' => 'backup', 'password' => $secret],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The test SAVES NOTHING: passing it does not mean a target now exists.
$check = Api::Settings()->ValidateBackupStorage([
    'type'   => 'FTP',
    'config' => $config,
])['data'];

if ($check['validated']) Api::Settings()->CreateBackupStorage($payload);

Getting the Consent Address

post/api/v1/admin/settings/backup/storage/oauth-url
Settings/GetBackupOauthUrl admin

Builds the address where the user grants a cloud provider access.

Body 3
typestringreqThe provider asking for consent.
idintThe id of an existing target.
configobjectThe provider settings.
Response fields data — 2
authorize_urlstringThe address the user gets sent to.
statestringThe signed state value. It has to match when the user comes back.
Errors 4
unknown_provider422The provider is not recognised.
oauth_unsupported422The provider does not use a consent flow.
oauth_url_failed422The address could not be built.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage/oauth-url' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"type":"GoogleDrive"}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/oauth-url', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ type: 'GoogleDrive' }),
});

const { data } = await res.json();
window.location.href = data.authorize_url;
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/oauth-url');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['type' => 'GoogleDrive']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The address alone is not enough: no target can be CREATED before the user comes back.
$url = Api::Settings()->GetBackupOauthUrl(['type' => 'GoogleDrive'])['data']['authorize_url'];

Reading the Bunny Zones

post/api/v1/admin/settings/backup/storage/bunny-zones
Settings/GetBunnyZones admin

Returns the Bunny storage zones the given account key can reach.

Body 2
account_api_keystringreqThe Bunny account key. Sending the mask means a target id is needed too.
idintThe id of an existing target.
Response fields data — 1
zonesarrayThe storage zones on the account.
Errors 3
api_key_required422No account key was given.
fetch_zones_failed422The zone list could not be fetched.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage/bunny-zones' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"account_api_key":"'"$BUNNY_KEY"'"}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/bunny-zones', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ account_api_key: bunnyKey }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/bunny-zones');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['account_api_key' => $bunnyKey]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A stored target returns its key masked; when sending the mask back, give the ID as well.
$zones = Api::Settings()->GetBunnyZones([
    'account_api_key' => '*****',
    'id'              => $storageId,
])['data']['zones'];

Listing the Targets

get/api/v1/admin/settings/backup/storage
Settings/GetBackupStorages admin

Returns the backup storage targets defined.

Query 1
searchstringSearches by name.
Response fields data[] — 6
idintThe target id.
namestringThe target name.
typestringThe provider type.
statusstringThe target status.
validated_atstring | nullWhen the connection was last tested. Empty means it was never tested.
created_atstring | nullWhen it was created.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The last test can be OLD: the password may have changed since that day.
$targets = Api::Settings()->GetBackupStorages()['data'];
$stale   = array_filter($targets, fn ($t) => $t['validated_at'] === null);

Creating a Target

post/api/v1/admin/settings/backup/storage
Settings/CreateBackupStorage admin connects first

Sets up a new storage target, trying to connect before it saves.

Body 3
namestringreqThe target name. Up to 150 characters.
typestringreqThe provider key.
configobjectThe provider settings. A cloud provider also needs the consent token.
Response fields 201 — data
dataobjectThe target created. Same shape as the detail endpoint, with secrets masked.
Errors 5
name_required422The name is empty.
invalid_provider422The provider is not valid.
oauth_required422The cloud provider has no consent yet.
storage_test_failed422The connection could not be made.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Offsite FTP","type":"FTP","config":{"host":"ftp.example.com","username":"backup","password":"secret","folder_path":"/backups"}}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Offsite FTP',
    type: 'FTP',
    config: {
      host: 'ftp.example.com',
      username: 'backup',
      password: secret,
      folder_path: '/backups',
    },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name'   => 'Offsite FTP',
        'type'   => 'FTP',
        'config' => [
            'host'        => 'ftp.example.com',
            'username'    => 'backup',
            'password'    => $secret,
            'folder_path' => '/backups',
        ],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The mask is USELESS here: there is no old value to keep, so send the real password.
Api::Settings()->CreateBackupStorage([
    'name'   => 'Offsite FTP',
    'type'   => 'FTP',
    'config' => ['host' => $host, 'username' => $user, 'password' => $secret],
]);

Reading One Target

get/api/v1/admin/settings/backup/storage/{id}
Settings/GetBackupStorage admin

Returns one target together with its settings.

Response fields data — 7
idintThe target id.
namestringThe target name.
typestringThe provider type.
statusstringThe target status.
configobjectThe provider settings. Secret values arrive masked.
validated_atstring | nullWhen the connection was last tested.
created_atstring | nullWhen it was created.
Errors 2
not_found404No such target.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Do not feed what you read straight into a NEW target: the password is masked.
$target = Api::Settings()->GetBackupStorage(['id' => $id])['data'];

Updating a Target

patch/api/v1/admin/settings/backup/storage/{id}
Settings/UpdateBackupStorage admin

Changes the target name and its settings.

Body 2
namestringThe new name.
configobjectThe provider's settings, written as a whole. Send the full set: a field you leave out is dropped, and a checkbox or number falls back to zero. ***** keeps an encrypted field's stored value.
Response fields data
dataobjectThe target as it now stands. Same shape as the detail endpoint, with secrets masked.
Errors 5
not_found404No such target.
name_required422The name is empty.
oauth_required422The cloud provider has no consent yet.
storage_test_failed422The connection could not be made.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Offsite FTP (EU)","config":{"folder_path":"/backups-eu","password":"*****"}}'
const res = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Offsite FTP (EU)',
    config: { folder_path: '/backups-eu', password: '*****' },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name'   => 'Offsite FTP (EU)',
        'config' => ['folder_path' => '/backups-eu', 'password' => '*****'],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Change the path and the connection is tested AGAIN; a missing folder fails the update.
Api::Settings()->UpdateBackupStorage([
    'id'     => $id,
    'config' => ['folder_path' => '/backups-eu', 'password' => '*****'],
]);

Deleting a Target

delete/api/v1/admin/settings/backup/storage/{id}
Settings/DeleteBackupStorage admin consent is withdrawn

Removes a storage target.

Response fields data — 2
deletedboolWhether the delete ran.
idintThe id of the target removed.
Errors 3
not_found404No such target.
storage_in_use422A schedule still points at this target.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// On a cloud target the delete also WITHDRAWS consent: re-adding it needs a fresh grant.
Api::Settings()->DeleteBackupStorage(['id' => $id]);

Pitfalls

The mask protects on read and does nothing on create

The detail and list endpoints return secrets masked. Sending the mask back on an update keeps the old value. On create there is no old value to keep. Feed what you read straight into a new target and the mask itself gets stored as the password. The connection test then fails.

Testing is not saving

The test endpoint saves nothing and only tries the connection. Passing it does not mean a target exists. The panel shows the two steps back to back, which is where this gets confused. The test succeeds, the create call is forgotten, and no target exists to give a schedule.

A cloud provider is not set up with settings alone

For a provider that asks for consent, writing settings is not enough: the user has to grant access in a browser. Without the token the create call fails with consent missing. The consent field in the provider list tells you in advance which providers want that three-step path.

A target a schedule uses cannot be deleted

While a schedule points at a target, the delete call is refused. Move the schedule to another target first, or remove it. The guard is deliberate: silently deleting the target would turn the schedule into a job that runs into nothing every night.

Deleting withdraws the consent too

Deleting a cloud target also revokes the access grant at the provider. Adding the same account back asks for a fresh round of consent, and the old token is of no use. The revoke is attempted as best it can be. If the provider cannot be reached, the target still goes and the grant may live on at their end.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.