1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 02:23:25 +00:00

Fix backup

This commit is contained in:
Hinton
2025-07-29 11:49:41 +02:00
parent 2de08377ee
commit 9d6a5e66c7
4 changed files with 534 additions and 18 deletions

View File

@@ -0,0 +1,230 @@
# Improved Backup System
## Overview
The migration tools now include an improved backup system that preserves full file paths, enabling safe rollback operations even for files in nested directory structures.
## Problem Solved
**Previous Issue**: The original backup system only stored filenames without paths:
```
backups/
├── component.html.backup # Lost path info!
├── template.html.backup # Could be from anywhere!
└── form.html.backup # No way to restore correctly!
```
**New Solution**: Path-preserving backup system:
```
backups/
├── path-mapping.json # Maps backup files to original paths
├── src_app_components_login.html.backup # Unique filename with path info
├── src_shared_templates_form.html.backup # No naming conflicts
└── libs_ui_components_button.html.backup # Safe restoration
```
## How It Works
### 1. Backup Creation
When creating backups, the system:
1. **Generates unique backup filenames** by replacing path separators with underscores:
```typescript
const relativePath = path.relative(process.cwd(), filePath);
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
```
2. **Creates a path mapping file** that tracks original locations:
```json
{
"src_app_login.html.backup": "/full/path/to/src/app/login.html",
"libs_ui_button.html.backup": "/full/path/to/libs/ui/button.html"
}
```
3. **Copies files to backup directory** with the unique names.
### 2. Backup Restoration
When restoring backups, the system:
1. **Reads the path mapping file** to get original locations
2. **Creates missing directories** if they don't exist
3. **Restores files to their exact original paths**
4. **Validates each restoration** before proceeding
## Usage Examples
### TypeScript Migration with Backup
```bash
# Create backups and migrate
npm run migrate -- --backup --output ./migration-reports
# If something goes wrong, rollback
npm run cli -- rollback --backup-dir ./migration-reports/backups
```
### Template Migration with Backup
```bash
# Create backups and migrate templates
npm run template-migrate -- --pattern "src/**/*.html" --backup --output ./reports
# Rollback if needed
npm run template-cli -- rollback --backup-dir ./reports/backups
```
## File Structure
### Backup Directory Structure
```
migration-reports/
└── backups/
├── path-mapping.json # Critical: Maps backup files to originals
├── src_app_login_login.component.html.backup
├── src_shared_ui_button.component.html.backup
├── libs_forms_input.component.html.backup
└── apps_web_dashboard_main.component.html.backup
```
### Path Mapping Format
```json
{
"src_app_login_login.component.html.backup": "/project/src/app/login/login.component.html",
"src_shared_ui_button.component.html.backup": "/project/src/shared/ui/button.component.html",
"libs_forms_input.component.html.backup": "/project/libs/forms/input.component.html"
}
```
## Safety Features
### 1. Path Validation
- Verifies path mapping file exists before restoration
- Warns about orphaned backup files without mappings
- Creates missing directories during restoration
### 2. Error Handling
- Graceful handling of missing mapping files
- Clear error messages for corrupted backups
- Verbose logging for troubleshooting
### 3. Backward Compatibility Detection
```bash
❌ Path mapping file not found. Cannot restore files safely.
This backup was created with an older version that doesn't preserve paths.
```
## Migration from Old Backup System
If you have backups created with the old system (without path mapping):
1. **Manual Restoration Required**: The old backups cannot be automatically restored
2. **Identify Original Locations**: You'll need to manually determine where files belong
3. **Create New Backups**: Re-run migrations with `--backup` to create proper backups
## Testing
The backup system includes comprehensive tests covering:
- Path preservation across nested directories
- Restoration accuracy
- Missing directory creation
- Error handling scenarios
- Orphaned file detection
Run tests:
```bash
npm test -- templates/backup-system.spec.ts
```
## Best Practices
### 1. Always Use Backups for Production
```bash
# Good: Creates backups before migration
npm run template-migrate -- --pattern "src/**/*.html" --backup
# Risky: No backup created
npm run template-migrate -- --pattern "src/**/*.html"
```
### 2. Verify Backup Creation
```bash
# Check that path-mapping.json exists
ls -la migration-reports/backups/path-mapping.json
# Verify backup count matches expected files
cat migration-reports/backups/path-mapping.json | jq 'keys | length'
```
### 3. Test Rollback on Small Set First
```bash
# Test rollback on a few files first
npm run template-migrate -- --file "src/app/test.html" --backup
npm run template-cli -- rollback --backup-dir ./migration-reports/backups
```
## Troubleshooting
### Issue: "Path mapping file not found"
**Cause**: Backup was created with old version or mapping file was deleted
**Solution**: Cannot auto-restore; manual restoration required
### Issue: "No mapping found for backup file"
**Cause**: Backup file exists but not in mapping (corrupted backup)
**Solution**: Check backup integrity; may need to recreate
### Issue: Restoration fails with permission errors
**Cause**: Insufficient permissions to create directories or write files
**Solution**: Check file permissions and disk space
## Implementation Details
### Filename Sanitization
```typescript
// Convert paths to safe filenames
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
// Examples:
// "src/app/login.html" → "src_app_login.html.backup"
// "libs\\ui\\button.html" → "libs_ui_button.html.backup"
```
### Directory Creation
```typescript
// Ensure target directory exists during restoration
const originalDir = path.dirname(originalPath);
if (!fs.existsSync(originalDir)) {
fs.mkdirSync(originalDir, { recursive: true });
}
```
### Path Mapping Storage
```typescript
// Save mapping as JSON for easy parsing
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
```
This improved backup system ensures that migration operations can be safely reversed, even in complex project structures with deeply nested files.

View File

@@ -0,0 +1,222 @@
import * as fs from "fs";
import * as path from "path";
describe("Backup System", () => {
const testDir = path.join(__dirname, "test-backup-system");
const backupDir = path.join(testDir, "backups");
const originalTemplate = `<div>
<h1>{{ 'title' | i18n }}</h1>
<p>{{ 'description' | i18n }}</p>
</div>`;
const modifiedTemplate = `<div>
<h1><span i18n="@@title">Title</span></h1>
<p><span i18n="@@description">Description</span></p>
</div>`;
beforeEach(() => {
// Create test directory structure
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true });
}
fs.mkdirSync(testDir, { recursive: true });
// Create nested directory structure to test path preservation
const nestedDir = path.join(testDir, "nested", "deep");
fs.mkdirSync(nestedDir, { recursive: true });
// Create test files
fs.writeFileSync(path.join(testDir, "test1.html"), originalTemplate);
fs.writeFileSync(path.join(nestedDir, "test2.html"), originalTemplate);
});
afterEach(() => {
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true });
}
});
describe("backup creation", () => {
it("should create backups with path mapping", () => {
const templateFiles = [
path.join(testDir, "test1.html"),
path.join(testDir, "nested", "deep", "test2.html"),
];
// Simulate the backup creation logic
const pathMapping: Record<string, string> = {};
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
for (const filePath of templateFiles) {
const relativePath = path.relative(process.cwd(), filePath);
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
const backupPath = path.join(backupDir, backupFileName);
fs.copyFileSync(filePath, backupPath);
pathMapping[backupFileName] = filePath;
}
// Save path mapping
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
// Verify backup files exist
expect(fs.existsSync(mappingPath)).toBe(true);
expect(Object.keys(pathMapping)).toHaveLength(2);
// Verify backup files contain original content
for (const [backupFileName, originalPath] of Object.entries(pathMapping)) {
const backupPath = path.join(backupDir, backupFileName);
expect(fs.existsSync(backupPath)).toBe(true);
const backupContent = fs.readFileSync(backupPath, "utf-8");
const originalContent = fs.readFileSync(originalPath, "utf-8");
expect(backupContent).toBe(originalContent);
}
});
it("should handle nested directory paths correctly", () => {
const filePath = path.join(testDir, "nested", "deep", "test2.html");
const relativePath = path.relative(process.cwd(), filePath);
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
// Should convert slashes/backslashes to underscores
expect(backupFileName).toContain("_");
expect(backupFileName).not.toContain("/");
expect(backupFileName).not.toContain("\\");
expect(backupFileName.endsWith(".backup")).toBe(true);
});
});
describe("backup restoration", () => {
it("should restore files to original locations", () => {
const templateFiles = [
path.join(testDir, "test1.html"),
path.join(testDir, "nested", "deep", "test2.html"),
];
// Create backups
const pathMapping: Record<string, string> = {};
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
for (const filePath of templateFiles) {
const relativePath = path.relative(process.cwd(), filePath);
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
const backupPath = path.join(backupDir, backupFileName);
fs.copyFileSync(filePath, backupPath);
pathMapping[backupFileName] = filePath;
}
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
// Modify original files
for (const filePath of templateFiles) {
fs.writeFileSync(filePath, modifiedTemplate);
}
// Verify files are modified
for (const filePath of templateFiles) {
const content = fs.readFileSync(filePath, "utf-8");
expect(content).toBe(modifiedTemplate);
}
// Restore from backups
const loadedMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
for (const backupFile of backupFiles) {
const backupPath = path.join(backupDir, backupFile);
const originalPath = loadedMapping[backupFile];
if (originalPath) {
// Ensure directory exists
const originalDir = path.dirname(originalPath);
if (!fs.existsSync(originalDir)) {
fs.mkdirSync(originalDir, { recursive: true });
}
fs.copyFileSync(backupPath, originalPath);
}
}
// Verify files are restored
for (const filePath of templateFiles) {
const content = fs.readFileSync(filePath, "utf-8");
expect(content).toBe(originalTemplate);
}
});
it("should handle missing directories during restoration", () => {
const filePath = path.join(testDir, "new", "nested", "path", "test.html");
const backupFileName = "test_new_nested_path_test.html.backup";
const pathMapping = { [backupFileName]: filePath };
// Create backup directory and mapping
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const backupPath = path.join(backupDir, backupFileName);
fs.writeFileSync(backupPath, originalTemplate);
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
// Restore (should create missing directories)
const originalDir = path.dirname(filePath);
if (!fs.existsSync(originalDir)) {
fs.mkdirSync(originalDir, { recursive: true });
}
fs.copyFileSync(backupPath, filePath);
// Verify file was restored and directories were created
expect(fs.existsSync(filePath)).toBe(true);
expect(fs.readFileSync(filePath, "utf-8")).toBe(originalTemplate);
});
});
describe("error handling", () => {
it("should handle missing path mapping file", () => {
// Create backup files without mapping
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
fs.writeFileSync(path.join(backupDir, "test.html.backup"), originalTemplate);
const mappingPath = path.join(backupDir, "path-mapping.json");
// Should detect missing mapping file
expect(fs.existsSync(mappingPath)).toBe(false);
});
it("should handle backup files without mapping entries", () => {
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
// Create backup file
const backupFileName = "orphaned.html.backup";
fs.writeFileSync(path.join(backupDir, backupFileName), originalTemplate);
// Create mapping without this file
const pathMapping = { "other.html.backup": "/some/other/path.html" };
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
const loadedMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
// Should not find mapping for orphaned file
expect(loadedMapping[backupFileName]).toBeUndefined();
});
});
});

View File

@@ -306,12 +306,23 @@ program
try {
console.log(chalk.blue("🔄 Rolling back template migration..."));
const backupDir = options.backup_dir;
const backupDir = options.backupDir;
if (!fs.existsSync(backupDir)) {
console.error(chalk.red(`❌ Backup directory not found: ${backupDir}`));
process.exit(1);
}
// Check for path mapping file
const mappingPath = path.join(backupDir, "path-mapping.json");
if (!fs.existsSync(mappingPath)) {
console.error(chalk.red("❌ Path mapping file not found. Cannot restore files safely."));
console.log(
chalk.gray("This backup was created with an older version that doesn't preserve paths."),
);
process.exit(1);
}
const pathMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
if (backupFiles.length === 0) {
@@ -322,15 +333,24 @@ program
let restoredCount = 0;
for (const backupFile of backupFiles) {
const backupPath = path.join(backupDir, backupFile);
const originalPath = backupFile.replace(".backup", "");
const originalPath = pathMapping[backupFile];
if (fs.existsSync(originalPath)) {
fs.copyFileSync(backupPath, originalPath);
restoredCount++;
if (!originalPath) {
console.warn(chalk.yellow(`⚠️ No mapping found for backup file: ${backupFile}`));
continue;
}
if (options.verbose) {
console.log(chalk.gray(`Restored: ${originalPath}`));
}
// Ensure the directory exists
const originalDir = path.dirname(originalPath);
if (!fs.existsSync(originalDir)) {
fs.mkdirSync(originalDir, { recursive: true });
}
fs.copyFileSync(backupPath, originalPath);
restoredCount++;
if (options.verbose) {
console.log(chalk.gray(`Restored: ${originalPath}`));
}
}
@@ -426,13 +446,25 @@ async function createBackups(templateFiles: string[], outputDir: string): Promis
fs.mkdirSync(backupDir, { recursive: true });
}
// Create a mapping file to track original paths
const pathMapping: Record<string, string> = {};
for (const filePath of templateFiles) {
if (fs.existsSync(filePath)) {
const backupPath = path.join(backupDir, path.basename(filePath) + ".backup");
// Create a unique backup filename that preserves path info
const relativePath = path.relative(process.cwd(), filePath);
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
const backupPath = path.join(backupDir, backupFileName);
fs.copyFileSync(filePath, backupPath);
pathMapping[backupFileName] = filePath;
}
}
// Save the path mapping for restoration
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
console.log(chalk.green(`📦 Created backups for ${templateFiles.length} files`));
}

View File

@@ -189,12 +189,23 @@ program
try {
console.log(chalk.blue("🔄 Rolling back migration..."));
const backupDir = options.backup_dir;
const backupDir = options.backupDir;
if (!fs.existsSync(backupDir)) {
console.error(chalk.red(`❌ Backup directory not found: ${backupDir}`));
process.exit(1);
}
// Check for path mapping file
const mappingPath = path.join(backupDir, "path-mapping.json");
if (!fs.existsSync(mappingPath)) {
console.error(chalk.red("❌ Path mapping file not found. Cannot restore files safely."));
console.log(
chalk.gray("This backup was created with an older version that doesn't preserve paths."),
);
process.exit(1);
}
const pathMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
if (backupFiles.length === 0) {
@@ -205,15 +216,24 @@ program
let restoredCount = 0;
for (const backupFile of backupFiles) {
const backupPath = path.join(backupDir, backupFile);
const originalPath = backupFile.replace(".backup", "");
const originalPath = pathMapping[backupFile];
if (fs.existsSync(originalPath)) {
fs.copyFileSync(backupPath, originalPath);
restoredCount++;
if (!originalPath) {
console.warn(chalk.yellow(`⚠️ No mapping found for backup file: ${backupFile}`));
continue;
}
if (options.verbose) {
console.log(chalk.gray(`Restored: ${originalPath}`));
}
// Ensure the directory exists
const originalDir = path.dirname(originalPath);
if (!fs.existsSync(originalDir)) {
fs.mkdirSync(originalDir, { recursive: true });
}
fs.copyFileSync(backupPath, originalPath);
restoredCount++;
if (options.verbose) {
console.log(chalk.gray(`Restored: ${originalPath}`));
}
}
@@ -234,13 +254,25 @@ async function createBackups(migrator: TypeScriptMigrator, outputDir: string): P
const usages = migrator.analyzeUsage();
const filesToBackup = new Set(usages.map((u) => u.filePath));
// Create a mapping file to track original paths
const pathMapping: Record<string, string> = {};
for (const filePath of filesToBackup) {
if (fs.existsSync(filePath)) {
const backupPath = path.join(backupDir, path.basename(filePath) + ".backup");
// Create a unique backup filename that preserves path info
const relativePath = path.relative(process.cwd(), filePath);
const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
const backupPath = path.join(backupDir, backupFileName);
fs.copyFileSync(filePath, backupPath);
pathMapping[backupFileName] = filePath;
}
}
// Save the path mapping for restoration
const mappingPath = path.join(backupDir, "path-mapping.json");
fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
console.log(chalk.green(`📦 Created backups for ${filesToBackup.size} files`));
}