mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
* 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
312 lines
9.3 KiB
TypeScript
312 lines
9.3 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
|
|
|
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
|
import createFetchMock from 'vitest-fetch-mock';
|
|
|
|
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
|
|
|
|
vi.mock('@immich/sdk');
|
|
|
|
describe('getAlbumName', () => {
|
|
it('should return a non-undefined value', () => {
|
|
if (os.platform() === 'win32') {
|
|
// This is meaningless for Unix systems.
|
|
expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test');
|
|
}
|
|
expect(getAlbumName('D:/parentfolder/test/Filename.txt', {} as UploadOptionsDto)).toBe('test');
|
|
});
|
|
|
|
it('has higher priority to return `albumName` in `options`', () => {
|
|
expect(getAlbumName('/parentfolder/test/Filename.txt', { albumName: 'example' } as UploadOptionsDto)).toBe(
|
|
'example',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('uploadFiles', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
const testFilePath = path.join(testDir, 'test.png');
|
|
const testFileData = 'test';
|
|
const baseUrl = 'http://example.com';
|
|
const apiKey = 'key';
|
|
const retry = 3;
|
|
|
|
const fetchMocker = createFetchMock(vi);
|
|
|
|
beforeEach(() => {
|
|
// Create a test file
|
|
fs.writeFileSync(testFilePath, testFileData);
|
|
|
|
// Defaults
|
|
vi.mocked(defaults).baseUrl = baseUrl;
|
|
vi.mocked(defaults).headers = { 'x-api-key': apiKey };
|
|
|
|
fetchMocker.enableMocks();
|
|
fetchMocker.resetMocks();
|
|
});
|
|
|
|
it('returns new assets when upload file is successful', async () => {
|
|
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
|
return {
|
|
status: 200,
|
|
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
|
|
};
|
|
});
|
|
|
|
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
|
|
{
|
|
filepath: testFilePath,
|
|
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns new assets when upload file retry is successful', async () => {
|
|
let counter = 0;
|
|
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
|
counter++;
|
|
if (counter < retry) {
|
|
throw new Error('Network error');
|
|
}
|
|
|
|
return {
|
|
status: 200,
|
|
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
|
|
};
|
|
});
|
|
|
|
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
|
|
{
|
|
filepath: testFilePath,
|
|
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns new assets when upload file retry is failed', async () => {
|
|
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
|
throw new Error('Network error');
|
|
});
|
|
|
|
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('checkForDuplicates', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
const testFilePath = path.join(testDir, 'test.png');
|
|
const testFileData = 'test';
|
|
const testFileChecksum = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'; // SHA1
|
|
const retry = 3;
|
|
|
|
beforeEach(() => {
|
|
// Create a test file
|
|
fs.writeFileSync(testFilePath, testFileData);
|
|
});
|
|
|
|
it('checks duplicates', async () => {
|
|
vi.mocked(checkBulkUpload).mockResolvedValue({
|
|
results: [
|
|
{
|
|
action: Action.Accept,
|
|
id: testFilePath,
|
|
},
|
|
],
|
|
});
|
|
|
|
await checkForDuplicates([testFilePath], { concurrency: 1 });
|
|
|
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
|
assetBulkUploadCheckDto: {
|
|
assets: [
|
|
{
|
|
checksum: testFileChecksum,
|
|
id: testFilePath,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('returns duplicates when check duplicates is rejected', async () => {
|
|
vi.mocked(checkBulkUpload).mockResolvedValue({
|
|
results: [
|
|
{
|
|
action: Action.Reject,
|
|
id: testFilePath,
|
|
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
reason: Reason.Duplicate,
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
duplicates: [
|
|
{
|
|
filepath: testFilePath,
|
|
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
},
|
|
],
|
|
newFiles: [],
|
|
});
|
|
});
|
|
|
|
it('returns new assets when check duplicates is accepted', async () => {
|
|
vi.mocked(checkBulkUpload).mockResolvedValue({
|
|
results: [
|
|
{
|
|
action: Action.Accept,
|
|
id: testFilePath,
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
duplicates: [],
|
|
newFiles: [testFilePath],
|
|
});
|
|
});
|
|
|
|
it('returns results when check duplicates retry is successful', async () => {
|
|
let mocked = vi.mocked(checkBulkUpload);
|
|
for (let i = 1; i < retry; i++) {
|
|
mocked = mocked.mockRejectedValueOnce(new Error('Network error'));
|
|
}
|
|
mocked.mockResolvedValue({
|
|
results: [
|
|
{
|
|
action: Action.Accept,
|
|
id: testFilePath,
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
duplicates: [],
|
|
newFiles: [testFilePath],
|
|
});
|
|
});
|
|
|
|
it('returns results when check duplicates retry is failed', async () => {
|
|
vi.mocked(checkBulkUpload).mockRejectedValue(new Error('Network error'));
|
|
|
|
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
duplicates: [],
|
|
newFiles: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('startWatch', () => {
|
|
let testFolder: string;
|
|
let checkBulkUploadMocked: MockedFunction<typeof checkBulkUpload>;
|
|
|
|
beforeEach(async () => {
|
|
vi.restoreAllMocks();
|
|
|
|
vi.mocked(getSupportedMediaTypes).mockResolvedValue({
|
|
image: ['.jpg'],
|
|
sidecar: ['.xmp'],
|
|
video: ['.mp4'],
|
|
});
|
|
|
|
testFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'test-startWatch-'));
|
|
checkBulkUploadMocked = vi.mocked(checkBulkUpload);
|
|
checkBulkUploadMocked.mockResolvedValue({
|
|
results: [],
|
|
});
|
|
});
|
|
|
|
it('should start watching a directory and upload new files', async () => {
|
|
const testFilePath = path.join(testFolder, 'test.jpg');
|
|
|
|
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
|
|
await sleep(100); // to debounce the watcher from considering the test file as a existing file
|
|
await fs.promises.writeFile(testFilePath, 'testjpg');
|
|
|
|
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
|
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
|
assetBulkUploadCheckDto: {
|
|
assets: [
|
|
expect.objectContaining({
|
|
id: testFilePath,
|
|
}),
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should filter out unsupported files', async () => {
|
|
const testFilePath = path.join(testFolder, 'test.jpg');
|
|
const unsupportedFilePath = path.join(testFolder, 'test.txt');
|
|
|
|
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
|
|
await sleep(100); // to debounce the watcher from considering the test file as a existing file
|
|
await fs.promises.writeFile(testFilePath, 'testjpg');
|
|
await fs.promises.writeFile(unsupportedFilePath, 'testtxt');
|
|
|
|
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
|
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
|
assetBulkUploadCheckDto: {
|
|
assets: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: testFilePath,
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
|
|
expect(checkBulkUpload).not.toHaveBeenCalledWith({
|
|
assetBulkUploadCheckDto: {
|
|
assets: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: unsupportedFilePath,
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should filger out ignored patterns', async () => {
|
|
const testFilePath = path.join(testFolder, 'test.jpg');
|
|
const ignoredPattern = 'ignored';
|
|
const ignoredFolder = path.join(testFolder, ignoredPattern);
|
|
await fs.promises.mkdir(ignoredFolder, { recursive: true });
|
|
const ignoredFilePath = path.join(ignoredFolder, 'ignored.jpg');
|
|
|
|
await startWatch([testFolder], { concurrency: 1, ignore: ignoredPattern }, { batchSize: 1, debounceTimeMs: 10 });
|
|
await sleep(100); // to debounce the watcher from considering the test file as a existing file
|
|
await fs.promises.writeFile(testFilePath, 'testjpg');
|
|
await fs.promises.writeFile(ignoredFilePath, 'ignoredjpg');
|
|
|
|
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
|
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
|
assetBulkUploadCheckDto: {
|
|
assets: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: testFilePath,
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
|
|
expect(checkBulkUpload).not.toHaveBeenCalledWith({
|
|
assetBulkUploadCheckDto: {
|
|
assets: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: ignoredFilePath,
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.promises.rm(testFolder, { recursive: true, force: true });
|
|
});
|
|
});
|