mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' of https://github.com/immich-app/immich into feat/sidecar-asset-files
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
22.20.0
|
||||
24.11.0
|
||||
|
||||
@@ -35,10 +35,10 @@ services:
|
||||
- 2285:2285
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:7fe72c486b910f6b1a9769c937dad5d63648ddee82e056f47417542dd40825bb
|
||||
image: redis:6.2-alpine@sha256:77697a75da9f94e9357b61fcaf8345f69e3d9d32e9d15032c8415c21263977dc
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:11ced39d65a92a54d12890ced6a26cc2003f92697d6f0d4d944b98459dba7138
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
|
||||
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.0.1",
|
||||
"version": "2.2.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.18.1",
|
||||
"@types/node": "^22.18.12",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -34,7 +34,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
"exiftool-vendored": "^31.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
@@ -43,7 +43,7 @@
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.34.3",
|
||||
"sharp": "^0.34.4",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -52,6 +52,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.20.0"
|
||||
"node": "24.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ describe('/albums', () => {
|
||||
expect(body).toEqual({
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ isFavorite: false })],
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
@@ -310,6 +311,7 @@ describe('/albums', () => {
|
||||
expect(body).toEqual({
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
@@ -345,6 +347,7 @@ describe('/albums', () => {
|
||||
expect(body).toEqual({
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
@@ -362,6 +365,7 @@ describe('/albums', () => {
|
||||
expect(body).toEqual({
|
||||
...user1Albums[0],
|
||||
assets: [],
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
@@ -382,6 +386,7 @@ describe('/albums', () => {
|
||||
expect(body).toEqual({
|
||||
...user2Albums[0],
|
||||
assets: [],
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
|
||||
@@ -113,6 +113,7 @@ describe('/server', () => {
|
||||
importFaces: false,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
ocr: false,
|
||||
passwordLogin: true,
|
||||
search: true,
|
||||
sidecar: true,
|
||||
|
||||
@@ -582,7 +582,7 @@ describe('/tags', () => {
|
||||
expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]);
|
||||
});
|
||||
|
||||
it('should remove duplicate assets only once', async () => {
|
||||
it.skip('should remove duplicate assets only once', async () => {
|
||||
const tagA = await create(user.accessToken, { name: 'TagA' });
|
||||
await tagAssets(
|
||||
{ id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
JobName,
|
||||
LoginResponseDto,
|
||||
createStack,
|
||||
deleteUserAdmin,
|
||||
@@ -327,6 +328,8 @@ describe('/admin/users', () => {
|
||||
{ headers: asBearerAuth(user.accessToken) },
|
||||
);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/admin/users/${user.userId}`)
|
||||
.send({ force: true })
|
||||
|
||||
@@ -442,6 +442,176 @@ describe(`immich upload`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich upload --delete-duplicates', () => {
|
||||
it('should delete local duplicate files', async () => {
|
||||
const {
|
||||
stderr: firstStderr,
|
||||
stdout: firstStdout,
|
||||
exitCode: firstExitCode,
|
||||
} = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(firstStderr).toContain('{message}');
|
||||
expect(firstStdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
|
||||
);
|
||||
expect(firstExitCode).toBe(0);
|
||||
|
||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||
await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`);
|
||||
|
||||
// Upload with --delete-duplicates flag
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`/tmp/albums/nature/silver_fir.jpg`,
|
||||
'--delete-duplicates',
|
||||
]);
|
||||
|
||||
// Check that the duplicate file was deleted
|
||||
const files = await readdir(`/tmp/albums/nature`);
|
||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||
expect(files.length).toBe(0);
|
||||
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Found 0 new files and 1 duplicate'),
|
||||
expect.stringContaining('All assets were already uploaded, nothing to do'),
|
||||
]),
|
||||
);
|
||||
expect(stderr).toContain('{message}');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Verify no new assets were uploaded
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should have accurate dry run with --delete-duplicates', async () => {
|
||||
const {
|
||||
stderr: firstStderr,
|
||||
stdout: firstStdout,
|
||||
exitCode: firstExitCode,
|
||||
} = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(firstStderr).toContain('{message}');
|
||||
expect(firstStdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
|
||||
);
|
||||
expect(firstExitCode).toBe(0);
|
||||
|
||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||
await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`);
|
||||
|
||||
// Upload with --delete-duplicates and --dry-run flags
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`/tmp/albums/nature/silver_fir.jpg`,
|
||||
'--delete-duplicates',
|
||||
'--dry-run',
|
||||
]);
|
||||
|
||||
// Check that the duplicate file was NOT deleted in dry run mode
|
||||
const files = await readdir(`/tmp/albums/nature`);
|
||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||
expect(files.length).toBe(1);
|
||||
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Found 0 new files and 1 duplicate'),
|
||||
expect.stringContaining('Would have deleted 1 local asset'),
|
||||
]),
|
||||
);
|
||||
expect(stderr).toContain('{message}');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Verify no new assets were uploaded
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with both --delete and --delete-duplicates flags', async () => {
|
||||
// First, upload a file to create a duplicate on the server
|
||||
const {
|
||||
stderr: firstStderr,
|
||||
stdout: firstStdout,
|
||||
exitCode: firstExitCode,
|
||||
} = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(firstStderr).toContain('{message}');
|
||||
expect(firstStdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
|
||||
);
|
||||
expect(firstExitCode).toBe(0);
|
||||
|
||||
// Both new and duplicate files
|
||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||
await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`); // duplicate
|
||||
await symlink(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `/tmp/albums/nature/el_torcal_rocks.jpg`); // new
|
||||
|
||||
// Upload with both --delete and --delete-duplicates flags
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`/tmp/albums/nature`,
|
||||
'--delete',
|
||||
'--delete-duplicates',
|
||||
]);
|
||||
|
||||
// Check that both files were deleted (new file due to --delete, duplicate due to --delete-duplicates)
|
||||
const files = await readdir(`/tmp/albums/nature`);
|
||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||
expect(files.length).toBe(0);
|
||||
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Found 1 new files and 1 duplicate'),
|
||||
expect.stringContaining('Successfully uploaded 1 new asset'),
|
||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||
]),
|
||||
);
|
||||
expect(stderr).toContain('{message}');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Verify one new asset was uploaded (total should be 2 now)
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(2);
|
||||
});
|
||||
|
||||
it('should only delete duplicates when --delete-duplicates is used without --delete', async () => {
|
||||
const {
|
||||
stderr: firstStderr,
|
||||
stdout: firstStdout,
|
||||
exitCode: firstExitCode,
|
||||
} = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(firstStderr).toContain('{message}');
|
||||
expect(firstStdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
|
||||
);
|
||||
expect(firstExitCode).toBe(0);
|
||||
|
||||
// Both new and duplicate files
|
||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||
await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`); // duplicate
|
||||
await symlink(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `/tmp/albums/nature/el_torcal_rocks.jpg`); // new
|
||||
|
||||
// Upload with only --delete-duplicates flag
|
||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete-duplicates']);
|
||||
|
||||
// Check that only the duplicate was deleted, new file should remain
|
||||
const files = await readdir(`/tmp/albums/nature`);
|
||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||
expect(files).toEqual(['el_torcal_rocks.jpg']);
|
||||
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Found 1 new files and 1 duplicate'),
|
||||
expect.stringContaining('Successfully uploaded 1 new asset'),
|
||||
]),
|
||||
);
|
||||
expect(stderr).toContain('{message}');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Verify one new asset was uploaded (total should be 2 now)
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich upload --skip-hash', () => {
|
||||
it('should skip hashing', async () => {
|
||||
const filename = `albums/nature/silver_fir.jpg`;
|
||||
|
||||
@@ -119,5 +119,6 @@ export const deviceDto = {
|
||||
isPendingSyncReset: false,
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
appVersion: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ test.describe('Asset Viewer Navbar', () => {
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
await page.keyboard.press('f');
|
||||
await expect(page.locator('#notification-list').getByTestId('message')).toHaveText('Added to favorites');
|
||||
await expect(page.getByText('Added to favorites')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,6 @@ test.describe('Slideshow', () => {
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible();
|
||||
await page.keyboard.press('f');
|
||||
await expect(page.locator('#notification-list')).not.toBeVisible();
|
||||
await expect(page.getByText('Added to favorites')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ test.describe('Registration', () => {
|
||||
await page.getByRole('button', { name: 'User Privacy' }).click();
|
||||
await page.getByRole('button', { name: 'Storage Template' }).click();
|
||||
await page.getByRole('button', { name: 'Backups' }).click();
|
||||
await page.getByRole('button', { name: 'Mobile App' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
// success
|
||||
@@ -85,6 +86,7 @@ test.describe('Registration', () => {
|
||||
await page.getByRole('button', { name: 'Theme' }).click();
|
||||
await page.getByRole('button', { name: 'Language' }).click();
|
||||
await page.getByRole('button', { name: 'User Privacy' }).click();
|
||||
await page.getByRole('button', { name: 'Mobile App' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
// success
|
||||
|
||||
Reference in New Issue
Block a user