feat: allow deletion and creation of files with patches

This commit is contained in:
Eli Bosley
2025-02-04 13:40:11 -05:00
parent 2fce2e9a28
commit 1392bdeecb
5 changed files with 30 additions and 7 deletions

View File

@@ -92,11 +92,12 @@ export abstract class FileModification {
if (!patchContents.trim()) {
throw new Error('Patch contents are empty');
}
const currentContent = await readFile(this.filePath, 'utf8');
const currentContent = await readFile(this.filePath, 'utf8').catch(() => '');
const parsedPatch = parsePatch(patchContents)[0];
if (!parsedPatch?.hunks.length) {
throw new Error('Invalid Patch Format: No hunks found');
}
const results = applyPatch(currentContent, parsedPatch);
if (results === false) {
throw new Error(`Failed to apply patch to ${this.filePath}`);
@@ -178,7 +179,12 @@ export abstract class FileModification {
throw new Error(`Failed to rollback patch from ${this.filePath}`);
}
await writeFile(this.filePath, results);
if (results === '') {
// Delete the file if the patch results in an empty string
await unlink(this.filePath);
} else {
await writeFile(this.filePath, results);
}
// Clean up the patch file after successful rollback
try {

View File

@@ -74,7 +74,7 @@ const downloadOrRetrieveOriginalFile = async (filePath: string, fileUrl: string)
);
}
}
return await readFile(filePath, 'utf-8');
return await readFile(filePath, 'utf-8').catch(() => '');
};
async function testModification(testCase: ModificationTestCase, patcher: FileModification) {
@@ -101,7 +101,7 @@ async function testModification(testCase: ModificationTestCase, patcher: FileMod
// Rollback and verify original state
await patcher.rollback();
const revertedContent = await readFile(filePath, 'utf-8');
const revertedContent = await readFile(filePath, 'utf-8').catch(() => '');
await expect(revertedContent).toMatch(originalContent);
}

View File

@@ -1,9 +1,6 @@
import { Logger } from '@nestjs/common';
import { readFile } from 'node:fs/promises';
import { createPatch } from 'diff';
import { execa } from 'execa';
import { fileExists } from '@app/core/utils/files/file-exists';
import {
FileModification,

View File

@@ -11,6 +11,7 @@ import {
ShouldApplyWithReason,
} from '@app/unraid-api/unraid-file-modifier/file-modification';
import { UnraidFileModificationService } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.service';
import { fileExistsSync } from '@app/core/utils/files/file-exists';
const FIXTURE_PATH = join(__dirname, 'modifications', '__test__', '__fixtures__', 'text-patch-file.txt');
const ORIGINAL_CONTENT = 'original';
@@ -80,6 +81,25 @@ describe.sequential('FileModificationService', () => {
await expect(service.applyModification(mod)).resolves.toBe(undefined);
});
it('should apply modification if file does not exist', async () => {
const mod = new TestFileModification(logger);
// @ts-expect-error - This is a protected method, but we need to mock it
mod.generatePatch = vi.fn().mockResolvedValue(createPatch(FIXTURE_PATH, '', 'modified'));
await fs.unlink(FIXTURE_PATH);
await expect(service.applyModification(mod)).resolves.toBe(undefined);
expect(mockLogger.warn).toHaveBeenCalledWith('Could not load pregenerated patch for: test');
expect(mockLogger.log).toHaveBeenCalledWith(
'Applying modification: test - Always Apply this mod'
);
expect(mockLogger.log).toHaveBeenCalledWith('Modification applied successfully: test');
const content = await fs.readFile(FIXTURE_PATH, 'utf-8');
expect(content).toBe('modified');
await service.rollbackAll();
expect(fileExistsSync(FIXTURE_PATH)).toBe(false);
expect(mockLogger.log).toHaveBeenCalledWith('Rolling back modification: test');
expect(mockLogger.log).toHaveBeenCalledWith('Successfully rolled back modification: test');
});
it('should not rollback any mods without loaded', async () => {
await expect(service.rollbackAll()).resolves.toBe(undefined);
});