From d52ed51aabb698a4ed4f9be291759ffbd9e6f242 Mon Sep 17 00:00:00 2001
From: Mert <101130780+mertalev@users.noreply.github.com>
Date: Thu, 25 Apr 2024 21:14:48 -0400
Subject: [PATCH] fix(cli): dry run being inaccurate (#9088)

---
 cli/src/commands/asset.ts            |   6 +-
 cli/src/index.ts                     |   3 +-
 e2e/src/cli/specs/upload.e2e-spec.ts | 154 ++++++++++++++++++++++++++-
 3 files changed, 158 insertions(+), 5 deletions(-)

diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts
index 65d6871b6d..181240e5b8 100644
--- a/cli/src/commands/asset.ts
+++ b/cli/src/commands/asset.ts
@@ -141,7 +141,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
 
   if (dryRun) {
     console.log(`Would have uploaded ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`);
-    return [];
+    return files.map((filepath) => ({ id: '', filepath }));
   }
 
   const uploadProgress = new SingleBar(
@@ -244,7 +244,7 @@ const deleteFiles = async (files: string[], options: UploadOptionsDto): Promise<
   }
 
   if (options.dryRun) {
-    console.log(`Would now have deleted assets, but skipped due to dry run`);
+    console.log(`Would have deleted ${files.length} local asset${s(files.length)}`);
     return;
   }
 
@@ -285,7 +285,7 @@ const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
   if (dryRun) {
     // TODO print asset counts for new albums
     console.log(`Would have created ${newAlbums.size} new album${s(newAlbums.size)}`);
-    console.log(`Would have updated ${assets.length} asset${s(assets.length)}`);
+    console.log(`Would have updated albums of ${assets.length} asset${s(assets.length)}`);
     return;
   }
 
diff --git a/cli/src/index.ts b/cli/src/index.ts
index 24828d8bb0..341a70bef0 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -60,7 +60,8 @@ program
   .addOption(
     new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
       .env('IMMICH_DRY_RUN')
-      .default(false),
+      .default(false)
+      .conflicts('skipHash'),
   )
   .addOption(
     new Option('-c, --concurrency <number>', 'Number of assets to upload at the same time')
diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts
index 7d1b1aa3e2..89f38feadc 100644
--- a/e2e/src/cli/specs/upload.e2e-spec.ts
+++ b/e2e/src/cli/specs/upload.e2e-spec.ts
@@ -51,7 +51,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('All assets were already uploaded, nothing to do'),
         ]),
       );
-      expect(first.exitCode).toBe(0);
+      expect(second.exitCode).toBe(0);
     });
 
     it('should skip files that do not exist', async () => {
@@ -63,6 +63,44 @@ describe(`immich upload`, () => {
       const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
       expect(assets.length).toBe(0);
     });
+
+    it('should have accurate dry run', async () => {
+      const { stderr, stdout, exitCode } = await immichCli([
+        'upload',
+        `${testAssetDir}/albums/nature/silver_fir.jpg`,
+        '--dry-run',
+      ]);
+      expect(stderr).toBe('');
+      expect(stdout.split('\n')).toEqual(
+        expect.arrayContaining([expect.stringContaining('Would have uploaded 1 asset')]),
+      );
+      expect(exitCode).toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(0);
+    });
+
+    it('dry run should handle duplicates', async () => {
+      const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
+      expect(first.stderr).toBe('');
+      expect(first.stdout.split('\n')).toEqual(
+        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
+      );
+      expect(first.exitCode).toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(1);
+
+      const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
+      expect(second.stderr).toBe('');
+      expect(second.stdout.split('\n')).toEqual(
+        expect.arrayContaining([
+          expect.stringContaining('Found 8 new files and 1 duplicate'),
+          expect.stringContaining('Would have uploaded 8 assets'),
+        ]),
+      );
+      expect(second.exitCode).toBe(0);
+    });
   });
 
   describe('immich upload --recursive', () => {
@@ -136,6 +174,31 @@ describe(`immich upload`, () => {
       expect(albums2.length).toBe(1);
       expect(albums2[0].albumName).toBe('nature');
     });
+
+    it('should have accurate dry run', async () => {
+      const { stderr, stdout, exitCode } = await immichCli([
+        'upload',
+        `${testAssetDir}/albums/nature/`,
+        '--recursive',
+        '--album',
+        '--dry-run',
+      ]);
+      expect(stdout.split('\n')).toEqual(
+        expect.arrayContaining([
+          expect.stringContaining('Would have uploaded 9 assets'),
+          expect.stringContaining('Would have created 1 new album'),
+          expect.stringContaining('Would have updated albums of 9 assets'),
+        ]),
+      );
+      expect(stderr).toBe('');
+      expect(exitCode).toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(0);
+
+      const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
+      expect(albums.length).toBe(0);
+    });
   });
 
   describe('immich upload --recursive --album-name=e2e', () => {
@@ -163,6 +226,31 @@ describe(`immich upload`, () => {
       expect(albums.length).toBe(1);
       expect(albums[0].albumName).toBe('e2e');
     });
+
+    it('should have accurate dry run', async () => {
+      const { stderr, stdout, exitCode } = await immichCli([
+        'upload',
+        `${testAssetDir}/albums/nature/`,
+        '--recursive',
+        '--album-name=e2e',
+        '--dry-run',
+      ]);
+      expect(stdout.split('\n')).toEqual(
+        expect.arrayContaining([
+          expect.stringContaining('Would have uploaded 9 assets'),
+          expect.stringContaining('Would have created 1 new album'),
+          expect.stringContaining('Would have updated albums of 9 assets'),
+        ]),
+      );
+      expect(stderr).toBe('');
+      expect(exitCode).toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(0);
+
+      const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
+      expect(albums.length).toBe(0);
+    });
   });
 
   describe('immich upload --delete', () => {
@@ -191,6 +279,32 @@ describe(`immich upload`, () => {
       const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
       expect(assets.length).toBe(9);
     });
+
+    it('should have accurate dry run', async () => {
+      await mkdir(`/tmp/albums/nature`, { recursive: true });
+      const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
+      for (const file of filesToLink) {
+        await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
+      }
+
+      const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete', '--dry-run']);
+
+      const files = await readdir(`/tmp/albums/nature`);
+      await rm(`/tmp/albums/nature`, { recursive: true });
+      expect(files.length).toBe(9);
+
+      expect(stdout.split('\n')).toEqual(
+        expect.arrayContaining([
+          expect.stringContaining('Would have uploaded 9 assets'),
+          expect.stringContaining('Would have deleted 9 local assets'),
+        ]),
+      );
+      expect(stderr).toBe('');
+      expect(exitCode).toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(0);
+    });
   });
 
   describe('immich upload --skip-hash', () => {
@@ -217,6 +331,22 @@ describe(`immich upload`, () => {
       const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
       expect(assets.length).toBe(1);
     });
+
+    it('should throw an error if attempting dry run', async () => {
+      const { stderr, stdout, exitCode } = await immichCli([
+        'upload',
+        `${testAssetDir}/albums/nature/`,
+        '--skip-hash',
+        '--dry-run',
+      ]);
+
+      expect(stdout).toBe('');
+      expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
+      expect(exitCode).not.toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(0);
+    });
   });
 
   describe('immich upload --concurrency <number>', () => {
@@ -302,5 +432,27 @@ describe(`immich upload`, () => {
       const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
       expect(assets.length).toBe(1);
     });
+
+    it('should have accurate dry run', async () => {
+      const { stderr, stdout, exitCode } = await immichCli([
+        'upload',
+        `${testAssetDir}/albums/nature/`,
+        '--ignore',
+        'silver_fir.jpg',
+        '--dry-run',
+      ]);
+
+      expect(stderr).toBe('');
+      expect(stdout.split('\n')).toEqual(
+        expect.arrayContaining([
+          'Found 8 new files and 0 duplicates',
+          expect.stringContaining('Would have uploaded 8 assets'),
+        ]),
+      );
+      expect(exitCode).toBe(0);
+
+      const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
+      expect(assets.length).toBe(0);
+    });
   });
 });