Skip to content

Commit 07db2ab

Browse files
committed
Release 2.0.10
1 parent fbb2008 commit 07db2ab

File tree

5 files changed

+176
-35
lines changed

5 files changed

+176
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,4 @@ config/databases/commands/*
274274
config/databases/custom-commands
275275

276276
dist
277+
.cursor/

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mage-db-sync",
3-
"version": "2.0.9",
3+
"version": "2.0.10",
44
"description": "Database synchronizer for Magento, based on Magerun",
55
"license": "MIT",
66
"author": {

src/tasks/DownloadTask.ts

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ class DownloadTask {
302302
// Uses official magerun2 table groups: https://github.com/netz98/n98-magerun2
303303
const customStripParts: string[] = [];
304304
const keepOptions = config.settings.stripOptions || [];
305-
305+
306306
// Always strip these for development (safe to remove)
307307
customStripParts.push(
308308
'@log', // Log tables
@@ -312,41 +312,41 @@ class DownloadTask {
312312
'@replica', // Replica tables
313313
'@newrelic_reporting' // New Relic tables
314314
);
315-
315+
316316
// Strip based on what user wants to KEEP (unchecked = strip)
317317
if (!keepOptions.includes('customers')) {
318318
customStripParts.push('@customers');
319319
}
320-
320+
321321
if (!keepOptions.includes('admin')) {
322322
customStripParts.push('@admin', '@oauth', '@2fa');
323323
}
324-
324+
325325
if (!keepOptions.includes('sales')) {
326326
customStripParts.push('@sales');
327327
}
328-
328+
329329
if (!keepOptions.includes('quotes')) {
330330
customStripParts.push('@quotes');
331331
}
332-
332+
333333
if (!keepOptions.includes('search')) {
334334
customStripParts.push('@search', '@idx');
335335
}
336-
336+
337337
if (!keepOptions.includes('dotmailer')) {
338338
customStripParts.push('@dotmailer', '@mailchimp');
339339
}
340-
340+
341341
// Note: We don't strip config even if unchecked, as it would break the database
342342
// Configuration data (core_config_data) is not in any strip group - it's always kept
343343
if (!keepOptions.includes('config')) {
344344
logger.warn('Configuration settings are always kept as they are required for Magento to function');
345345
}
346-
346+
347347
const customStripString = customStripParts.join(' ');
348348
stripOptions = customStripString ? `--strip="${customStripString}"` : '';
349-
349+
350350
logger.info('Using custom strip configuration', {
351351
keepOptions,
352352
stripGroups: customStripParts,
@@ -357,26 +357,30 @@ class DownloadTask {
357357
const keepCustomerOptions = staticSettings.settings?.databaseStripKeepCustomerData || '';
358358
stripOptions = keepCustomerOptions ? `--strip="${keepCustomerOptions}"` : '';
359359
} else if (config.settings.strip === 'full and human readable') {
360-
const staticSettings = this.services.getConfig().getStaticSettings();
361-
const fullStripOptions = staticSettings.settings?.databaseStripDevelopment || '';
362-
stripOptions = fullStripOptions ? `--strip="${fullStripOptions}"` : '';
360+
// FULL dump with human-readable format - NO stripping
361+
stripOptions = '';
363362
humanReadable = '--human-readable';
363+
logger.info('Using full database dump with human-readable format (no stripping)');
364364
} else if (config.settings.strip === 'full') {
365-
const staticSettings = this.services.getConfig().getStaticSettings();
366-
const fullStripOptions = staticSettings.settings?.databaseStripDevelopment || '';
367-
stripOptions = fullStripOptions ? `--strip="${fullStripOptions}"` : '';
365+
// FULL dump - NO stripping
366+
stripOptions = '';
367+
logger.info('Using full database dump (no stripping)');
368368
} else {
369+
// Default: apply development strip options
369370
const staticSettings = this.services.getConfig().getStaticSettings();
370371
const developmentStripOptions = staticSettings.settings?.databaseStripDevelopment || '';
371372
stripOptions = developmentStripOptions ? `--strip="${developmentStripOptions}"` : '';
372373
}
373374

375+
// Escape filename for shell usage
376+
const escapedFileName = shellEscape(databaseFileName);
377+
374378
// Build compression command based on what's available
375379
if (compression.type === 'gzip') {
376-
dumpCommand = `db:dump --stdout -n --no-tablespaces ${humanReadable} ${stripOptions} | gzip ${compression.level} > ${databaseFileName}`;
380+
dumpCommand = `db:dump --stdout -n --no-tablespaces ${humanReadable} ${stripOptions} | gzip ${compression.level} > ${escapedFileName}`;
377381
} else {
378382
// No compression - just dump to file
379-
dumpCommand = `db:dump --stdout -n --no-tablespaces ${humanReadable} ${stripOptions} > ${databaseFileName}`;
383+
dumpCommand = `db:dump --stdout -n --no-tablespaces ${humanReadable} ${stripOptions} > ${escapedFileName}`;
380384
}
381385

382386
logger.info('Using compression for database dump', {
@@ -387,27 +391,80 @@ class DownloadTask {
387391
});
388392

389393
const fullCommand = sshMagentoRootFolderMagerunCommand(
390-
`${dumpCommand}; mv ${databaseFileName} ~`,
394+
`${dumpCommand}; mv ${escapedFileName} ~`,
391395
config
392396
);
393397

394-
task.output = 'Dumping database (this may take a minute)...';
398+
task.output = 'Starting database dump...';
395399
logger.info('Starting database dump', {
396400
database: config.serverVariables.databaseName,
397401
stripType
398402
});
399403

400-
await ssh.execCommand(fullCommand).then(function (result: any) {
404+
// Start the dump command (non-blocking)
405+
const dumpPromise = ssh.execCommand(fullCommand);
406+
407+
// Monitor file size in real-time
408+
const startTime = Date.now();
409+
let lastSize = 0;
410+
let lastSizeTime = Date.now();
411+
412+
const sizeCheckInterval = setInterval(async () => {
413+
try {
414+
// Check file size on server (in Magento root first, then home)
415+
const sizeCommand = sshMagentoRootFolderMagerunCommand(
416+
`stat -f%z ${escapedFileName} 2>/dev/null || stat -c%s ${escapedFileName} 2>/dev/null || echo "0"`,
417+
config
418+
);
419+
420+
const sizeResult = await ssh.execCommand(sizeCommand);
421+
const currentSize = parseInt(sizeResult.stdout.trim() || '0');
422+
423+
if (currentSize > 0) {
424+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
425+
const elapsedMinutes = Math.floor(elapsed / 60);
426+
const elapsedSeconds = elapsed % 60;
427+
const timeStr = elapsedMinutes > 0
428+
? `${elapsedMinutes}m ${elapsedSeconds}s`
429+
: `${elapsedSeconds}s`;
430+
431+
// Calculate speed since last check
432+
const timeDiff = (Date.now() - lastSizeTime) / 1000;
433+
const sizeDiff = currentSize - lastSize;
434+
const speed = timeDiff > 0 ? sizeDiff / timeDiff : 0;
435+
436+
lastSize = currentSize;
437+
lastSizeTime = Date.now();
438+
439+
const sizeStr = ProgressDisplay.formatBytes(currentSize);
440+
const speedStr = speed > 0 ? ` ${chalk.cyan('~' + ProgressDisplay.formatSpeed(speed))}` : '';
441+
442+
task.output = `Dumping database... ${chalk.bold.cyan(sizeStr)}${speedStr} ${chalk.gray(`(${timeStr} elapsed)`)}`;
443+
}
444+
} catch (err) {
445+
// Ignore errors during size check (file might not exist yet)
446+
}
447+
}, 2000); // Check every 2 seconds
448+
449+
await dumpPromise.then(function (result: any) {
450+
clearInterval(sizeCheckInterval);
451+
401452
if (result.code && result.code !== 0) {
402453
throw new Error(
403454
`Database dump failed\n[TIP] Check database permissions and disk space\nError: ${result.stderr}`
404455
);
405456
}
406-
task.output = '✓ Database dump completed';
457+
458+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
459+
const finalSizeStr = lastSize > 0 ? ` (${ProgressDisplay.formatBytes(lastSize)})` : '';
460+
task.output = `✓ Database dump completed${finalSizeStr} in ${elapsed}s`;
461+
}).catch((err: Error) => {
462+
clearInterval(sizeCheckInterval);
463+
throw err;
407464
});
408465

409466
const duration = PerformanceMonitor.end('database-dump');
410-
logger.info('Database dump complete', { duration });
467+
logger.info('Database dump complete', { duration, finalSize: lastSize });
411468
task.title = `Dumped database`;
412469
}
413470
});

src/tasks/ImportTask.ts

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { localhostMagentoRootExec } from '../utils/Console';
1+
import { localhostMagentoRootExec, shellEscape } from '../utils/Console';
22
import { Listr } from 'listr2';
33
import { ServiceContainer } from '../core/ServiceContainer';
44
import { ProgressDisplay } from '../utils/ProgressDisplay';
@@ -45,8 +45,10 @@ class ImportTask {
4545
compressionType
4646
});
4747

48-
// Estimate import duration (~10MB/s for compressed, ~5MB/s for uncompressed)
49-
const speed = isCompressed ? 10 * 1024 * 1024 : 5 * 1024 * 1024;
48+
// More conservative speed estimates based on database write operations
49+
// Compressed: ~3-5MB/s (decompression + write overhead)
50+
// Uncompressed: ~2-3MB/s (raw SQL parsing and execution)
51+
const speed = isCompressed ? 4 * 1024 * 1024 : 2.5 * 1024 * 1024;
5052
const estimatedDuration = sqlFileSize > 0 ? (sqlFileSize / speed) * 1000 : 60000;
5153

5254
// Show file info
@@ -57,10 +59,26 @@ class ImportTask {
5759
// Start progress estimation
5860
let progressInterval: NodeJS.Timeout;
5961
let lastPercentage = 0;
62+
let isInFinalPhase = false;
6063

6164
progressInterval = setInterval(() => {
6265
const elapsed = Date.now() - startTime;
63-
let percentage = Math.min(95, Math.round((elapsed / estimatedDuration) * 100));
66+
let percentage = Math.round((elapsed / estimatedDuration) * 100);
67+
68+
// More realistic progress stages:
69+
// 0-80%: Normal progression based on estimate
70+
// 80-90%: Slow down (database operations are slower near end)
71+
// 90%+: Final phase - show elapsed time instead
72+
if (percentage <= 80) {
73+
// Normal progression
74+
} else if (percentage <= 90) {
75+
// Slow down between 80-90%
76+
percentage = 80 + Math.min(10, (percentage - 80) * 0.5);
77+
} else {
78+
// After 90%, don't show percentage, show elapsed time
79+
percentage = 90;
80+
isInFinalPhase = true;
81+
}
6482

6583
// Avoid going backwards
6684
if (percentage > lastPercentage) {
@@ -73,8 +91,18 @@ class ImportTask {
7391
const estimatedBytes = Math.min(sqlFileSize, (percentage / 100) * sqlFileSize);
7492
const avgSpeed = elapsed > 0 ? (estimatedBytes / (elapsed / 1000)) : 0;
7593

76-
const statusText = percentage >= 90 ? chalk.yellow('(finishing...)') : '';
77-
task.output = `${progressBar} ${chalk.bold.cyan(percentage + '%')} ${chalk.gray(ProgressDisplay.formatBytes(estimatedBytes) + ' / ' + sizeInfo)}${compressionInfo} ${chalk.cyan('~' + ProgressDisplay.formatSpeed(avgSpeed))} ${statusText}`;
94+
// Show different status based on phase
95+
let statusText = '';
96+
if (isInFinalPhase) {
97+
const elapsedSeconds = Math.floor(elapsed / 1000);
98+
statusText = chalk.yellow(`(finalizing database... ${elapsedSeconds}s elapsed)`);
99+
task.output = `${progressBar} ${chalk.bold.cyan(percentage + '%')} ${statusText}`;
100+
} else if (percentage >= 80) {
101+
statusText = chalk.yellow('(processing indexes and constraints...)');
102+
task.output = `${progressBar} ${chalk.bold.cyan(percentage + '%')} ${chalk.gray(ProgressDisplay.formatBytes(estimatedBytes) + ' / ' + sizeInfo)}${compressionInfo} ${chalk.cyan('~' + ProgressDisplay.formatSpeed(avgSpeed))} ${statusText}`;
103+
} else {
104+
task.output = `${progressBar} ${chalk.bold.cyan(percentage + '%')} ${chalk.gray(ProgressDisplay.formatBytes(estimatedBytes) + ' / ' + sizeInfo)}${compressionInfo} ${chalk.cyan('~' + ProgressDisplay.formatSpeed(avgSpeed))}`;
105+
}
78106
}, 1000);
79107

80108
try {
@@ -96,8 +124,8 @@ class ImportTask {
96124

97125
importCommand += ` --drop` + // Drop and recreate database
98126
` --force` + // Continue on SQL errors
99-
` --skip-authorization-entry-creation` + // We'll add them later
100-
` -q`; // Quiet mode
127+
` --skip-authorization-entry-creation`; // We'll add them later
128+
// NOTE: Removed -q flag to allow magerun2's progress output
101129

102130
logger.info('Executing magerun2 db:import', {
103131
command: importCommand,
@@ -106,7 +134,62 @@ class ImportTask {
106134
hasOptimize: !isCompressed // Only optimize uncompressed files
107135
});
108136

109-
await localhostMagentoRootExec(importCommand, config);
137+
// Execute with streaming output to capture real progress
138+
const { spawn } = require('child_process');
139+
const escapedFolder = shellEscape(config.settings.currentFolder);
140+
141+
await new Promise<void>((resolve, reject) => {
142+
// Use shell to execute the cd + command
143+
const proc = spawn('sh', ['-c', `cd ${escapedFolder} && ${importCommand}`]);
144+
145+
let lastOutput = '';
146+
147+
// Capture stdout for progress
148+
proc.stdout.on('data', (data: Buffer) => {
149+
const output = data.toString();
150+
151+
// Check if magerun2 is showing progress (it uses \r for updates)
152+
if (output.includes('\r') || output.includes('%')) {
153+
clearInterval(progressInterval);
154+
155+
// Parse magerun2's progress output
156+
// magerun2 typically outputs: "Importing... XX%" or progress bars
157+
const lines = output.split('\r').filter(l => l.trim());
158+
if (lines.length > 0) {
159+
lastOutput = lines[lines.length - 1].trim();
160+
161+
// Try to extract percentage from magerun2 output
162+
const percentMatch = lastOutput.match(/(\d+)%/);
163+
if (percentMatch) {
164+
const realPercentage = parseInt(percentMatch[1]);
165+
const progressBar = EnhancedProgress.createProgressBar(realPercentage, 20);
166+
task.output = `${progressBar} ${chalk.bold.cyan(realPercentage + '%')} ${chalk.gray(sizeInfo)}${compressionInfo} ${chalk.green('[magerun2]')}`;
167+
} else {
168+
// Show raw magerun2 output if no percentage found
169+
task.output = lastOutput;
170+
}
171+
}
172+
}
173+
});
174+
175+
// Capture stderr (magerun2 might output there too)
176+
proc.stderr.on('data', (data: Buffer) => {
177+
const output = data.toString();
178+
logger.debug('magerun2 stderr', { output });
179+
});
180+
181+
proc.on('close', (code: number) => {
182+
if (code === 0) {
183+
resolve();
184+
} else {
185+
reject(new Error(`magerun2 db:import failed with code ${code}`));
186+
}
187+
});
188+
189+
proc.on('error', (err: Error) => {
190+
reject(err);
191+
});
192+
});
110193

111194
clearInterval(progressInterval);
112195

0 commit comments

Comments
 (0)