瀏覽代碼

Merge pull request #161 from meienberger/fix/cleanup-before-install

feat: cleanup folder before install
Nicolas Meienberger 2 年之前
父節點
當前提交
73d369442a

+ 2 - 3
.github/workflows/ci.yml

@@ -65,7 +65,6 @@ jobs:
       - name: Run tests
       - name: Run tests
         run: pnpm -r test
         run: pnpm -r test
 
 
-      - uses: codecov/codecov-action@v2
+      - uses: codecov/codecov-action@v3
         with:
         with:
-          token: ${{ secrets.CODECOV_TOKEN }}
-          files: ./packages/system-api/coverage/clover.xml,./packages/dashboard/coverage/clover.xml
+          token: ${{ secrets.CODECOV_TOKEN }}

+ 19 - 5
packages/system-api/__mocks__/fs-extra.ts

@@ -1,6 +1,7 @@
 import path from 'path';
 import path from 'path';
 const fs: {
 const fs: {
   __createMockFiles: typeof createMockFiles;
   __createMockFiles: typeof createMockFiles;
+  __resetAllMocks: typeof resetAllMocks;
   readFileSync: typeof readFileSync;
   readFileSync: typeof readFileSync;
   existsSync: typeof existsSync;
   existsSync: typeof existsSync;
   writeFileSync: typeof writeFileSync;
   writeFileSync: typeof writeFileSync;
@@ -9,6 +10,7 @@ const fs: {
   readdirSync: typeof readdirSync;
   readdirSync: typeof readdirSync;
   copyFileSync: typeof copyFileSync;
   copyFileSync: typeof copyFileSync;
   copySync: typeof copyFileSync;
   copySync: typeof copyFileSync;
+  createFileSync: typeof createFileSync;
 } = jest.genMockFromModule('fs-extra');
 } = jest.genMockFromModule('fs-extra');
 
 
 let mockFiles = Object.create(null);
 let mockFiles = Object.create(null);
@@ -45,12 +47,14 @@ const mkdirSync = (p: string) => {
   mockFiles[p] = Object.create(null);
   mockFiles[p] = Object.create(null);
 };
 };
 
 
-const rmSync = (p: string, options: { recursive: boolean }) => {
-  if (options.recursive) {
-    delete mockFiles[p];
-  } else {
-    delete mockFiles[p][Object.keys(mockFiles[p])[0]];
+const rmSync = (p: string) => {
+  if (mockFiles[p] instanceof Array) {
+    mockFiles[p].forEach((file: string) => {
+      delete mockFiles[path.join(p, file)];
+    });
   }
   }
+
+  delete mockFiles[p];
 };
 };
 
 
 const readdirSync = (p: string) => {
 const readdirSync = (p: string) => {
@@ -85,6 +89,14 @@ const copySync = (source: string, destination: string) => {
   }
   }
 };
 };
 
 
+const createFileSync = (p: string) => {
+  mockFiles[p] = '';
+};
+
+const resetAllMocks = () => {
+  mockFiles = Object.create(null);
+};
+
 fs.readdirSync = readdirSync;
 fs.readdirSync = readdirSync;
 fs.existsSync = existsSync;
 fs.existsSync = existsSync;
 fs.readFileSync = readFileSync;
 fs.readFileSync = readFileSync;
@@ -93,6 +105,8 @@ fs.mkdirSync = mkdirSync;
 fs.rmSync = rmSync;
 fs.rmSync = rmSync;
 fs.copyFileSync = copyFileSync;
 fs.copyFileSync = copyFileSync;
 fs.copySync = copySync;
 fs.copySync = copySync;
+fs.createFileSync = createFileSync;
 fs.__createMockFiles = createMockFiles;
 fs.__createMockFiles = createMockFiles;
+fs.__resetAllMocks = resetAllMocks;
 
 
 module.exports = fs;
 module.exports = fs;

+ 1 - 0
packages/system-api/src/modules/apps/__tests__/apps.factory.ts

@@ -54,6 +54,7 @@ const createApp = async (props: IProps) => {
   MockFiles[`${config.ROOT_FOLDER}/.env`] = 'TEST=test';
   MockFiles[`${config.ROOT_FOLDER}/.env`] = 'TEST=test';
   MockFiles[`${config.ROOT_FOLDER}/repos/repo-id`] = '';
   MockFiles[`${config.ROOT_FOLDER}/repos/repo-id`] = '';
   MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
   MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
+  MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
   MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
   MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
 
 
   if (installed) {
   if (installed) {

+ 26 - 0
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -109,6 +109,32 @@ describe('Install app', () => {
     expect(envMap.get('RANDOM_FIELD')).toBeDefined();
     expect(envMap.get('RANDOM_FIELD')).toBeDefined();
     expect(envMap.get('RANDOM_FIELD')).toHaveLength(32);
     expect(envMap.get('RANDOM_FIELD')).toHaveLength(32);
   });
   });
+
+  it('Should correctly copy app from repos to apps folder', async () => {
+    await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
+    const appFolder = fs.readdirSync(`${config.ROOT_FOLDER}/apps/${app1.id}`);
+
+    expect(appFolder).toBeDefined();
+    expect(appFolder.indexOf('docker-compose.yml')).toBeGreaterThanOrEqual(0);
+  });
+
+  it('Should cleanup any app folder existing before install', async () => {
+    const { MockFiles, appInfo } = await createApp({});
+    app1 = appInfo;
+    MockFiles[`/tipi/apps/${appInfo.id}/docker-compose.yml`] = 'test';
+    MockFiles[`/tipi/apps/${appInfo.id}/test.yml`] = 'test';
+    MockFiles[`/tipi/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
+
+    // @ts-ignore
+    fs.__createMockFiles(MockFiles);
+
+    expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(true);
+
+    await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
+
+    expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(false);
+    expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/docker-compose.yml`)).toBe(true);
+  });
 });
 });
 
 
 describe('Uninstall app', () => {
 describe('Uninstall app', () => {

+ 1 - 1
packages/system-api/src/modules/apps/apps.service.ts

@@ -65,7 +65,7 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
   if (app) {
   if (app) {
     await startApp(id);
     await startApp(id);
   } else {
   } else {
-    ensureAppFolder(id);
+    ensureAppFolder(id, true);
     const appIsValid = await checkAppRequirements(id);
     const appIsValid = await checkAppRequirements(id);
 
 
     if (!appIsValid) {
     if (!appIsValid) {

+ 1 - 0
packages/system-api/src/modules/apps/apps.types.ts

@@ -15,6 +15,7 @@ export enum AppCategoriesEnum {
   DATA = 'data',
   DATA = 'data',
   MUSIC = 'music',
   MUSIC = 'music',
   FINANCE = 'finance',
   FINANCE = 'finance',
+  GAMING = 'gaming',
 }
 }
 
 
 export enum FieldTypes {
 export enum FieldTypes {

+ 200 - 0
packages/system-api/src/modules/fs/__tests__/fs.helpers.test.ts

@@ -0,0 +1,200 @@
+import childProcess from 'child_process';
+import config from '../../../config';
+import { getAbsolutePath, readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, runScript, getSeed, ensureAppFolder } from '../fs.helpers';
+import fs from 'fs-extra';
+
+jest.mock('fs-extra');
+
+beforeEach(() => {
+  // @ts-ignore
+  fs.__resetAllMocks();
+});
+
+describe('Test: getAbsolutePath', () => {
+  it('should return the absolute path', () => {
+    expect(getAbsolutePath('/test')).toBe(`${config.ROOT_FOLDER}/test`);
+  });
+});
+
+describe('Test: readJsonFile', () => {
+  it('should return the json file', () => {
+    // Arrange
+    const rawFile = '{"test": "test"}';
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/test-file.json`]: rawFile,
+    };
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    // Act
+    const file = readJsonFile('/test-file.json');
+
+    // Assert
+    expect(file).toEqual({ test: 'test' });
+  });
+
+  it('should return null if the file does not exist', () => {
+    expect(readJsonFile('/test')).toBeNull();
+  });
+});
+
+describe('Test: readFile', () => {
+  it('should return the file', () => {
+    const rawFile = 'test';
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/test-file.txt`]: rawFile,
+    };
+
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    expect(readFile('/test-file.txt')).toEqual('test');
+  });
+
+  it('should return empty string if the file does not exist', () => {
+    expect(readFile('/test')).toEqual('');
+  });
+});
+
+describe('Test: readdirSync', () => {
+  it('should return the files', () => {
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/test/test-file.txt`]: 'test',
+    };
+
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    expect(readdirSync('/test')).toEqual(['test-file.txt']);
+  });
+
+  it('should return empty array if the directory does not exist', () => {
+    expect(readdirSync('/test')).toEqual([]);
+  });
+});
+
+describe('Test: fileExists', () => {
+  it('should return true if the file exists', () => {
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/test-file.txt`]: 'test',
+    };
+
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    expect(fileExists('/test-file.txt')).toBeTruthy();
+  });
+
+  it('should return false if the file does not exist', () => {
+    expect(fileExists('/test-file.txt')).toBeFalsy();
+  });
+});
+
+describe('Test: writeFile', () => {
+  it('should write the file', () => {
+    const spy = jest.spyOn(fs, 'writeFileSync');
+
+    writeFile('/test-file.txt', 'test');
+
+    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test-file.txt`, 'test');
+  });
+});
+
+describe('Test: createFolder', () => {
+  it('should create the folder', () => {
+    const spy = jest.spyOn(fs, 'mkdirSync');
+
+    createFolder('/test');
+
+    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`);
+  });
+});
+
+describe('Test: deleteFolder', () => {
+  it('should delete the folder', () => {
+    const spy = jest.spyOn(fs, 'rmSync');
+
+    deleteFolder('/test');
+
+    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`, { recursive: true });
+  });
+});
+
+describe('Test: runScript', () => {
+  it('should run the script', () => {
+    const spy = jest.spyOn(childProcess, 'execFile');
+    const callback = jest.fn();
+
+    runScript('/test', [], callback);
+
+    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`, [], {}, callback);
+  });
+});
+
+describe('Test: getSeed', () => {
+  it('should return the seed', () => {
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/state/seed`]: 'test',
+    };
+
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    expect(getSeed()).toEqual('test');
+  });
+});
+
+describe('Test: ensureAppFolder', () => {
+  beforeEach(() => {
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
+    };
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+  });
+
+  it('should copy the folder from repo', () => {
+    // Act
+    ensureAppFolder('test');
+
+    // Assert
+    const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
+    expect(files).toEqual(['test.yml']);
+  });
+
+  it('should not copy the folder if it already exists', () => {
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
+      [`${config.ROOT_FOLDER}/apps/test`]: ['docker-compose.yml'],
+      [`${config.ROOT_FOLDER}/apps/test/docker-compose.yml`]: 'test',
+    };
+
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    // Act
+    ensureAppFolder('test');
+
+    // Assert
+    const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
+    expect(files).toEqual(['docker-compose.yml']);
+  });
+
+  it('Should overwrite the folder if clean up is true', () => {
+    const mockFiles = {
+      [`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
+      [`${config.ROOT_FOLDER}/apps/test`]: ['docker-compose.yml'],
+      [`${config.ROOT_FOLDER}/apps/test/docker-compose.yml`]: 'test',
+    };
+
+    // @ts-ignore
+    fs.__createMockFiles(mockFiles);
+
+    // Act
+    ensureAppFolder('test', true);
+
+    // Assert
+    const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
+    expect(files).toEqual(['test.yml']);
+  });
+});

+ 6 - 2
packages/system-api/src/modules/fs/fs.helpers.ts

@@ -42,9 +42,13 @@ export const getSeed = () => {
   return seed.toString();
   return seed.toString();
 };
 };
 
 
-export const ensureAppFolder = (appName: string) => {
+export const ensureAppFolder = (appName: string, cleanup = false) => {
+  if (cleanup) {
+    deleteFolder(`/apps/${appName}`);
+  }
+
   if (!fileExists(`/apps/${appName}/docker-compose.yml`)) {
   if (!fileExists(`/apps/${appName}/docker-compose.yml`)) {
-    fs.removeSync(getAbsolutePath(`/apps/${appName}`));
+    deleteFolder(`/apps/${appName}`);
     // Copy from apps repo
     // Copy from apps repo
     fs.copySync(getAbsolutePath(`/repos/${config.APPS_REPO_ID}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
     fs.copySync(getAbsolutePath(`/repos/${config.APPS_REPO_ID}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
   }
   }

+ 1 - 3
scripts/unsafe-cleanup.sh

@@ -25,7 +25,5 @@ rm -rf "${ROOT_FOLDER}/app-data"
 rm -rf "${ROOT_FOLDER}/data/postgres"
 rm -rf "${ROOT_FOLDER}/data/postgres"
 mkdir -p "${ROOT_FOLDER}/app-data"
 mkdir -p "${ROOT_FOLDER}/app-data"
 
 
-# Put {"installed":""} in state/apps.json
-echo '{"installed":""}' >"${ROOT_FOLDER}/state/apps.json"
-
+cd "$ROOT_FOLDER"
 "${ROOT_FOLDER}/scripts/start.sh"
 "${ROOT_FOLDER}/scripts/start.sh"