mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-16 20:49:28 +00:00
feat: backup databases
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { exec } from 'node:child_process';
|
||||
import util from 'util';
|
||||
import fs from 'fs/promises';
|
||||
import fsNormal from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import forge from 'node-forge';
|
||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
||||
@@ -17,6 +16,7 @@ import { day } from './dayjs';
|
||||
import { saveBuildLog } from './buildPacks/common';
|
||||
import { scheduler } from './scheduler';
|
||||
import type { ExecaChildProcess } from 'execa';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
export const version = '3.12.34';
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
@@ -1942,3 +1942,51 @@ export function generateSecrets(
|
||||
}
|
||||
return envs;
|
||||
}
|
||||
|
||||
export async function backupPostgresqlDatabase(database, reply) {
|
||||
const backupFolder = '/tmp'
|
||||
const fileName = `${database.id}-${new Date().getTime()}.gz`
|
||||
const backupFileName = `${backupFolder}/${fileName}`
|
||||
console.log({ database })
|
||||
let command = null
|
||||
switch (database?.type) {
|
||||
case 'postgresql':
|
||||
command = `docker exec ${database.id} sh -c "PGPASSWORD=${database.rootUserPassword} pg_dumpall -U postgres | gzip > ${backupFileName}"`
|
||||
break;
|
||||
case 'mongodb':
|
||||
command = `docker exec ${database.id} sh -c "mongodump --archive=${backupFileName} --gzip --username=${database.rootUser} --password=${database.rootUserPassword}"`
|
||||
break;
|
||||
case 'mysql':
|
||||
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
|
||||
break;
|
||||
case 'mariadb':
|
||||
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
|
||||
break;
|
||||
case 'couchdb':
|
||||
command = `docker exec ${database.id} sh -c "tar -czvf ${backupFileName} /bitnami/couchdb/data"`
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
await executeCommand({
|
||||
dockerId: database.destinationDockerId,
|
||||
command,
|
||||
});
|
||||
const copyCommand = `docker cp ${database.id}:${backupFileName} ${backupFileName}`
|
||||
await executeCommand({
|
||||
dockerId: database.destinationDockerId,
|
||||
command: copyCommand
|
||||
});
|
||||
if (isDev) {
|
||||
await executeCommand({
|
||||
dockerId: database.destinationDockerId,
|
||||
command: `docker cp ${database.id}:${backupFileName} /app/backups/`
|
||||
});
|
||||
}
|
||||
const stream = fsNormal.createReadStream(backupFileName);
|
||||
reply.header('Content-Type', 'application/octet-stream');
|
||||
reply.header('Content-Disposition', `attachment; filename=${fileName}`);
|
||||
reply.header('Content-Length', fsNormal.statSync(backupFileName).size);
|
||||
reply.header('Content-Transfer-Encoding', 'binary');
|
||||
return reply.send(stream)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import fs from 'fs/promises';
|
||||
import {
|
||||
ComposeFile,
|
||||
backupPostgresqlDatabase,
|
||||
createDirectories,
|
||||
decrypt,
|
||||
defaultComposeConfiguration,
|
||||
@@ -351,6 +352,21 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
||||
return errorHandler({ status, message });
|
||||
}
|
||||
}
|
||||
export async function backupDatabase(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const { id } = request.params;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
return await backupPostgresqlDatabase(database, reply);
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message });
|
||||
}
|
||||
}
|
||||
export async function stopDatabase(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
|
||||
import { backupDatabase, cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
|
||||
|
||||
import type { OnlyId } from '../../../../types';
|
||||
|
||||
@@ -39,6 +39,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
|
||||
fastify.post<OnlyId>('/:id/start', async (request) => await startDatabase(request));
|
||||
fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(request));
|
||||
fastify.post<OnlyId>('/:id/backup', async (request, reply) => await backupDatabase(request, reply));
|
||||
};
|
||||
|
||||
export default root;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dev } from '$app/env';
|
||||
import Cookies from 'js-cookie';
|
||||
import { dashify } from './common';
|
||||
|
||||
export function getAPIUrl() {
|
||||
if (GITPOD_WORKSPACE_URL) {
|
||||
@@ -100,6 +101,14 @@ async function send({
|
||||
responseData = await response.json();
|
||||
} else if (contentType?.indexOf('text/plain') !== -1) {
|
||||
responseData = await response.text();
|
||||
} else if (contentType?.indexOf('application/octet-stream') !== -1) {
|
||||
responseData = await response.blob();
|
||||
const fileName = dashify(data.id + '-' + data.name)
|
||||
const fileLink = document.createElement('a');
|
||||
fileLink.href = URL.createObjectURL(new Blob([responseData]))
|
||||
fileLink.download = fileName + '.gz';
|
||||
fileLink.click();
|
||||
fileLink.remove();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -13,17 +13,19 @@
|
||||
import Redis from './_Redis.svelte';
|
||||
import CouchDb from './_CouchDb.svelte';
|
||||
import EdgeDB from './_EdgeDB.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { addToast, appSession, status } from '$lib/store';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = {
|
||||
main: false,
|
||||
public: false
|
||||
public: false,
|
||||
backup: false
|
||||
};
|
||||
let publicUrl = '';
|
||||
let appendOnly = database.settings.appendOnly;
|
||||
@@ -131,6 +133,22 @@
|
||||
loading.main = false;
|
||||
}
|
||||
}
|
||||
async function backupDatabase() {
|
||||
try {
|
||||
loading.backup = true;
|
||||
addToast({
|
||||
message:
|
||||
'Backup will be downloaded soon and saved to /var/lib/docker/volumes/coolify-local-backup/ on the host system.',
|
||||
type: 'success',
|
||||
timeout: 15000
|
||||
});
|
||||
return await post(`/databases/${id}/backup`, { id, name: database.name });
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.backup = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl p-4">
|
||||
@@ -145,6 +163,19 @@
|
||||
class:bg-databases={!loading.main}
|
||||
disabled={loading.main}>{$t('forms.save')}</button
|
||||
>
|
||||
{#if database.type !== 'redis' && database.type !== 'edgedb'}
|
||||
{#if $status.database.isRunning}
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
on:click={backupDatabase}
|
||||
class:loading={loading.backup}
|
||||
class:bg-databases={!loading.backup}
|
||||
disabled={loading.backup}>Backup Database</button
|
||||
>
|
||||
{:else}
|
||||
<button disabled class="btn btn-sm">Backup Database (start the database)</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
||||
|
||||
Reference in New Issue
Block a user