ante-erp-cli 1.9.12 → 1.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ante-cli.js +2 -3
- package/package.json +1 -1
- package/src/commands/backup.js +87 -39
- package/src/commands/restore.js +11 -32
package/bin/ante-cli.js
CHANGED
|
@@ -81,10 +81,9 @@ program
|
|
|
81
81
|
const backupCmd = program
|
|
82
82
|
.command('backup')
|
|
83
83
|
.alias('dump')
|
|
84
|
-
.description('Create backup of ANTE ERP')
|
|
84
|
+
.description('Create backup of ANTE ERP databases (PostgreSQL and MongoDB)')
|
|
85
85
|
.option('-o, --output <path>', 'Output file path')
|
|
86
|
-
.option('--
|
|
87
|
-
.option('--databases-only', 'Only backup databases')
|
|
86
|
+
.option('-k, --keyword <text>', 'Add keyword to backup filename for easier identification')
|
|
88
87
|
.action(backup);
|
|
89
88
|
|
|
90
89
|
backupCmd
|
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { readdirSync } from 'fs';
|
|
4
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import Table from 'cli-table3';
|
|
7
7
|
import { getInstallDir } from '../utils/config.js';
|
|
8
|
+
import { dumpDatabase, parseMongoUrl } from '../utils/mongodb.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Create backup of ANTE ERP
|
|
11
|
+
* Create backup of ANTE ERP databases (PostgreSQL and MongoDB)
|
|
11
12
|
*/
|
|
12
13
|
export async function backup(options) {
|
|
13
14
|
try {
|
|
14
15
|
const installDir = getInstallDir();
|
|
15
16
|
const composeFile = join(installDir, 'docker-compose.yml');
|
|
16
17
|
const backupDir = join(installDir, 'backups');
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// Generate backup filename with optional keyword
|
|
18
20
|
const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const keywordPart = options.keyword
|
|
22
|
+
? `-${options.keyword.replace(/[^a-zA-Z0-9-_]/g, '-')}`
|
|
23
|
+
: '';
|
|
24
|
+
const backupFile = options.output || join(backupDir, `ante-backup-${timestamp}${keywordPart}.tar.gz`);
|
|
25
|
+
|
|
26
|
+
const spinner = ora('Creating database backup...').start();
|
|
27
|
+
|
|
23
28
|
try {
|
|
24
29
|
// Create temporary backup directory
|
|
25
30
|
const tempDir = join(installDir, '.backup-temp');
|
|
26
31
|
await execa('mkdir', ['-p', tempDir]);
|
|
27
32
|
|
|
28
33
|
// Backup PostgreSQL - dump directly to file inside container
|
|
29
|
-
spinner.text = 'Backing up PostgreSQL...';
|
|
34
|
+
spinner.text = 'Backing up PostgreSQL database...';
|
|
30
35
|
await execa('docker', [
|
|
31
36
|
'compose',
|
|
32
37
|
'-f',
|
|
@@ -50,23 +55,64 @@ export async function backup(options) {
|
|
|
50
55
|
'ante-postgres:/tmp/postgres.dump',
|
|
51
56
|
join(tempDir, 'postgres.dump')
|
|
52
57
|
]);
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
|
|
59
|
+
// Backup MongoDB
|
|
60
|
+
spinner.text = 'Backing up MongoDB database...';
|
|
61
|
+
|
|
62
|
+
// Read MongoDB password from .env file
|
|
63
|
+
const envPath = join(installDir, '.env');
|
|
64
|
+
const envContent = readFileSync(envPath, 'utf8');
|
|
65
|
+
|
|
66
|
+
// Try to find MONGODB_URI first (development environment)
|
|
67
|
+
let mongoInfo;
|
|
68
|
+
const mongoUriMatch = envContent.match(/MONGODB_URI=(.+)/);
|
|
69
|
+
|
|
70
|
+
if (mongoUriMatch) {
|
|
71
|
+
// Development environment with full URI
|
|
72
|
+
const mongoUri = mongoUriMatch[1].trim();
|
|
73
|
+
mongoInfo = parseMongoUrl(mongoUri);
|
|
74
|
+
} else {
|
|
75
|
+
// CLI installation environment - construct URI from components
|
|
76
|
+
const mongoPasswordMatch = envContent.match(/MONGO_PASSWORD=(.+)/);
|
|
77
|
+
|
|
78
|
+
if (!mongoPasswordMatch) {
|
|
79
|
+
throw new Error('MONGO_PASSWORD not found in .env file');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const mongoPassword = mongoPasswordMatch[1].trim();
|
|
83
|
+
|
|
84
|
+
// Construct MongoDB connection info for CLI installation
|
|
85
|
+
mongoInfo = {
|
|
86
|
+
user: 'ante',
|
|
87
|
+
password: mongoPassword,
|
|
88
|
+
host: 'localhost',
|
|
89
|
+
port: '27017',
|
|
90
|
+
database: 'ante',
|
|
91
|
+
authDatabase: 'admin',
|
|
92
|
+
isSrv: false
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Create MongoDB dump in temporary directory
|
|
97
|
+
const mongoDumpTempDir = join(tempDir, 'mongodb-temp');
|
|
98
|
+
const mongoResult = await dumpDatabase(mongoInfo, mongoDumpTempDir);
|
|
99
|
+
|
|
100
|
+
if (!mongoResult.success) {
|
|
101
|
+
throw new Error(`MongoDB backup failed: ${mongoResult.error}`);
|
|
62
102
|
}
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
await execa('
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
|
|
104
|
+
// Move MongoDB dump to final structure (mongodb/ folder)
|
|
105
|
+
await execa('mv', [
|
|
106
|
+
join(mongoDumpTempDir, mongoInfo.database),
|
|
107
|
+
join(tempDir, 'mongodb')
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// Remove temporary MongoDB dump directory
|
|
111
|
+
await execa('rm', ['-rf', mongoDumpTempDir]);
|
|
112
|
+
|
|
68
113
|
// Create tar.gz archive
|
|
69
114
|
spinner.text = 'Creating compressed archive...';
|
|
115
|
+
await execa('mkdir', ['-p', backupDir]);
|
|
70
116
|
await execa('tar', [
|
|
71
117
|
'-czf',
|
|
72
118
|
backupFile,
|
|
@@ -74,21 +120,24 @@ export async function backup(options) {
|
|
|
74
120
|
tempDir,
|
|
75
121
|
'.'
|
|
76
122
|
]);
|
|
77
|
-
|
|
123
|
+
|
|
78
124
|
// Cleanup
|
|
79
125
|
await execa('rm', ['-rf', tempDir]);
|
|
80
|
-
|
|
81
|
-
spinner.succeed(
|
|
82
|
-
|
|
83
|
-
// Show backup
|
|
126
|
+
|
|
127
|
+
spinner.succeed(chalk.green('Database backup created successfully!'));
|
|
128
|
+
|
|
129
|
+
// Show backup details
|
|
84
130
|
const { stdout: size } = await execa('du', ['-h', backupFile]);
|
|
85
|
-
console.log(chalk.
|
|
86
|
-
|
|
131
|
+
console.log(chalk.white('\n✓ PostgreSQL database backed up'));
|
|
132
|
+
console.log(chalk.white(`✓ MongoDB database backed up (${mongoResult.collections} collections, ${mongoResult.size})`));
|
|
133
|
+
console.log(chalk.cyan(`\nBackup file: ${backupFile}`));
|
|
134
|
+
console.log(chalk.gray(`Total size: ${size.split('\t')[0]}\n`));
|
|
135
|
+
|
|
87
136
|
} catch (error) {
|
|
88
137
|
spinner.fail('Backup failed');
|
|
89
138
|
throw error;
|
|
90
139
|
}
|
|
91
|
-
|
|
140
|
+
|
|
92
141
|
} catch (error) {
|
|
93
142
|
console.error(chalk.red('Error:'), error.message);
|
|
94
143
|
process.exit(1);
|
|
@@ -102,42 +151,41 @@ export async function listBackups() {
|
|
|
102
151
|
try {
|
|
103
152
|
const installDir = getInstallDir();
|
|
104
153
|
const backupDir = join(installDir, 'backups');
|
|
105
|
-
|
|
154
|
+
|
|
106
155
|
console.log(chalk.bold('\n💾 Available Backups\n'));
|
|
107
|
-
|
|
156
|
+
|
|
108
157
|
const files = readdirSync(backupDir)
|
|
109
158
|
.filter(f => f.endsWith('.tar.gz'))
|
|
110
159
|
.sort()
|
|
111
160
|
.reverse();
|
|
112
|
-
|
|
161
|
+
|
|
113
162
|
if (files.length === 0) {
|
|
114
163
|
console.log(chalk.yellow('No backups found\n'));
|
|
115
164
|
return;
|
|
116
165
|
}
|
|
117
|
-
|
|
166
|
+
|
|
118
167
|
const table = new Table({
|
|
119
168
|
head: [chalk.cyan('Backup File'), chalk.cyan('Size'), chalk.cyan('Date')],
|
|
120
169
|
style: { head: [], border: ['gray'] }
|
|
121
170
|
});
|
|
122
|
-
|
|
171
|
+
|
|
123
172
|
for (const file of files) {
|
|
124
173
|
const filePath = join(backupDir, file);
|
|
125
174
|
const { stdout: size } = await execa('du', ['-h', filePath]);
|
|
126
175
|
const { stdout: date } = await execa('stat', ['-c', '%y', filePath]);
|
|
127
|
-
|
|
176
|
+
|
|
128
177
|
table.push([
|
|
129
178
|
file,
|
|
130
179
|
size.split('\t')[0],
|
|
131
180
|
date.split('.')[0]
|
|
132
181
|
]);
|
|
133
182
|
}
|
|
134
|
-
|
|
183
|
+
|
|
135
184
|
console.log(table.toString());
|
|
136
185
|
console.log();
|
|
137
|
-
|
|
186
|
+
|
|
138
187
|
} catch (error) {
|
|
139
188
|
console.error(chalk.red('Error:'), error.message);
|
|
140
189
|
process.exit(1);
|
|
141
190
|
}
|
|
142
191
|
}
|
|
143
|
-
|
package/src/commands/restore.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getInstallDir } from '../utils/config.js';
|
|
|
8
8
|
import { execInContainer, stopServices, startServices } from '../utils/docker.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Restore ANTE ERP from backup
|
|
11
|
+
* Restore ANTE ERP databases from backup (PostgreSQL and MongoDB only)
|
|
12
12
|
*/
|
|
13
13
|
export async function restore(backupFile, options = {}) {
|
|
14
14
|
try {
|
|
@@ -58,8 +58,8 @@ export async function restore(backupFile, options = {}) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Warning and confirmation
|
|
61
|
-
console.log(chalk.yellow('\n⚠ WARNING: This will restore
|
|
62
|
-
console.log(chalk.gray('Current data will be replaced with backup data'));
|
|
61
|
+
console.log(chalk.yellow('\n⚠ WARNING: This will restore databases from backup'));
|
|
62
|
+
console.log(chalk.gray('Current database data will be replaced with backup data'));
|
|
63
63
|
console.log(chalk.white('\nBackup file:'), chalk.cyan(basename(backupFile)));
|
|
64
64
|
|
|
65
65
|
if (!options.force) {
|
|
@@ -136,42 +136,23 @@ export async function restore(backupFile, options = {}) {
|
|
|
136
136
|
spinner.text = 'Restoring MongoDB database...';
|
|
137
137
|
const mongoDumpDir = join(tempDir, 'mongodb');
|
|
138
138
|
if (existsSync(mongoDumpDir)) {
|
|
139
|
+
// Copy MongoDB dump directory to container
|
|
140
|
+
// The backup structure is: mongodb/ contains the database files directly
|
|
139
141
|
await execa('docker', [
|
|
140
142
|
'cp',
|
|
141
143
|
mongoDumpDir,
|
|
142
|
-
'ante-mongodb:/tmp/
|
|
144
|
+
'ante-mongodb:/tmp/'
|
|
143
145
|
]);
|
|
144
146
|
|
|
147
|
+
// Restore from the mongodb directory (contains BSON files)
|
|
148
|
+
// Use --nsInclude to restore all collections from the dump
|
|
145
149
|
await execInContainer(
|
|
146
150
|
composeFile,
|
|
147
151
|
'mongodb',
|
|
148
|
-
['mongorestore', '--drop', '--
|
|
152
|
+
['mongorestore', '--drop', '--dir', '/tmp/mongodb', '--db', 'ante']
|
|
149
153
|
);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
// Restore uploaded files
|
|
153
|
-
spinner.text = 'Restoring uploaded files...';
|
|
154
|
-
const uploadsDir = join(tempDir, 'uploads');
|
|
155
|
-
if (existsSync(uploadsDir)) {
|
|
156
|
-
await execa('docker', [
|
|
157
|
-
'cp',
|
|
158
|
-
uploadsDir,
|
|
159
|
-
'ante-backend:/app/'
|
|
160
|
-
]);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Restore configuration files
|
|
164
|
-
spinner.text = 'Restoring configuration...';
|
|
165
|
-
const envBackup = join(tempDir, 'env.backup');
|
|
166
|
-
if (existsSync(envBackup)) {
|
|
167
|
-
await execa('cp', [envBackup, join(installDir, '.env')]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const composeBackup = join(tempDir, 'docker-compose.yml');
|
|
171
|
-
if (existsSync(composeBackup)) {
|
|
172
|
-
await execa('cp', [composeBackup, composeFile]);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
156
|
// Start all services
|
|
176
157
|
spinner.text = 'Starting all services...';
|
|
177
158
|
await startServices(composeFile);
|
|
@@ -180,15 +161,13 @@ export async function restore(backupFile, options = {}) {
|
|
|
180
161
|
spinner.text = 'Cleaning up...';
|
|
181
162
|
await execa('rm', ['-rf', tempDir]);
|
|
182
163
|
|
|
183
|
-
spinner.succeed(chalk.green('
|
|
164
|
+
spinner.succeed(chalk.green('Database restore completed successfully!'));
|
|
184
165
|
|
|
185
166
|
console.log(chalk.white('\n✓ PostgreSQL database restored'));
|
|
186
167
|
console.log(chalk.white('✓ MongoDB database restored'));
|
|
187
|
-
console.log(chalk.white('✓ Uploaded files restored'));
|
|
188
|
-
console.log(chalk.white('✓ Configuration restored'));
|
|
189
168
|
console.log(chalk.white('✓ All services started'));
|
|
190
169
|
|
|
191
|
-
console.log(chalk.cyan('\nANTE is now running with restored data'));
|
|
170
|
+
console.log(chalk.cyan('\nANTE is now running with restored database data'));
|
|
192
171
|
|
|
193
172
|
} catch (error) {
|
|
194
173
|
spinner.fail(chalk.red('Restore failed'));
|