mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-17 12:33:06 +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 fs from 'fs/promises';
|
||||||
|
import fsNormal from 'fs';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import forge from 'node-forge';
|
import forge from 'node-forge';
|
||||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
||||||
@@ -17,6 +16,7 @@ import { day } from './dayjs';
|
|||||||
import { saveBuildLog } from './buildPacks/common';
|
import { saveBuildLog } from './buildPacks/common';
|
||||||
import { scheduler } from './scheduler';
|
import { scheduler } from './scheduler';
|
||||||
import type { ExecaChildProcess } from 'execa';
|
import type { ExecaChildProcess } from 'execa';
|
||||||
|
import { FastifyReply } from 'fastify';
|
||||||
|
|
||||||
export const version = '3.12.34';
|
export const version = '3.12.34';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
@@ -1942,3 +1942,51 @@ export function generateSecrets(
|
|||||||
}
|
}
|
||||||
return envs;
|
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 fs from 'fs/promises';
|
||||||
import {
|
import {
|
||||||
ComposeFile,
|
ComposeFile,
|
||||||
|
backupPostgresqlDatabase,
|
||||||
createDirectories,
|
createDirectories,
|
||||||
decrypt,
|
decrypt,
|
||||||
defaultComposeConfiguration,
|
defaultComposeConfiguration,
|
||||||
@@ -351,6 +352,21 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
return errorHandler({ status, message });
|
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>) {
|
export async function stopDatabase(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
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';
|
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/start', async (request) => await startDatabase(request));
|
||||||
fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(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;
|
export default root;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { dev } from '$app/env';
|
import { dev } from '$app/env';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
import { dashify } from './common';
|
||||||
|
|
||||||
export function getAPIUrl() {
|
export function getAPIUrl() {
|
||||||
if (GITPOD_WORKSPACE_URL) {
|
if (GITPOD_WORKSPACE_URL) {
|
||||||
@@ -100,6 +101,14 @@ async function send({
|
|||||||
responseData = await response.json();
|
responseData = await response.json();
|
||||||
} else if (contentType?.indexOf('text/plain') !== -1) {
|
} else if (contentType?.indexOf('text/plain') !== -1) {
|
||||||
responseData = await response.text();
|
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 {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,19 @@
|
|||||||
import Redis from './_Redis.svelte';
|
import Redis from './_Redis.svelte';
|
||||||
import CouchDb from './_CouchDb.svelte';
|
import CouchDb from './_CouchDb.svelte';
|
||||||
import EdgeDB from './_EdgeDB.svelte';
|
import EdgeDB from './_EdgeDB.svelte';
|
||||||
import { post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import { addToast, appSession, status } from '$lib/store';
|
import { addToast, appSession, status } from '$lib/store';
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
import Explainer from '$lib/components/Explainer.svelte';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
|
||||||
let loading = {
|
let loading = {
|
||||||
main: false,
|
main: false,
|
||||||
public: false
|
public: false,
|
||||||
|
backup: false
|
||||||
};
|
};
|
||||||
let publicUrl = '';
|
let publicUrl = '';
|
||||||
let appendOnly = database.settings.appendOnly;
|
let appendOnly = database.settings.appendOnly;
|
||||||
@@ -131,6 +133,22 @@
|
|||||||
loading.main = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-6xl p-4">
|
<div class="mx-auto max-w-6xl p-4">
|
||||||
@@ -145,6 +163,19 @@
|
|||||||
class:bg-databases={!loading.main}
|
class:bg-databases={!loading.main}
|
||||||
disabled={loading.main}>{$t('forms.save')}</button
|
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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user