2024-09-07 13:21:25 -04:00
|
|
|
import { SystemMetadataKey } from 'src/enum';
|
2024-10-31 13:42:58 -04:00
|
|
|
import { StorageService } from 'src/services/storage.service';
|
|
|
|
|
import { ImmichStartupError } from 'src/utils/misc';
|
2024-10-02 10:54:35 -04:00
|
|
|
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
2025-02-10 18:47:42 -05:00
|
|
|
import { newTestService, ServiceMocks } from 'test/utils';
|
2023-02-25 09:12:03 -05:00
|
|
|
|
|
|
|
|
describe(StorageService.name, () => {
|
|
|
|
|
let sut: StorageService;
|
2025-02-10 18:47:42 -05:00
|
|
|
let mocks: ServiceMocks;
|
2023-02-25 09:12:03 -05:00
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
beforeEach(() => {
|
2025-02-10 18:47:42 -05:00
|
|
|
({ sut, mocks } = newTestService(StorageService));
|
2023-02-25 09:12:03 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should work', () => {
|
|
|
|
|
expect(sut).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
2024-08-15 16:12:41 -04:00
|
|
|
describe('onBootstrap', () => {
|
2024-09-07 13:21:25 -04:00
|
|
|
it('should enable mount folder checking', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue(null);
|
2025-07-25 15:25:36 -04:00
|
|
|
mocks.asset.getFileSamples.mockResolvedValue([]);
|
2024-09-07 13:21:25 -04:00
|
|
|
|
|
|
|
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
|
|
|
|
|
2025-07-15 14:50:13 -04:00
|
|
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, {
|
2024-10-29 14:43:27 +00:00
|
|
|
mountChecks: {
|
2024-10-31 11:29:42 +00:00
|
|
|
backups: true,
|
2024-10-29 14:43:27 +00:00
|
|
|
'encoded-video': true,
|
|
|
|
|
library: true,
|
|
|
|
|
profile: true,
|
|
|
|
|
thumbs: true,
|
|
|
|
|
upload: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-07-18 10:57:29 -04:00
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/encoded-video'));
|
|
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/library'));
|
|
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/profile'));
|
|
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/thumbs'));
|
|
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/upload'));
|
|
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/backups'));
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/encoded-video/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/library/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/profile/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/thumbs/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/upload/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/backups/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
2024-09-07 13:21:25 -04:00
|
|
|
});
|
|
|
|
|
|
2024-10-29 14:43:27 +00:00
|
|
|
it('should enable mount folder checking for a new folder type', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue({
|
2024-10-29 14:43:27 +00:00
|
|
|
mountChecks: {
|
2024-10-31 11:29:42 +00:00
|
|
|
backups: false,
|
2024-10-29 14:43:27 +00:00
|
|
|
'encoded-video': true,
|
|
|
|
|
library: false,
|
|
|
|
|
profile: true,
|
|
|
|
|
thumbs: true,
|
|
|
|
|
upload: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-07-25 15:25:36 -04:00
|
|
|
mocks.asset.getFileSamples.mockResolvedValue([]);
|
2024-10-29 14:43:27 +00:00
|
|
|
|
|
|
|
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
|
|
|
|
|
2025-07-15 14:50:13 -04:00
|
|
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, {
|
2024-10-29 14:43:27 +00:00
|
|
|
mountChecks: {
|
2024-10-31 11:29:42 +00:00
|
|
|
backups: true,
|
2024-10-29 14:43:27 +00:00
|
|
|
'encoded-video': true,
|
|
|
|
|
library: true,
|
|
|
|
|
profile: true,
|
|
|
|
|
thumbs: true,
|
|
|
|
|
upload: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledTimes(2);
|
2025-07-18 10:57:29 -04:00
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/library'));
|
|
|
|
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/backups'));
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledTimes(2);
|
2025-07-18 10:57:29 -04:00
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/library/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.storage.createFile).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('upload/backups/.immich'),
|
|
|
|
|
expect.any(Buffer),
|
|
|
|
|
);
|
2024-10-29 14:43:27 +00:00
|
|
|
});
|
|
|
|
|
|
2024-09-07 13:21:25 -04:00
|
|
|
it('should throw an error if .immich is missing', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } });
|
|
|
|
|
mocks.storage.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
2024-09-07 13:21:25 -04:00
|
|
|
|
2024-10-01 13:04:37 -04:00
|
|
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to read');
|
2024-09-07 13:21:25 -04:00
|
|
|
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
|
|
|
|
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
2024-09-07 13:21:25 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error if .immich is present but read-only', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } });
|
|
|
|
|
mocks.storage.overwriteFile.mockRejectedValue(
|
|
|
|
|
new Error("ENOENT: no such file or directory, open '/app/.immich'"),
|
|
|
|
|
);
|
2024-09-07 13:21:25 -04:00
|
|
|
|
2024-10-01 13:04:37 -04:00
|
|
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to write');
|
|
|
|
|
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
2024-10-01 13:04:37 -04:00
|
|
|
});
|
|
|
|
|
|
2024-10-08 23:08:49 +02:00
|
|
|
it('should skip mount file creation if file already exists', async () => {
|
|
|
|
|
const error = new Error('Error creating file') as any;
|
|
|
|
|
error.code = 'EEXIST';
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} });
|
|
|
|
|
mocks.storage.createFile.mockRejectedValue(error);
|
2025-07-25 15:25:36 -04:00
|
|
|
mocks.asset.getFileSamples.mockResolvedValue([]);
|
2024-10-08 23:08:49 +02:00
|
|
|
|
|
|
|
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
|
|
|
|
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.logger.warn).toHaveBeenCalledWith('Found existing mount file, skipping creation');
|
2024-10-08 23:08:49 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error if mount file could not be created', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} });
|
|
|
|
|
mocks.storage.createFile.mockRejectedValue(new Error('Error creating file'));
|
2024-10-08 23:08:49 +02:00
|
|
|
|
|
|
|
|
await expect(sut.onBootstrap()).rejects.toBeInstanceOf(ImmichStartupError);
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
2024-10-08 23:08:49 +02:00
|
|
|
});
|
|
|
|
|
|
2024-10-01 13:04:37 -04:00
|
|
|
it('should startup if checks are disabled', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } });
|
|
|
|
|
mocks.config.getEnv.mockReturnValue(
|
2024-10-01 13:04:37 -04:00
|
|
|
mockEnvData({
|
|
|
|
|
storage: { ignoreMountCheckErrors: true },
|
|
|
|
|
}),
|
|
|
|
|
);
|
2025-07-25 15:25:36 -04:00
|
|
|
mocks.asset.getFileSamples.mockResolvedValue([]);
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.storage.overwriteFile.mockRejectedValue(
|
|
|
|
|
new Error("ENOENT: no such file or directory, open '/app/.immich'"),
|
|
|
|
|
);
|
2024-10-01 13:04:37 -04:00
|
|
|
|
|
|
|
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
2024-09-07 13:21:25 -04:00
|
|
|
|
2025-07-18 10:57:29 -04:00
|
|
|
expect(mocks.systemMetadata.set).not.toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, expect.anything());
|
2023-05-28 21:48:07 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-02-25 09:12:03 -05:00
|
|
|
describe('handleDeleteFiles', () => {
|
|
|
|
|
it('should handle null values', async () => {
|
|
|
|
|
await sut.handleDeleteFiles({ files: [undefined, null] });
|
|
|
|
|
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.storage.unlink).not.toHaveBeenCalled();
|
2023-02-25 09:12:03 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle an error removing a file', async () => {
|
2025-02-10 18:47:42 -05:00
|
|
|
mocks.storage.unlink.mockRejectedValue(new Error('something-went-wrong'));
|
2023-02-25 09:12:03 -05:00
|
|
|
|
|
|
|
|
await sut.handleDeleteFiles({ files: ['path/to/something'] });
|
|
|
|
|
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.storage.unlink).toHaveBeenCalledWith('path/to/something');
|
2023-02-25 09:12:03 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should remove the file', async () => {
|
|
|
|
|
await sut.handleDeleteFiles({ files: ['path/to/something'] });
|
|
|
|
|
|
2025-02-10 18:47:42 -05:00
|
|
|
expect(mocks.storage.unlink).toHaveBeenCalledWith('path/to/something');
|
2023-02-25 09:12:03 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|