mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 23:52:32 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d21c89c0ed |
@@ -859,6 +859,48 @@ describe('/libraries', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not delete excluded external files from disk when trash is emptied', async () => {
|
||||||
|
const relativePath = 'temp/offline-empty-trash/offline.png';
|
||||||
|
const filePath = `${testAssetDir}/${relativePath}`;
|
||||||
|
const internalFilePath = `${testAssetDirInternal}/${relativePath}`;
|
||||||
|
|
||||||
|
utils.createImageFile(filePath);
|
||||||
|
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/offline-empty-trash`],
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: internalFilePath,
|
||||||
|
});
|
||||||
|
expect(assets.count).toBe(1);
|
||||||
|
|
||||||
|
await utils.updateLibrary(admin.accessToken, library.id, {
|
||||||
|
exclusionPatterns: ['**/offline-empty-trash/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
expect(trashedAsset.isTrashed).toBe(true);
|
||||||
|
expect(trashedAsset.isOffline).toBe(true);
|
||||||
|
|
||||||
|
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
|
||||||
|
|
||||||
|
expect(existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
utils.removeImageFile(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should not set an asset offline if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
|
it('should not set an asset offline if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { DB } from 'src/schema';
|
|||||||
export class TrashRepository {
|
export class TrashRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
getDeletedIds(): AsyncIterableIterator<{ id: string }> {
|
getDeletedIds(): AsyncIterableIterator<{ id: string; isOffline: boolean }> {
|
||||||
return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.Deleted).stream();
|
return this.db.selectFrom('asset').select(['id', 'isOffline']).where('status', '=', AssetStatus.Deleted).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ import { TrashService } from 'src/services/trash.service';
|
|||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
async function* makeAssetIdStream(count: number): AsyncIterableIterator<{ id: string }> {
|
async function* makeAssetIdStream(count: number): AsyncIterableIterator<{ id: string; isOffline: boolean }> {
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
yield { id: `asset-${i + 1}` };
|
yield { id: `asset-${i + 1}`, isOffline: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* makeDeletedAssetStream(
|
||||||
|
assets: Array<{ id: string; isOffline: boolean }>,
|
||||||
|
): AsyncIterableIterator<{ id: string; isOffline: boolean }> {
|
||||||
|
for (const asset of assets) {
|
||||||
|
await Promise.resolve();
|
||||||
|
yield asset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,5 +108,27 @@ describe(TrashService.name, () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not delete offline assets on disk', async () => {
|
||||||
|
mocks.trash.getDeletedIds.mockReturnValue(
|
||||||
|
makeDeletedAssetStream([
|
||||||
|
{ id: 'asset-1', isOffline: false },
|
||||||
|
{ id: 'asset-2', isOffline: true },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(sut.handleEmptyTrash()).resolves.toEqual(JobStatus.Success);
|
||||||
|
|
||||||
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
name: JobName.AssetDelete,
|
||||||
|
data: { id: 'asset-1', deleteOnDisk: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: JobName.AssetDelete,
|
||||||
|
data: { id: 'asset-2', deleteOnDisk: false },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ export class TrashService extends BaseService {
|
|||||||
const assets = this.trashRepository.getDeletedIds();
|
const assets = this.trashRepository.getDeletedIds();
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const batch: string[] = [];
|
const batch: Array<{ id: string; isOffline: boolean }> = [];
|
||||||
for await (const { id } of assets) {
|
for await (const asset of assets) {
|
||||||
batch.push(id);
|
batch.push(asset);
|
||||||
|
|
||||||
if (batch.length === JOBS_ASSET_PAGINATION_SIZE) {
|
if (batch.length === JOBS_ASSET_PAGINATION_SIZE) {
|
||||||
await this.handleBatch(batch);
|
await this.handleBatch(batch);
|
||||||
@@ -70,14 +70,14 @@ export class TrashService extends BaseService {
|
|||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleBatch(ids: string[]) {
|
private async handleBatch(assets: Array<{ id: string; isOffline: boolean }>) {
|
||||||
this.logger.debug(`Queueing ${ids.length} asset(s) for deletion from the trash`);
|
this.logger.debug(`Queueing ${assets.length} asset(s) for deletion from the trash`);
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
ids.map((assetId) => ({
|
assets.map(({ id, isOffline }) => ({
|
||||||
name: JobName.AssetDelete,
|
name: JobName.AssetDelete,
|
||||||
data: {
|
data: {
|
||||||
id: assetId,
|
id,
|
||||||
deleteOnDisk: true,
|
deleteOnDisk: !isOffline,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user