fix: toDb() crash on import + add snapshot before import
- Fix: replace QueryComposer::toDb() (protected) with camelCaseToUnderscore
for table name resolution — was causing 500 on real import
- Feat: optional database snapshot (mysqldump) before import with UI checkbox
(checked by default). Snapshot saved to data/backups/snapshot-{timestamp}/
- Feat: skip entity types whose module is not installed (import resilience)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,14 @@
|
||||
<span class="text-muted"> — {{translate 'importNoFilesHint' category='messages' scope='DataMigration'}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="createSnapshot" checked>
|
||||
{{translate 'Create snapshot before import' scope='DataMigration'}}
|
||||
<span class="text-muted"> — {{translate 'snapshotHint' category='messages' scope='DataMigration'}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
|
||||
@@ -302,6 +302,7 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
|
||||
const preserveIds = this.$el.find('input[name="preserveIds"]').is(':checked');
|
||||
const skipDuplicates = this.$el.find('input[name="skipDuplicates"]').is(':checked');
|
||||
const noFiles = this.$el.find('input[name="importNoFiles"]').is(':checked');
|
||||
const createSnapshot = !dryRun && this.$el.find('input[name="createSnapshot"]').is(':checked');
|
||||
|
||||
const confirmMsg = dryRun
|
||||
? this.translate('dryRunConfirmation', 'messages', 'DataMigration')
|
||||
@@ -313,37 +314,72 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
|
||||
.addClass('disabled').attr('disabled', true);
|
||||
this.$el.find('.import-progress').show();
|
||||
|
||||
Espo.Ajax.postRequest('DataMigration/action/import', {
|
||||
backupName: this.selectedBackup,
|
||||
preserveIds: preserveIds,
|
||||
noFiles: noFiles,
|
||||
skipDuplicates: skipDuplicates,
|
||||
dryRun: dryRun,
|
||||
}, {timeout: 600000})
|
||||
.then(response => {
|
||||
this.isImporting = false;
|
||||
this.$el.find('[data-action="import"], [data-action="dryRunImport"]')
|
||||
.removeClass('disabled').attr('disabled', false);
|
||||
this.$el.find('.import-progress').hide();
|
||||
const runImport = () => {
|
||||
Espo.Ajax.postRequest('DataMigration/action/import', {
|
||||
backupName: this.selectedBackup,
|
||||
preserveIds: preserveIds,
|
||||
noFiles: noFiles,
|
||||
skipDuplicates: skipDuplicates,
|
||||
dryRun: dryRun,
|
||||
}, {timeout: 600000})
|
||||
.then(response => {
|
||||
this.isImporting = false;
|
||||
this.$el.find('[data-action="import"], [data-action="dryRunImport"]')
|
||||
.removeClass('disabled').attr('disabled', false);
|
||||
this.$el.find('.import-progress').hide();
|
||||
|
||||
this.importLog = response.log || [];
|
||||
this.renderLog('.import-log-container', this.importLog, !dryRun);
|
||||
this.importLog = response.log || [];
|
||||
this.renderLog('.import-log-container', this.importLog, !dryRun);
|
||||
|
||||
const label = dryRun ? 'Dry run completed' : 'Import completed';
|
||||
const label = dryRun ? 'Dry run completed' : 'Import completed';
|
||||
|
||||
Espo.Ui.success(this.translate(label, 'labels', 'DataMigration'));
|
||||
Espo.Ui.success(this.translate(label, 'labels', 'DataMigration'));
|
||||
|
||||
if (!dryRun) {
|
||||
this.loadStatus();
|
||||
}
|
||||
})
|
||||
.catch(xhr => {
|
||||
this.isImporting = false;
|
||||
this.$el.find('[data-action="import"], [data-action="dryRunImport"]')
|
||||
.removeClass('disabled').attr('disabled', false);
|
||||
this.$el.find('.import-progress').hide();
|
||||
Espo.Ui.error(this.translate('Import failed', 'labels', 'DataMigration'));
|
||||
});
|
||||
if (!dryRun) {
|
||||
this.loadStatus();
|
||||
}
|
||||
})
|
||||
.catch(xhr => {
|
||||
this.isImporting = false;
|
||||
this.$el.find('[data-action="import"], [data-action="dryRunImport"]')
|
||||
.removeClass('disabled').attr('disabled', false);
|
||||
this.$el.find('.import-progress').hide();
|
||||
Espo.Ui.error(this.translate('Import failed', 'labels', 'DataMigration'));
|
||||
});
|
||||
};
|
||||
|
||||
if (createSnapshot) {
|
||||
Espo.Ui.notify(this.translate('Creating snapshot...', 'labels', 'DataMigration'));
|
||||
|
||||
Espo.Ajax.postRequest('DataMigration/action/snapshot', {}, {timeout: 300000})
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
Espo.Ui.success(
|
||||
this.translate('Snapshot created', 'labels', 'DataMigration') +
|
||||
': ' + response.snapshotName + ' (' + response.dumpSizeFormatted + ')'
|
||||
);
|
||||
runImport();
|
||||
} else {
|
||||
this.isImporting = false;
|
||||
this.$el.find('[data-action="import"], [data-action="dryRunImport"]')
|
||||
.removeClass('disabled').attr('disabled', false);
|
||||
this.$el.find('.import-progress').hide();
|
||||
Espo.Ui.error(
|
||||
this.translate('Snapshot failed', 'labels', 'DataMigration') +
|
||||
': ' + (response.error || 'Unknown error')
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(xhr => {
|
||||
this.isImporting = false;
|
||||
this.$el.find('[data-action="import"], [data-action="dryRunImport"]')
|
||||
.removeClass('disabled').attr('disabled', false);
|
||||
this.$el.find('.import-progress').hide();
|
||||
Espo.Ui.error(this.translate('Snapshot failed', 'labels', 'DataMigration'));
|
||||
});
|
||||
} else {
|
||||
runImport();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user