Skip to content

Commit e2c34f1

Browse files
authored
feat(cli): watch paths for auto uploading daemon (#14923)
* feat(cli): watch paths for auto uploading daemon * chore: update package-lock * test(cli): Batcher util calss * feat(cli): expose batcher params from startWatch() * test(cli): startWatch() for `--watch` * refactor(cli): more reliable watcher * feat(cli): disable progress bar on --no-progress or --watch * fix(cli): extensions match when upload with watch * feat(cli): basic logs without progress on upload * feat(cli): hide progress in uploadFiles() * refactor(cli): use promise-based setTimeout() instead of hand crafted sleep() * refactor(cli): unexport UPLOAD_WATCH consts * refactor(cli): rename fsWatchListener() to onFile() * test(cli): prefix dot to mocked getSupportedMediaTypes() * test(cli): add tests for ignored patterns/ unsupported exts * refactor(cli): minor changes for code reviews * feat(cli): disable onFile logs when progress bar is enabled
1 parent 23b1256 commit e2c34f1

File tree

7 files changed

+397
-33
lines changed

7 files changed

+397
-33
lines changed

cli/package-lock.json

+49-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@types/byte-size": "^8.1.0",
2020
"@types/cli-progress": "^3.11.0",
2121
"@types/lodash-es": "^4.17.12",
22+
"@types/micromatch": "^4.0.9",
2223
"@types/mock-fs": "^4.13.1",
2324
"@types/node": "^22.13.4",
2425
"@typescript-eslint/eslint-plugin": "^8.15.0",
@@ -62,11 +63,13 @@
6263
"node": ">=20.0.0"
6364
},
6465
"dependencies": {
66+
"chokidar": "^4.0.3",
6567
"fast-glob": "^3.3.2",
6668
"fastq": "^1.17.1",
67-
"lodash-es": "^4.17.21"
69+
"lodash-es": "^4.17.21",
70+
"micromatch": "^4.0.8"
6871
},
6972
"volta": {
7073
"node": "22.14.0"
7174
}
72-
}
75+
}

cli/src/commands/asset.spec.ts

+113-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as fs from 'node:fs';
22
import * as os from 'node:os';
33
import * as path from 'node:path';
4-
import { describe, expect, it, vi } from 'vitest';
4+
import { setTimeout as sleep } from 'node:timers/promises';
5+
import { describe, expect, it, MockedFunction, vi } from 'vitest';
56

6-
import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk';
7+
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
78
import createFetchMock from 'vitest-fetch-mock';
89

9-
import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset';
10+
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
1011

1112
vi.mock('@immich/sdk');
1213

@@ -199,3 +200,112 @@ describe('checkForDuplicates', () => {
199200
});
200201
});
201202
});
203+
204+
describe('startWatch', () => {
205+
let testFolder: string;
206+
let checkBulkUploadMocked: MockedFunction<typeof checkBulkUpload>;
207+
208+
beforeEach(async () => {
209+
vi.restoreAllMocks();
210+
211+
vi.mocked(getSupportedMediaTypes).mockResolvedValue({
212+
image: ['.jpg'],
213+
sidecar: ['.xmp'],
214+
video: ['.mp4'],
215+
});
216+
217+
testFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'test-startWatch-'));
218+
checkBulkUploadMocked = vi.mocked(checkBulkUpload);
219+
checkBulkUploadMocked.mockResolvedValue({
220+
results: [],
221+
});
222+
});
223+
224+
it('should start watching a directory and upload new files', async () => {
225+
const testFilePath = path.join(testFolder, 'test.jpg');
226+
227+
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
228+
await sleep(100); // to debounce the watcher from considering the test file as a existing file
229+
await fs.promises.writeFile(testFilePath, 'testjpg');
230+
231+
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
232+
expect(checkBulkUpload).toHaveBeenCalledWith({
233+
assetBulkUploadCheckDto: {
234+
assets: [
235+
expect.objectContaining({
236+
id: testFilePath,
237+
}),
238+
],
239+
},
240+
});
241+
});
242+
243+
it('should filter out unsupported files', async () => {
244+
const testFilePath = path.join(testFolder, 'test.jpg');
245+
const unsupportedFilePath = path.join(testFolder, 'test.txt');
246+
247+
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
248+
await sleep(100); // to debounce the watcher from considering the test file as a existing file
249+
await fs.promises.writeFile(testFilePath, 'testjpg');
250+
await fs.promises.writeFile(unsupportedFilePath, 'testtxt');
251+
252+
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
253+
expect(checkBulkUpload).toHaveBeenCalledWith({
254+
assetBulkUploadCheckDto: {
255+
assets: expect.arrayContaining([
256+
expect.objectContaining({
257+
id: testFilePath,
258+
}),
259+
]),
260+
},
261+
});
262+
263+
expect(checkBulkUpload).not.toHaveBeenCalledWith({
264+
assetBulkUploadCheckDto: {
265+
assets: expect.arrayContaining([
266+
expect.objectContaining({
267+
id: unsupportedFilePath,
268+
}),
269+
]),
270+
},
271+
});
272+
});
273+
274+
it('should filger out ignored patterns', async () => {
275+
const testFilePath = path.join(testFolder, 'test.jpg');
276+
const ignoredPattern = 'ignored';
277+
const ignoredFolder = path.join(testFolder, ignoredPattern);
278+
await fs.promises.mkdir(ignoredFolder, { recursive: true });
279+
const ignoredFilePath = path.join(ignoredFolder, 'ignored.jpg');
280+
281+
await startWatch([testFolder], { concurrency: 1, ignore: ignoredPattern }, { batchSize: 1, debounceTimeMs: 10 });
282+
await sleep(100); // to debounce the watcher from considering the test file as a existing file
283+
await fs.promises.writeFile(testFilePath, 'testjpg');
284+
await fs.promises.writeFile(ignoredFilePath, 'ignoredjpg');
285+
286+
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
287+
expect(checkBulkUpload).toHaveBeenCalledWith({
288+
assetBulkUploadCheckDto: {
289+
assets: expect.arrayContaining([
290+
expect.objectContaining({
291+
id: testFilePath,
292+
}),
293+
]),
294+
},
295+
});
296+
297+
expect(checkBulkUpload).not.toHaveBeenCalledWith({
298+
assetBulkUploadCheckDto: {
299+
assets: expect.arrayContaining([
300+
expect.objectContaining({
301+
id: ignoredFilePath,
302+
}),
303+
]),
304+
},
305+
});
306+
});
307+
308+
afterEach(async () => {
309+
await fs.promises.rm(testFolder, { recursive: true, force: true });
310+
});
311+
});

0 commit comments

Comments
 (0)