Compare commits

...
Author SHA1 Message Date
Christy Jacob 7e3ebc15a0 feat: add restore functionality 2023-03-19 17:23:51 +05:30
Christy Jacob 6840f73a83 feat: update backup script 2023-03-17 10:27:31 +05:30
Christy Jacob 543e57d0bf feat: add worker 2023-03-11 12:24:33 +05:30
Christy Jacob a7b074449f feat: initial commit 2023-03-10 11:49:29 +05:30
Christy Jacob 1092450d97 feat: initial commit 2023-03-10 11:48:39 +05:30
20 changed files with 741 additions and 14 deletions
+6 -1
View File
@@ -31,6 +31,8 @@ RUN npm run build
FROM appwrite/base:0.1.0 as final
RUN apk add mysql-client
LABEL maintainer="team@appwrite.io"
ARG VERSION=dev
@@ -147,12 +149,14 @@ RUN mkdir -p /storage/uploads && \
mkdir -p /storage/certificates && \
mkdir -p /storage/functions && \
mkdir -p /storage/debug && \
mkdir -p /storage/backups && \
chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \
chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \
chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \
chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \
chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions && \
chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug
chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug && \
chown -Rf www-data.www-data /storage/backups && chmod -Rf 0755 /storage/backups
# Executables
RUN chmod +x /usr/local/bin/doctor && \
@@ -169,6 +173,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/vars && \
chmod +x /usr/local/bin/worker-audits && \
chmod +x /usr/local/bin/worker-backups && \
chmod +x /usr/local/bin/worker-certificates && \
chmod +x /usr/local/bin/worker-databases && \
chmod +x /usr/local/bin/worker-deletes && \
+83
View File
@@ -1185,6 +1185,89 @@ $collections = [
],
],
'backups' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('backups'),
'name' => 'backups',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1024,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('path'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 512,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => 'processing',
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
]
],
],
'users' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('users'),
+3 -3
View File
@@ -185,7 +185,7 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '0.0.1',
'version' => '0.0.3',
'url' => 'https://github.com/appwrite/sdk-for-console',
'package' => '',
'enabled' => true,
@@ -197,8 +197,8 @@ return [
'source' => \realpath(__DIR__ . '/../sdks/console-web'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-console.git',
'gitBranch' => 'main',
'gitRepoName' => 'sdk-for-console',
'gitUserName' => 'appwrite',
'gitRepoName' => 'christyjacob4-appwrite-console',
'gitUserName' => '',
],
[
'key' => 'cli',
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+293 -3
View File
@@ -4,7 +4,8 @@ use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Validator\Event;
use Appwrite\Event\Event;
use Appwrite\Event\Validator\Event as EventValidator;
use Appwrite\Network\Validator\CNAME;
use Utopia\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\Origin;
@@ -659,7 +660,7 @@ App::post('/v1/projects/:projectId/webhooks')
->label('sdk.response.model', Response::MODEL_WEBHOOK)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('events', null, new ArrayList(new EventValidator(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
@@ -782,7 +783,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('webhookId', '', new UID(), 'Webhook unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('events', null, new ArrayList(new EventValidator(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
@@ -1538,3 +1539,292 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
$response->noContent();
});
App::post('/v1/projects/:projectId/backups')
->desc('Create Backup')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'createBackup')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_BACKUP)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', '', new Text(256), 'Backup name.')
->param('description', '', new Text(1024), 'Backup description.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, string $description, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$backup = $dbForConsole->createDocument('backups', new Document([
'$id' => ID::unique(),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'name' => $name,
'description' => $description,
'status' => 'pending',
]));
$event = new Event(Event::BACKUPS_QUEUE_NAME, Event::BACKUPS_CLASS_NAME);
$event
->setProject($project)
->setPayload([
'type' => 'backup',
'backupId' => $backup->getId(),
])
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($backup, Response::MODEL_BACKUP);
});
App::get('/v1/projects/:projectId/backups')
->desc('List Backups')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'listBackups')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_BACKUP_LIST)
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$backups = $dbForConsole->find('backups', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(5000)
]);
$response->dynamic(new Document([
'backups' => $backups,
'total' => count($backups),
]), Response::MODEL_BACKUP_LIST);
});
App::get('/v1/projects/:projectId/backups/:backupId')
->desc('Get Backup')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'getBackup')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_BACKUP)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('backupId', '', new UID(), 'Backup unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $backupId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$backup = $dbForConsole->findOne('backups', [
Query::equal('$id', [$backupId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($backup === false || $backup->isEmpty()) {
throw new Exception(Exception::BACKUP_NOT_FOUND);
}
$response->dynamic($backup, Response::MODEL_BACKUP);
});
App::delete('/v1/projects/:projectId/backups/:backupId')
->desc('Delete Backup')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'deleteBackup')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('backupId', '', new UID(), 'Backup unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $backupId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$backup = $dbForConsole->findOne('backups', [
Query::equal('$id', [$backupId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($backup === false || $backup->isEmpty()) {
throw new Exception(Exception::BACKUP_NOT_FOUND);
}
if (!$dbForConsole->deleteDocument('backups', $backupId)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to delete backup from DB.');
}
$response->noContent();
});
App::patch('/v1/projects/:projectId/backups/:backupId')
->desc('Update Backup')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateBackup')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_BACKUP)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('backupId', '', new UID(), 'Backup unique ID.')
->param('name', null, new Text(256), 'Backup name.', true)
->param('description', null, new Text(1024), 'Backup description.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $backupId, ?string $name, ?string $description, ?string $schedule, ?int $keep, ?array $webhooks, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$backup = $dbForConsole->findOne('backups', [
Query::equal('$id', [$backupId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($backup === false || $backup->isEmpty()) {
throw new Exception(Exception::BACKUP_NOT_FOUND);
}
$backup
->setAttribute('name', $name ?? $backup->getAttribute('name'))
->setAttribute('description', $description ?? $backup->getAttribute('description'));
if(!$dbForConsole->updateDocument('backups', $backup->getId(), $backup)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to update backup in DB.');
}
$response->dynamic($backup, Response::MODEL_BACKUP);
});
App::post('/v1/projects/:projectId/backups/:backupId/restore')
->desc('Restore Backup')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'restoreBackup')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('backupId', '', new UID(), 'Backup unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $backupId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$backup = $dbForConsole->findOne('backups', [
Query::equal('$id', [$backupId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($backup === false || $backup->isEmpty()) {
throw new Exception(Exception::BACKUP_NOT_FOUND);
}
$event = new Event(Event::BACKUPS_QUEUE_NAME, Event::BACKUPS_CLASS_NAME);
$event
->setProject($project)
->setPayload([
'type' => 'restore',
'backupId' => $backup->getId(),
])
->trigger();
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED)->noContent();
});
// App::get('/v1/projects/:projectId/backups/:backupId/export')
// ->desc('Export Backup')
// ->groups(['api', 'projects'])
// ->label('scope', 'projects.write')
// ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
// ->label('sdk.namespace', 'projects')
// ->label('sdk.method', 'exportBackup')
// ->label('sdk.response.code', Response::STATUS_CODE_OK)
// ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
// ->label('sdk.response.model', Response::MODEL_BACKUP)
// ->param('projectId', '', new UID(), 'Project unique ID.')
// ->param('backupId', '', new UID(), 'Backup unique ID.')
// ->inject('response')
// ->inject('dbForConsole')
// ->action(function (string $projectId, string $backupId, Response $response, Database $dbForConsole) {
// $project = $dbForConsole->getDocument('projects', $projectId);
// if ($project->isEmpty()) {
// throw new Exception(Exception::PROJECT_NOT_FOUND);
// }
// $backup = $dbForConsole->findOne('backups', [
// Query::equal('_uid', [$backupId]),
// Query::equal('projectInternalId', [$project->getInternalId()]),
// ]);
// if ($backup === false || $backup->isEmpty()) {
// throw new Exception(Exception::BACKUP_NOT_FOUND);
// }
// $backupCollection = $dbForConsole->getCollection('backup_' . $backup->getId());
// $collections = $backupCollection->listCollections();
// $export = [];
// foreach ($collections as $collection) {
// $documents = $collection->find();
// foreach ($documents as $document) {
// $export[$collection->getId()][] = $document->getArrayCopy();
// }
// }
// $response->json($export);
// });
+1
View File
@@ -114,6 +114,7 @@ const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_BUILDS = '/storage/builds';
const APP_STORAGE_CACHE = '/storage/cache';
const APP_STORAGE_CERTIFICATES = '/storage/certificates';
const APP_STORAGE_BACKUPS = '/storage/backups';
const APP_STORAGE_CONFIG = '/storage/config';
const APP_STORAGE_READ_BUFFER = 20 * (1000 * 1000); //20MB other names `APP_STORAGE_MEMORY_LIMIT`, `APP_STORAGE_MEMORY_BUFFER`, `APP_STORAGE_READ_LIMIT`, `APP_STORAGE_BUFFER_LIMIT`
const APP_SOCIAL_TWITTER = 'https://twitter.com/appwrite';
+207
View File
@@ -0,0 +1,207 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Resque\Worker;
use Utopia\Audit\Audit;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Storage\Device\Local;
/**
* Todo - allow backups of specific tables
* Todo - delete temporary files generated during backup
* Todo - Backup certificates ?
* Todo - Backup Functions
* Todo - How to backup files?
*/
require_once __DIR__ . '/../init.php';
Console::title('Backups V1 Worker');
Console::success(APP_NAME . ' backups worker v1 has started');
class BackupsV1 extends Worker
{
public function getName(): string
{
return "backups";
}
public function init(): void
{
}
public function run(): void
{
$project = $this->args['project'];
$type = $this->args['payload']['type'];
$backupId = $this->args['payload']['backupId'];
$project = new Document($project);
/** Create a database dump of all the tables in this project database */
switch ($type) {
case 'backup':
$this->backup($project, $backupId);
break;
case 'restore':
$this->restore($project, $backupId);
break;
default:
Console::error('Unknown backup type: ' . $type);
break;
}
}
private function backup(Document $project, string $backupId)
{
try {
$dbForConsole = $this->getConsoleDB();
/** Update the backup state */
$backup = $dbForConsole->getDocument('backups', $backupId);
if ($backup->isEmpty()) {
Console::error('Backup not found: ' . $backupId);
return;
}
$backup->setAttribute('status', 'processing');
$dbForConsole->updateDocument('backups', $backupId, $backup);
/** Create list of tables to backup */
$dbForProject = $this->getProjectDB($project->getId(), $project);
$tables = $dbForProject->listCollections(1000);
$tablesToBackup = [];
foreach ($tables as $table) {
$tablesToBackup[] = $dbForProject->getNamespace() . '_' .$table->getId();
$tablesToBackup[] = $dbForProject->getNamespace() . '_' .$table->getId() . '_perms';
}
/** Backup the metadata tables */
$tablesToBackup[] = $dbForProject->getNamespace() . '__metadata';
$tablesToBackup[] = $dbForProject->getNamespace() . '__metadata_perms';
/**
* Create Temporary Backup File
*/
$sqldumpFilename = $backupId . '.sql';
$compressedFilename = $backupId . '.tar.gz';
$tmpSqlDump = "/tmp/backups/$backupId/$sqldumpFilename";
$tmpBackup = "/tmp/backups/$backupId/$compressedFilename";
if (!\file_exists(\dirname($tmpBackup))) {
if (!@\mkdir(\dirname($tmpBackup), 0755, true)) {
throw new Exception("Failed to create temporary directory", 500);
}
}
$deviceBackups = $this->getBackupsDevice($project->getId());
$path = $deviceBackups->getPath($compressedFilename);
$command = 'mysqldump -alv -h' . App::getEnv('_APP_DB_HOST') . ' -u' . App::getEnv('_APP_DB_USER') . ' -p' . App::getEnv('_APP_DB_PASS') . ' ' . App::getEnv('_APP_DB_SCHEMA') . ' ' . implode(' ', $tablesToBackup) . ' > ' . $tmpSqlDump;
Console::info('Running command: ' . $command);
$stdout = '';
$stderr = '';
$stdin = '';
$return = Console::execute($command, $stdin, $stdout, $stderr);
if ($return !== 0) {
throw new Exception('Failed to create backup: ' . $backupId);
}
/** Compress the SQL dump file */
$command = 'tar -czf ' . $tmpBackup . ' -C /tmp/backups/' . $backupId . ' ' . $sqldumpFilename;
Console::info('Running command: ' . $command);
$return = Console::execute($command, $stdin, $stdout, $stderr);
if ($return !== 0) {
throw new Exception('Failed to compress backup: ' . $backupId);
}
/** Zip the Backup file and move it to the path */
$return = $deviceBackups->move($tmpBackup, $path);
if ($return === false) {
throw new Exception('Failed to move backup: ' . $backupId);
}
$backup->setAttribute('path', $path);
$backup->setAttribute('status', 'completed');
$dbForConsole->updateDocument('backups', $backup->getId(), $backup);
} catch (Exception $e) {
Console::error($e->getMessage());
$backup->setAttribute('status', 'failed');
$dbForConsole->updateDocument('backups', $backup->getId(), $backup);
}
}
private function restore(Document $project, string $backupId) {
/** Write a function to read the backup and restore it */
try {
$dbForConsole = $this->getConsoleDB();
/** Update the backup state */
$backup = $dbForConsole->getDocument('backups', $backupId);
if ($backup->isEmpty()) {
Console::error('Backup not found: ' . $backupId);
return;
}
/**
* Create Temporary file to untar and store the resultant backup
*/
$sqldumpFilename = $backupId . '.sql';
$compressedFilename = $backupId . '.tar.gz';
$tmpSqlDump = "/tmp/backups/$backupId/$sqldumpFilename";
$tmpBackup = "/tmp/backups/$backupId/$compressedFilename";
if (!\file_exists(\dirname($tmpBackup))) {
if (!@\mkdir(\dirname($tmpBackup), 0755, true)) {
throw new Exception("Failed to create temporary directory", 500);
}
}
/** load the backup file to a temporary directory */
$localDevice = new Local();
$deviceBackups = $this->getBackupsDevice($project->getId());
$path = $backup->getAttribute('path', '');
$buffer = $deviceBackups->read($path);
if (!$localDevice->write($tmpBackup, $buffer)) {
throw new Exception('Failed to copy backup to temporary directory', 500);
};
// Untar the file in path and store the resulting sql file in tmpSqlDump
$command = 'tar -xzf ' . $tmpBackup . ' -C /tmp/backups/' . $backupId . ' ' . $sqldumpFilename;
Console::info('Running command: ' . $command);
$stdout = '';
$stderr = '';
$stdin = '';
$return = Console::execute($command, $stdin, $stdout, $stderr);
if ($return !== 0) {
throw new Exception('Failed to extract backup: ' . $backupId);
}
/** Restore the backup from the .sql file located at $tmpSqlDump */
$command = 'mysql -h' . App::getEnv('_APP_DB_HOST') . ' -u' . App::getEnv('_APP_DB_USER') . ' -p' . App::getEnv('_APP_DB_PASS') . ' ' . App::getEnv('_APP_DB_SCHEMA') . ' < ' . $tmpSqlDump;
Console::info('Running command: ' . $command);
$return = Console::execute($command, $stdin, $stdout, $stderr);
if ($return !== 0) {
throw new Exception('Failed to restore backup: ' . $backupId);
}
} catch (Exception $e) {
Console::error($e->getMessage());
}
}
public function shutdown(): void
{
}
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
INTERVAL=1 QUEUE='v1-backups' APP_INCLUDE='/usr/src/code/app/workers/backups.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
+30
View File
@@ -252,6 +252,35 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-backups:
entrypoint: worker-backups
<<: *x-logging
container_name: appwrite-worker-backups
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- appwrite-backups:/storage/backups:rw
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
entrypoint: worker-webhooks
<<: *x-logging
@@ -845,6 +874,7 @@ volumes:
appwrite-redis:
appwrite-cache:
appwrite-uploads:
appwrite-backups:
appwrite-certificates:
appwrite-functions:
appwrite-builds:
+3
View File
@@ -35,6 +35,9 @@ class Event
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
public const BACKUPS_QUEUE_NAME = 'v1-backups';
public const BACKUPS_CLASS_NAME = 'BackupsV1';
protected string $queue = '';
protected string $class = '';
protected string $event = '';
+3
View File
@@ -170,6 +170,9 @@ class Exception extends \Exception
public const VARIABLE_NOT_FOUND = 'variable_not_found';
public const VARIABLE_ALREADY_EXISTS = 'variable_already_exists';
/** Backups */
public const BACKUP_NOT_FOUND = 'backup_not_found';
/** Platform */
public const PLATFORM_NOT_FOUND = 'platform_not_found';
+10
View File
@@ -285,6 +285,16 @@ abstract class Worker
return $this->getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId);
}
/**
* Get Backups Device
* @param string $projectId of the project
* @return Device
*/
protected function getBackupsDevice(string $projectId): Device
{
return $this->getDevice(APP_STORAGE_BACKUPS . '/app-' . $projectId);
}
/**
* Get Builds Storage Device
+5
View File
@@ -30,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeEnum;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\Backup;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Database;
@@ -205,6 +206,8 @@ class Response extends SwooleResponse
public const MODEL_DOMAIN_LIST = 'domainList';
public const MODEL_VARIABLE = 'variable';
public const MODEL_VARIABLE_LIST = 'variableList';
public const MODEL_BACKUP = 'backup';
public const MODEL_BACKUP_LIST = 'backupList';
// Health
public const MODEL_HEALTH_STATUS = 'healthStatus';
@@ -274,6 +277,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
->setModel(new BaseList('Backups List', self::MODEL_BACKUP_LIST, 'backups', self::MODEL_BACKUP))
// Entities
->setModel(new Database())
->setModel(new Collection())
@@ -288,6 +292,7 @@ class Response extends SwooleResponse
->setModel(new AttributeIP())
->setModel(new AttributeURL())
->setModel(new AttributeDatetime())
->setModel(new Backup())
->setModel(new Index())
->setModel(new ModelDocument())
->setModel(new Log())
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Backup extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Bucket ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Bucket creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Bucket update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'Backup permissions. [Learn more about permissions](/docs/permissions).',
'default' => [],
'example' => ['read("any")'],
'array' => true,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Bucket name.',
'default' => '',
'example' => 'Documents',
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Allowed file extensions.',
'default' => [],
'example' => 'This Backup was created on the 21st of March 2023',
'array' => false
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Status of the backup. One of pending, processing, created, failed.',
'default' => [],
'example' => 'pending',
'array' => false
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Backup';
}
/**
* Get Collection
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_BACKUP;
}
}