feat(appstore): add remote app store templates with service upgrades and Redis/MariaDB platform support
This commit is contained in:
@@ -113,6 +113,12 @@ export class BackupManager {
|
||||
case 'clickhouse':
|
||||
await this.exportClickHouseDatabase(dataDir, resource, credentials);
|
||||
break;
|
||||
case 'mariadb':
|
||||
await this.exportMariaDBDatabase(dataDir, resource, credentials);
|
||||
break;
|
||||
case 'redis':
|
||||
await this.exportRedisData(dataDir, resource, credentials);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +364,8 @@ export class BackupManager {
|
||||
enableMongoDB: serviceConfig.platformRequirements?.mongodb,
|
||||
enableS3: serviceConfig.platformRequirements?.s3,
|
||||
enableClickHouse: serviceConfig.platformRequirements?.clickhouse,
|
||||
enableRedis: serviceConfig.platformRequirements?.redis,
|
||||
enableMariaDB: serviceConfig.platformRequirements?.mariadb,
|
||||
};
|
||||
|
||||
service = await this.oneboxRef.services.deployService(deployOptions);
|
||||
@@ -789,6 +797,24 @@ export class BackupManager {
|
||||
);
|
||||
restoredCount++;
|
||||
break;
|
||||
case 'mariadb':
|
||||
await this.importMariaDBDatabase(
|
||||
dataDir,
|
||||
existing.resource,
|
||||
existing.credentials,
|
||||
backupResource.resourceName
|
||||
);
|
||||
restoredCount++;
|
||||
break;
|
||||
case 'redis':
|
||||
await this.importRedisData(
|
||||
dataDir,
|
||||
existing.resource,
|
||||
existing.credentials,
|
||||
backupResource.resourceName
|
||||
);
|
||||
restoredCount++;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
warnings.push(
|
||||
@@ -1003,6 +1029,230 @@ export class BackupManager {
|
||||
logger.success(`ClickHouse database imported: ${resource.resourceName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export MariaDB database
|
||||
*/
|
||||
private async exportMariaDBDatabase(
|
||||
dataDir: string,
|
||||
resource: IPlatformResource,
|
||||
credentials: Record<string, string>,
|
||||
): Promise<void> {
|
||||
logger.info(`Exporting MariaDB database: ${resource.resourceName}`);
|
||||
|
||||
const mariadbService = this.oneboxRef.database.getPlatformServiceByType('mariadb');
|
||||
if (!mariadbService || !mariadbService.containerId) {
|
||||
throw new Error('MariaDB service not running');
|
||||
}
|
||||
|
||||
const dbName = credentials.database || resource.resourceName;
|
||||
const user = credentials.username || 'root';
|
||||
const password = credentials.password || '';
|
||||
|
||||
if (!dbName) {
|
||||
throw new Error('MariaDB database name not found in credentials');
|
||||
}
|
||||
|
||||
// Use mariadb-dump via docker exec
|
||||
const result = await this.oneboxRef.docker.execInContainer(mariadbService.containerId, [
|
||||
'mariadb-dump',
|
||||
'-u', user,
|
||||
`-p${password}`,
|
||||
'--single-transaction',
|
||||
'--routines',
|
||||
'--triggers',
|
||||
dbName,
|
||||
]);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`MariaDB dump failed: ${result.stderr.substring(0, 500)}`);
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(`${dataDir}/${resource.resourceName}.sql`, result.stdout);
|
||||
logger.success(`MariaDB database exported: ${resource.resourceName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import MariaDB database
|
||||
*/
|
||||
private async importMariaDBDatabase(
|
||||
dataDir: string,
|
||||
resource: IPlatformResource,
|
||||
credentials: Record<string, string>,
|
||||
backupResourceName: string,
|
||||
): Promise<void> {
|
||||
logger.info(`Importing MariaDB database: ${resource.resourceName}`);
|
||||
|
||||
const mariadbService = this.oneboxRef.database.getPlatformServiceByType('mariadb');
|
||||
if (!mariadbService || !mariadbService.containerId) {
|
||||
throw new Error('MariaDB service not running');
|
||||
}
|
||||
|
||||
const dbName = credentials.database || resource.resourceName;
|
||||
const user = credentials.username || 'root';
|
||||
const password = credentials.password || '';
|
||||
|
||||
if (!dbName) {
|
||||
throw new Error('MariaDB database name not found');
|
||||
}
|
||||
|
||||
// Read the SQL dump
|
||||
const sqlPath = `${dataDir}/${backupResourceName}.sql`;
|
||||
let sqlContent: string;
|
||||
try {
|
||||
sqlContent = await Deno.readTextFile(sqlPath);
|
||||
} catch {
|
||||
logger.warn(`MariaDB dump file not found: ${sqlPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sqlContent.trim()) {
|
||||
logger.warn(`MariaDB dump file is empty: ${sqlPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Split into individual statements and execute
|
||||
const statements = sqlContent
|
||||
.split(';\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0 && !s.startsWith('--') && !s.startsWith('/*'));
|
||||
|
||||
for (const statement of statements) {
|
||||
const result = await this.oneboxRef.docker.execInContainer(mariadbService.containerId, [
|
||||
'mariadb',
|
||||
'-u', user,
|
||||
`-p${password}`,
|
||||
dbName,
|
||||
'-e', statement + ';',
|
||||
]);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
logger.warn(`MariaDB statement failed: ${result.stderr.substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.success(`MariaDB database imported: ${resource.resourceName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export Redis data
|
||||
*/
|
||||
private async exportRedisData(
|
||||
dataDir: string,
|
||||
resource: IPlatformResource,
|
||||
credentials: Record<string, string>,
|
||||
): Promise<void> {
|
||||
logger.info(`Exporting Redis data: ${resource.resourceName}`);
|
||||
|
||||
const redisService = this.oneboxRef.database.getPlatformServiceByType('redis');
|
||||
if (!redisService || !redisService.containerId) {
|
||||
throw new Error('Redis service not running');
|
||||
}
|
||||
|
||||
const password = credentials.password || '';
|
||||
const dbIndex = credentials.db || '0';
|
||||
|
||||
// Trigger a BGSAVE to ensure data is flushed
|
||||
await this.oneboxRef.docker.execInContainer(redisService.containerId, [
|
||||
'redis-cli', '-a', password, 'BGSAVE',
|
||||
]);
|
||||
|
||||
// Wait for save to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Get all keys in the specific database and export them
|
||||
const keysResult = await this.oneboxRef.docker.execInContainer(redisService.containerId, [
|
||||
'redis-cli', '-a', password, '-n', dbIndex, 'KEYS', '*',
|
||||
]);
|
||||
|
||||
if (keysResult.exitCode !== 0) {
|
||||
throw new Error(`Redis KEYS failed: ${keysResult.stderr.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
const keys = keysResult.stdout.trim().split('\n').filter(k => k.length > 0);
|
||||
const exportData: Record<string, { type: string; value: string; ttl: number }> = {};
|
||||
|
||||
for (const key of keys) {
|
||||
// Get key type
|
||||
const typeResult = await this.oneboxRef.docker.execInContainer(redisService.containerId, [
|
||||
'redis-cli', '-a', password, '-n', dbIndex, 'TYPE', key,
|
||||
]);
|
||||
const keyType = typeResult.stdout.trim();
|
||||
|
||||
// Get TTL
|
||||
const ttlResult = await this.oneboxRef.docker.execInContainer(redisService.containerId, [
|
||||
'redis-cli', '-a', password, '-n', dbIndex, 'TTL', key,
|
||||
]);
|
||||
const ttl = parseInt(ttlResult.stdout.trim(), 10);
|
||||
|
||||
// DUMP the key (binary-safe serialization)
|
||||
const dumpResult = await this.oneboxRef.docker.execInContainer(redisService.containerId, [
|
||||
'redis-cli', '-a', password, '-n', dbIndex, '--no-auth-warning', 'DUMP', key,
|
||||
]);
|
||||
|
||||
exportData[key] = {
|
||||
type: keyType,
|
||||
value: dumpResult.stdout,
|
||||
ttl: ttl > 0 ? ttl : 0,
|
||||
};
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(
|
||||
`${dataDir}/${resource.resourceName}.json`,
|
||||
JSON.stringify({ dbIndex, keys: exportData }, null, 2)
|
||||
);
|
||||
|
||||
logger.success(`Redis data exported: ${resource.resourceName} (${keys.length} keys)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Redis data
|
||||
*/
|
||||
private async importRedisData(
|
||||
dataDir: string,
|
||||
resource: IPlatformResource,
|
||||
credentials: Record<string, string>,
|
||||
backupResourceName: string,
|
||||
): Promise<void> {
|
||||
logger.info(`Importing Redis data: ${resource.resourceName}`);
|
||||
|
||||
const redisService = this.oneboxRef.database.getPlatformServiceByType('redis');
|
||||
if (!redisService || !redisService.containerId) {
|
||||
throw new Error('Redis service not running');
|
||||
}
|
||||
|
||||
const password = credentials.password || '';
|
||||
const dbIndex = credentials.db || '0';
|
||||
|
||||
// Read the export file
|
||||
const jsonPath = `${dataDir}/${backupResourceName}.json`;
|
||||
let exportContent: string;
|
||||
try {
|
||||
exportContent = await Deno.readTextFile(jsonPath);
|
||||
} catch {
|
||||
logger.warn(`Redis export file not found: ${jsonPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const exportData = JSON.parse(exportContent);
|
||||
const keys = exportData.keys || {};
|
||||
let importedCount = 0;
|
||||
|
||||
for (const [key, data] of Object.entries(keys) as Array<[string, { type: string; value: string; ttl: number }]>) {
|
||||
// Use RESTORE to import the serialized key
|
||||
const args = ['redis-cli', '-a', password, '-n', dbIndex, 'RESTORE', key, String(data.ttl * 1000), data.value, 'REPLACE'];
|
||||
|
||||
const result = await this.oneboxRef.docker.execInContainer(redisService.containerId, args);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
logger.warn(`Redis RESTORE failed for key '${key}': ${result.stderr.substring(0, 200)}`);
|
||||
} else {
|
||||
importedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.success(`Redis data imported: ${resource.resourceName} (${importedCount} keys)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tar archive from directory
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user