diff --git a/api/src/unraid-api/unraid-file-modifier/file-modification.ts b/api/src/unraid-api/unraid-file-modifier/file-modification.ts index f2c813be9..6dc347891 100644 --- a/api/src/unraid-api/unraid-file-modifier/file-modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/file-modification.ts @@ -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 { diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/logrotate.conf b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/logrotate.conf deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts index a47db561f..90be0aa13 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts @@ -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); } diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts index 6bcb3e097..dab033df2 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts @@ -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, diff --git a/api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.spec.ts b/api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.spec.ts index cc21f43e0..3270d9642 100644 --- a/api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.spec.ts +++ b/api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.spec.ts @@ -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); });