database backups

This commit is contained in:
Saeed Vaziry
2023-08-25 21:05:18 +02:00
parent f9ac454a7c
commit ea3f011f34
55 changed files with 1111 additions and 172 deletions

View File

@ -1,4 +1,4 @@
curl --location --request POST 'https://content.dropboxapi.com/2/files/upload' \
curl -sb --location --request POST 'https://content.dropboxapi.com/2/files/upload' \
--header 'Accept: application/json' \
--header 'Dropbox-API-Arg: {"path":"__dest__"}' \
--header 'Content-Type: text/plain; charset=dropbox-cors-hack' \

View File

@ -0,0 +1,7 @@
<x-server-layout :server="$server">
<x-slot name="pageTitle">{{ __("Backup Files") }}</x-slot>
<div class="space-y-10">
<livewire:databases.database-backup-files :server="$server" :backup="$backup" />
</div>
</x-server-layout>

View File

@ -5,5 +5,7 @@
<livewire:databases.database-list :server="$server" />
<livewire:databases.database-user-list :server="$server" />
<livewire:databases.database-backups :server="$server" />
</div>
</x-server-layout>

View File

@ -58,7 +58,7 @@
</x-sidebar-link>
@endif
@if($server->database())
<x-sidebar-link :href="route('servers.databases', ['server' => $server])" :active="request()->routeIs('servers.databases')">
<x-sidebar-link :href="route('servers.databases', ['server' => $server])" :active="request()->routeIs('servers.databases') || request()->routeIs('servers.databases.backups')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>

View File

@ -28,6 +28,12 @@
</svg>
{{ __('Source Controls') }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link :href="route('storage-providers')" :active="request()->routeIs('storage-providers')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>
{{ __('Storage Providers') }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link :href="route('notification-channels')" :active="request()->routeIs('notification-channels')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />

View File

@ -1 +1 @@
<div wire:poll.7s></div>
<div wire:poll.5s></div>

View File

@ -0,0 +1,74 @@
<div x-data="">
<x-card-header>
<x-slot name="title">{{ __("Backup Files") }}</x-slot>
<x-slot name="description">{{ __("Here you can see your backup files") }}</x-slot>
<x-slot name="aside">
<div>
<x-secondary-button :href="route('servers.databases', ['server' => $server])">
{{ __('Back to Databases') }}
</x-secondary-button>
<x-primary-button class="ml-1" wire:click="backup" wire:loading.attr="disabled">
{{ __("Backup Now") }}
</x-primary-button>
</div>
</x-slot>
</x-card-header>
@if(count($files) > 0)
<x-table class="mt-5">
<tr>
<x-th>{{ __("Name") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Size") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th>{{ __("Restored") }}</x-th>
<x-th>{{ __("Restored To") }}</x-th>
<x-th></x-th>
</tr>
@foreach($files as $file)
<tr>
<x-td>{{ $file->name }}</x-td>
<x-td>
<x-datetime :value="$file->created_at" />
</x-td>
<x-td>{{ $file->size }}</x-td>
<x-td>
<div class="inline-flex">
@include('livewire.databases.partials.backup-file-status', ['status' => $file->status])
</div>
</x-td>
<x-td>
@if($file->restored_at)
<x-datetime :value="$file->restored_at" />
@else
-
@endif
</x-td>
<x-td>
@if($file->restored_to)
{{ $file->restored_to }}
@else
-
@endif
</x-td>
<x-td class="flex w-full justify-end">
@if(in_array($file->status, [\App\Enums\BackupFileStatus::CREATED, \App\Enums\BackupFileStatus::RESTORED, \App\Enums\BackupFileStatus::RESTORE_FAILED]))
<x-icon-button x-on:click="$wire.restoreId = '{{ $file->id }}'; $dispatch('open-modal', 'restore-backup')">
Restore
</x-icon-button>
@endif
<x-icon-button x-on:click="$wire.deleteId = '{{ $file->id }}'; $dispatch('open-modal', 'delete-backup-file')">
Delete
</x-icon-button>
</x-td>
</tr>
@endforeach
</x-table>
<div class="mt-5">
{{ $files->withQueryString()->links() }}
</div>
@include('livewire.databases.partials.restore-backup-modal', ['databases' => $server->databases])
@include('livewire.databases.partials.delete-backup-file-modal')
@else
<x-simple-card class="text-center">{{ __("You don't have any backups yet") }}</x-simple-card>
@endif
</div>

View File

@ -0,0 +1,49 @@
<div x-data="">
<x-card-header>
<x-slot name="title">{{ __("Backups") }}</x-slot>
<x-slot name="description">{{ __("You can backup your databases into external storages") }}</x-slot>
<x-slot name="aside">
<div>
<x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'create-backup')">
{{ __('Create Backup') }}
</x-primary-button>
@include('livewire.databases.partials.create-backup-modal')
</div>
</x-slot>
</x-card-header>
@if(count($backups) > 0)
<x-table>
<tr>
<x-th>{{ __("Database") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
@foreach($backups as $backup)
<tr>
<x-td>{{ $backup->database->name }}</x-td>
<x-td>
<x-datetime :value="$backup->created_at" />
</x-td>
<x-td>
<div class="inline-flex">
@include('livewire.databases.partials.backup-status', ['status' => $backup->status])
</div>
</x-td>
<x-td class="flex w-full justify-end">
<x-icon-button :href="route('servers.databases.backups', ['server' => $server->id, 'backup' => $backup->id])">
Files
</x-icon-button>
<x-icon-button x-on:click="$wire.deleteId = '{{ $backup->id }}'; $dispatch('open-modal', 'delete-backup')">
Delete
</x-icon-button>
</x-td>
</tr>
@endforeach
</x-table>
@include('livewire.databases.partials.delete-backup-modal')
@else
<x-simple-card class="text-center">{{ __("You don't have any backups yet") }}</x-simple-card>
@endif
</div>

View File

@ -15,10 +15,10 @@
@if(count($databases) > 0)
<x-table>
<tr>
<x-td>{{ __("Name") }}</x-td>
<x-td>{{ __("Created") }}</x-td>
<x-td>{{ __("Status") }}</x-td>
<x-td></x-td>
<x-th>{{ __("Name") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
@foreach($databases as $database)
<tr>

View File

@ -15,11 +15,11 @@
@if(count($databaseUsers) > 0)
<x-table>
<tr>
<x-td>{{ __("Username") }}</x-td>
<x-td>{{ __("Created") }}</x-td>
<x-td>{{ __("Linked Databases") }}</x-td>
<x-td>{{ __("Status") }}</x-td>
<x-td></x-td>
<x-th>{{ __("Username") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Linked Databases") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
@foreach($databaseUsers as $databaseUser)
<tr>

View File

@ -0,0 +1,21 @@
@if($status == \App\Enums\BackupFileStatus::CREATED)
<x-status status="success">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupFileStatus::CREATING)
<x-status status="warning">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupFileStatus::FAILED)
<x-status status="danger">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupFileStatus::DELETING)
<x-status status="danger">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupFileStatus::RESTORING)
<x-status status="warning">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupFileStatus::RESTORED)
<x-status status="success">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupFileStatus::RESTORE_FAILED)
<x-status status="danger">{{ $status }}</x-status>
@endif

View File

@ -0,0 +1,9 @@
@if($status == \App\Enums\BackupStatus::RUNNING)
<x-status status="success">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupStatus::DELETING)
<x-status status="danger">{{ $status }}</x-status>
@endif
@if($status == \App\Enums\BackupStatus::FAILED)
<x-status status="danger">{{ $status }}</x-status>
@endif

View File

@ -0,0 +1,78 @@
<x-modal name="create-backup">
<form wire:submit.prevent="create" class="p-6" x-data="{user: false, remote: false}">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Create Backup') }}
</h2>
<div class="mt-6">
<x-input-label for="database" :value="__('Database')" />
<x-select-input wire:model="database" id="database" name="database" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach($databases as $db)
<option value="{{ $db->id }}" @if($database == $db->id) selected @endif>{{ $db->name }}</option>
@endforeach
</x-select-input>
@error('database')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="storage" :value="__('Storage')" />
<x-select-input wire:model="storage" id="storage" name="storage" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach(auth()->user()->storageProviders as $st)
<option value="{{ $st->id }}" @if($storage == $st->id) selected @endif>{{ $st->profile }} - {{ $st->provider }}</option>
@endforeach
</x-select-input>
@error('storage')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="interval" :value="__('Interval')" />
<x-select-input wire:model="interval" id="interval" name="interval" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
<option value="0 * * * *" @if($interval === '0 * * * *') selected @endif>{{ __("Hourly") }}</option>
<option value="0 0 * * *" @if($interval === '0 0 * * *') selected @endif>{{ __("Daily") }}</option>
<option value="0 0 * * 0" @if($interval === '0 0 * * 0') selected @endif>{{ __("Weekly") }}</option>
<option value="0 0 1 * *" @if($interval === '0 0 1 * *') selected @endif>{{ __("Monthly") }}</option>
<option value="custom">{{ __("Custom") }}</option>
</x-select-input>
@error('interval')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@if($interval === 'custom')
<div class="mt-6">
<x-input-label for="custom" :value="__('Custom interval (Cron)')" />
<x-text-input wire:model.defer="custom" id="custom" name="custom" type="text" class="mt-1 w-full" placeholder="* * * * *" />
@error('custom')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@endif
<div class="mt-6">
<x-input-label for="keep" :value="__('Backups to Keep')" />
<x-text-input wire:model.defer="keep" id="keep" name="keep" type="text" class="mt-1 w-full" />
@error('keep')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-primary-button class="ml-3" @backup-created.window="$dispatch('close')">
{{ __('Create') }}
</x-primary-button>
</div>
</form>
</x-modal>

View File

@ -0,0 +1,6 @@
<x-confirm-modal
name="delete-backup-file"
:title="__('Confirm')"
:description="__('Are you sure that you want to delete this file?')"
method="delete"
/>

View File

@ -0,0 +1,6 @@
<x-confirm-modal
name="delete-backup"
:title="__('Confirm')"
:description="__('Are you sure that you want to delete this backup?')"
method="delete"
/>

View File

@ -0,0 +1,30 @@
<x-modal name="restore-backup">
<form wire:submit.prevent="restore" class="p-6" x-data="{}">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Restore Backup') }}
</h2>
<div class="mt-6">
<x-input-label for="database" :value="__('Database')" />
<x-select-input wire:model="restoreDatabaseId" id="restoreDatabaseId" name="restoreDatabaseId" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach($databases as $db)
<option value="{{ $db->id }}" @if($restoreDatabaseId == $db->id) selected @endif>{{ $db->name }}</option>
@endforeach
</x-select-input>
@error('restoreDatabaseId')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-primary-button class="ml-3" @restored.window="$dispatch('close')">
{{ __('Restore') }}
</x-primary-button>
</div>
</form>
</x-modal>

View File

@ -0,0 +1,54 @@
<div>
<x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'connect-provider')">
{{ __('Connect') }}
</x-primary-button>
<x-modal name="connect-provider">
<form wire:submit.prevent="connect" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Connect to a Storage Provider') }}
</h2>
<div class="mt-6">
<x-input-label for="provider" value="Provider" />
<x-select-input wire:model="provider" id="provider" name="provider" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach(config('core.storage_providers') as $p)
@if($p !== 'custom')
<option value="{{ $p }}" @if($provider === $p) selected @endif>{{ $p }}</option>
@endif
@endforeach
</x-select-input>
@error('provider')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="name" value="Name" />
<x-text-input wire:model.defer="name" id="name" name="name" type="text" class="mt-1 w-full" />
@error('name')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="token" value="API Key" />
<x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" />
@error('token')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-primary-button class="ml-3" @connected.window="$dispatch('close')">
{{ __('Connect') }}
</x-primary-button>
</div>
</form>
</x-modal>
</div>

View File

@ -0,0 +1,45 @@
<div>
<x-card-header>
<x-slot name="title">Storage Providers</x-slot>
<x-slot name="description">You can connect to your storage providers</x-slot>
<x-slot name="aside">
<livewire:storage-providers.connect-provider />
</x-slot>
</x-card-header>
<div x-data="" class="space-y-3">
@if(count($providers) > 0)
@foreach($providers as $provider)
<x-item-card>
<div class="flex-none">
<img src="{{ asset('static/images/' . $provider->provider . '.svg') }}" class="h-10 w-10" alt="">
</div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
<span class="mb-1">{{ $provider->profile }}</span>
<span class="text-sm text-gray-400">
<x-datetime :value="$provider->created_at"/>
</span>
</div>
<div class="flex items-center">
<div class="inline">
<x-icon-button x-on:click="$wire.deleteId = '{{ $provider->id }}'; $dispatch('open-modal', 'delete-provider')">
Delete
</x-icon-button>
</div>
</div>
</x-item-card>
@endforeach
<x-confirm-modal
name="delete-provider"
:title="__('Confirm')"
:description="__('Are you sure that you want to delete this provider?')"
method="delete"
/>
@else
<x-simple-card>
<div class="text-center">
{{ __("You haven't connected to any storage providers yet!") }}
</div>
</x-simple-card>
@endif
</div>
</div>

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-slot name="pageTitle">{{ __("Server Providers") }}</x-slot>
<x-slot name="pageTitle">{{ __("Storage Providers") }}</x-slot>
<livewire:server-providers.providers-list />
</x-profile-layout>

View File

@ -0,0 +1,5 @@
<x-profile-layout>
<x-slot name="pageTitle">{{ __("Storage Providers") }}</x-slot>
<livewire:storage-providers.providers-list />
</x-profile-layout>