Skip to content

Commit c26f6aa

Browse files
committed
wip move back to typeorm brackets
1 parent 02c5765 commit c26f6aa

File tree

9 files changed

+319
-94
lines changed

9 files changed

+319
-94
lines changed

e2e/src/api/specs/library.e2e-spec.ts

Lines changed: 192 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ describe('/libraries', () => {
421421
const { status } = await request(app)
422422
.post(`/libraries/${library.id}/scan`)
423423
.set('Authorization', `Bearer ${admin.accessToken}`)
424-
.send({ refreshModifiedFiles: true });
424+
.send();
425425
expect(status).toBe(204);
426426

427427
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -453,7 +453,7 @@ describe('/libraries', () => {
453453
const { status } = await request(app)
454454
.post(`/libraries/${library.id}/scan`)
455455
.set('Authorization', `Bearer ${admin.accessToken}`)
456-
.send({ refreshModifiedFiles: true });
456+
.send();
457457
expect(status).toBe(204);
458458

459459
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -577,7 +577,7 @@ describe('/libraries', () => {
577577
]);
578578
});
579579

580-
it('should not trash an online asset', async () => {
580+
it('should not set an asset offline if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
581581
const library = await utils.createLibrary(admin.accessToken, {
582582
ownerId: admin.userId,
583583
importPaths: [`${testAssetDirInternal}/temp`],
@@ -601,6 +601,195 @@ describe('/libraries', () => {
601601

602602
expect(assets).toEqual(assetsBefore);
603603
});
604+
605+
it('should set an offline asset to online if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
606+
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
607+
608+
const library = await utils.createLibrary(admin.accessToken, {
609+
ownerId: admin.userId,
610+
importPaths: [`${testAssetDirInternal}/temp/offline`],
611+
});
612+
613+
await scan(admin.accessToken, library.id);
614+
await utils.waitForQueueFinish(admin.accessToken, 'library');
615+
616+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
617+
618+
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
619+
620+
{
621+
const { status } = await request(app)
622+
.post(`/libraries/${library.id}/scan`)
623+
.set('Authorization', `Bearer ${admin.accessToken}`)
624+
.send();
625+
expect(status).toBe(204);
626+
}
627+
628+
await utils.waitForQueueFinish(admin.accessToken, 'library');
629+
630+
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
631+
expect(offlineAsset.isTrashed).toBe(true);
632+
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
633+
expect(offlineAsset.isOffline).toBe(true);
634+
635+
{
636+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
637+
expect(assets.count).toBe(1);
638+
}
639+
640+
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
641+
642+
{
643+
const { status } = await request(app)
644+
.post(`/libraries/${library.id}/scan`)
645+
.set('Authorization', `Bearer ${admin.accessToken}`)
646+
.send();
647+
expect(status).toBe(204);
648+
}
649+
650+
await utils.waitForQueueFinish(admin.accessToken, 'library');
651+
652+
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
653+
654+
expect(backOnlineAsset.isTrashed).toBe(false);
655+
expect(backOnlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
656+
expect(backOnlineAsset.isOffline).toBe(false);
657+
658+
{
659+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
660+
expect(assets.count).toBe(1);
661+
}
662+
});
663+
664+
it('should not set an offline asset to online if its file exists, is not covered by an exclusion pattern, but is outside of all import paths', async () => {
665+
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
666+
667+
const library = await utils.createLibrary(admin.accessToken, {
668+
ownerId: admin.userId,
669+
importPaths: [`${testAssetDirInternal}/temp/offline`],
670+
});
671+
672+
await scan(admin.accessToken, library.id);
673+
await utils.waitForQueueFinish(admin.accessToken, 'library');
674+
675+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
676+
677+
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
678+
679+
{
680+
const { status } = await request(app)
681+
.post(`/libraries/${library.id}/scan`)
682+
.set('Authorization', `Bearer ${admin.accessToken}`)
683+
.send();
684+
expect(status).toBe(204);
685+
}
686+
687+
await utils.waitForQueueFinish(admin.accessToken, 'library');
688+
689+
{
690+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
691+
expect(assets.count).toBe(1);
692+
}
693+
694+
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
695+
696+
expect(offlineAsset.isTrashed).toBe(true);
697+
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
698+
expect(offlineAsset.isOffline).toBe(true);
699+
700+
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
701+
702+
utils.createDirectory(`${testAssetDir}/temp/another-path/`);
703+
704+
await utils.updateLibrary(admin.accessToken, library.id, {
705+
importPaths: [`${testAssetDirInternal}/temp/another-path`],
706+
});
707+
708+
{
709+
const { status } = await request(app)
710+
.post(`/libraries/${library.id}/scan`)
711+
.set('Authorization', `Bearer ${admin.accessToken}`)
712+
.send();
713+
expect(status).toBe(204);
714+
}
715+
716+
await utils.waitForQueueFinish(admin.accessToken, 'library');
717+
718+
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
719+
720+
expect(stillOfflineAsset.isTrashed).toBe(true);
721+
expect(stillOfflineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
722+
expect(stillOfflineAsset.isOffline).toBe(true);
723+
724+
{
725+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
726+
expect(assets.count).toBe(1);
727+
}
728+
729+
utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
730+
});
731+
732+
it('should not set an offline asset to online if its file exists, is in an import path, but is covered by an exclusion pattern', async () => {
733+
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
734+
735+
const library = await utils.createLibrary(admin.accessToken, {
736+
ownerId: admin.userId,
737+
importPaths: [`${testAssetDirInternal}/temp/offline`],
738+
});
739+
740+
await scan(admin.accessToken, library.id);
741+
await utils.waitForQueueFinish(admin.accessToken, 'library');
742+
743+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
744+
745+
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
746+
747+
{
748+
const { status } = await request(app)
749+
.post(`/libraries/${library.id}/scan`)
750+
.set('Authorization', `Bearer ${admin.accessToken}`)
751+
.send();
752+
expect(status).toBe(204);
753+
}
754+
755+
await utils.waitForQueueFinish(admin.accessToken, 'library');
756+
757+
{
758+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
759+
expect(assets.count).toBe(1);
760+
}
761+
762+
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
763+
764+
expect(offlineAsset.isTrashed).toBe(true);
765+
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
766+
expect(offlineAsset.isOffline).toBe(true);
767+
768+
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
769+
770+
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
771+
772+
{
773+
const { status } = await request(app)
774+
.post(`/libraries/${library.id}/scan`)
775+
.set('Authorization', `Bearer ${admin.accessToken}`)
776+
.send();
777+
expect(status).toBe(204);
778+
}
779+
780+
await utils.waitForQueueFinish(admin.accessToken, 'library');
781+
782+
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
783+
784+
expect(stillOfflineAsset.isTrashed).toBe(true);
785+
expect(stillOfflineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
786+
expect(stillOfflineAsset.isOffline).toBe(true);
787+
788+
{
789+
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
790+
expect(assets.count).toBe(1);
791+
}
792+
});
604793
});
605794

606795
describe('POST /libraries/:id/validate', () => {

e2e/src/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Permission,
1111
PersonCreateDto,
1212
SharedLinkCreateDto,
13+
UpdateLibraryDto,
1314
UserAdminCreateDto,
1415
UserPreferencesUpdateDto,
1516
ValidateLibraryDto,
@@ -35,14 +36,15 @@ import {
3536
updateAlbumUser,
3637
updateAssets,
3738
updateConfig,
39+
updateLibrary,
3840
updateMyPreferences,
3941
upsertTags,
4042
validate,
4143
} from '@immich/sdk';
4244
import { BrowserContext } from '@playwright/test';
4345
import { exec, spawn } from 'node:child_process';
4446
import { createHash } from 'node:crypto';
45-
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
47+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4648
import { tmpdir } from 'node:os';
4749
import path, { dirname } from 'node:path';
4850
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
@@ -392,6 +394,14 @@ export const utils = {
392394
rmSync(path);
393395
},
394396

397+
renameImageFile: (oldPath: string, newPath: string) => {
398+
if (!existsSync(oldPath)) {
399+
return;
400+
}
401+
402+
renameSync(oldPath, newPath);
403+
},
404+
395405
removeDirectory: (path: string) => {
396406
if (!existsSync(path)) {
397407
return;
@@ -444,6 +454,9 @@ export const utils = {
444454
createLibrary: (accessToken: string, dto: CreateLibraryDto) =>
445455
createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
446456

457+
updateLibrary: (accessToken: string, id: string, dto: UpdateLibraryDto) =>
458+
updateLibrary({ id, updateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
459+
447460
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
448461
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
449462

server/src/interfaces/asset.interface.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
22
import { AssetEntity } from 'src/entities/asset.entity';
33
import { ExifEntity } from 'src/entities/exif.entity';
4-
import { LibraryEntity } from 'src/entities/library.entity';
54
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
65
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
76
import { Paginated, PaginationOptions } from 'src/utils/pagination';
@@ -193,5 +192,5 @@ export interface IAssetRepository {
193192
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
194193
upsertFile(file: UpsertFileOptions): Promise<void>;
195194
upsertFiles(files: UpsertFileOptions[]): Promise<void>;
196-
updateOffline(pagination: PaginationOptions, library: LibraryEntity): Paginated<AssetEntity>;
195+
updateOffline(importPaths: string[], exclusionPatterns: string[]): Promise<UpdateResult>;
197196
}

server/src/repositories/asset.repository.ts

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { Injectable } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
3-
import picomatch from 'picomatch';
43
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
54
import { AssetFileEntity } from 'src/entities/asset-files.entity';
65
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
76
import { AssetEntity } from 'src/entities/asset.entity';
87
import { ExifEntity } from 'src/entities/exif.entity';
9-
import { LibraryEntity } from 'src/entities/library.entity';
108
import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum';
119
import {
1210
AssetBuilderOptions,
@@ -716,39 +714,26 @@ export class AssetRepository implements IAssetRepository {
716714
await this.fileRepository.upsert(files, { conflictPaths: ['assetId', 'type'] });
717715
}
718716

719-
updateOffline(pagination: PaginationOptions, library: LibraryEntity): Paginated<AssetEntity> {
720-
return this.dataSource.manager.transaction(async (transactionalEntityManager) =>
721-
transactionalEntityManager.query(
722-
`
723-
WITH updated_rows AS (
724-
UPDATE assets
725-
SET "isOffline" = $1, "deletedAt" = $2
726-
WHERE "isOffline" = $3
727-
AND (
728-
"originalPath" NOT SIMILAR TO $4
729-
OR "originalPath" SIMILAR TO $5
730-
)
731-
RETURNING id
732-
)
733-
SELECT *
734-
FROM assets
735-
WHERE id NOT IN (SELECT id FROM updated_rows)
736-
AND "libraryId" = $6
737-
AND ($7 OR "deletedAt" IS NULL)
738-
LIMIT $8 OFFSET $9;
739-
`,
740-
[
741-
true, // $1 - is_offline = true
742-
new Date(), // $2 - deleted_at = current timestamp
743-
false, // $3 - is_offline = false
744-
library.importPaths.map((importPath) => `${importPath}%`).join('|'), // $4 - importPartMatcher pattern
745-
library.exclusionPatterns.map(globToSqlPattern).join('|'), // $5 - exclusionPatternMatcher pattern
746-
library.id, // $6 - libraryId matches job.id
747-
true, // $7 - withDeleted flag
748-
pagination.take, // $8 - LIMIT
749-
pagination.skip, // $9 - OFFSET
750-
],
751-
),
752-
);
717+
updateOffline(importPaths: string[], exclusionPatterns: string[]): Promise<UpdateResult> {
718+
const paths = importPaths.map((importPath) => `${importPath}%`).join('|');
719+
const exclusions = exclusionPatterns.map((pattern) => globToSqlPattern(pattern)).join('|');
720+
return this.repository
721+
.createQueryBuilder()
722+
.update()
723+
.set({
724+
isOffline: true,
725+
deletedAt: new Date(),
726+
})
727+
.where({ isOffline: false })
728+
.andWhere(
729+
new Brackets((qb) => {
730+
qb.where('originalPath NOT SIMILAR TO :paths', {
731+
paths,
732+
}).orWhere('originalPath SIMILAR TO :exclusions', {
733+
exclusions,
734+
});
735+
}),
736+
)
737+
.execute();
753738
}
754739
}

0 commit comments

Comments
 (0)