feat(appstore): add remote app store templates with service upgrades and Redis/MariaDB platform support

This commit is contained in:
2026-03-21 19:36:25 +00:00
parent c0f9f979c7
commit 22f34e7de5
35 changed files with 2496 additions and 254 deletions

View File

@@ -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
*/