mirror of
https://github.com/immich-app/immich.git
synced 2026-04-28 12:13:09 -07:00
Compare commits
1 Commits
feat/trash
...
fix/dont-d
| 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 () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
|
||||
@@ -7,8 +7,8 @@ import { DB } from 'src/schema';
|
||||
export class TrashRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
getDeletedIds(): AsyncIterableIterator<{ id: string }> {
|
||||
return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.Deleted).stream();
|
||||
getDeletedIds(): AsyncIterableIterator<{ id: string; isOffline: boolean }> {
|
||||
return this.db.selectFrom('asset').select(['id', 'isOffline']).where('status', '=', AssetStatus.Deleted).stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
||||
@@ -4,10 +4,19 @@ import { TrashService } from 'src/services/trash.service';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
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++) {
|
||||
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();
|
||||
|
||||
let count = 0;
|
||||
const batch: string[] = [];
|
||||
for await (const { id } of assets) {
|
||||
batch.push(id);
|
||||
const batch: Array<{ id: string; isOffline: boolean }> = [];
|
||||
for await (const asset of assets) {
|
||||
batch.push(asset);
|
||||
|
||||
if (batch.length === JOBS_ASSET_PAGINATION_SIZE) {
|
||||
await this.handleBatch(batch);
|
||||
@@ -70,14 +70,14 @@ export class TrashService extends BaseService {
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
private async handleBatch(ids: string[]) {
|
||||
this.logger.debug(`Queueing ${ids.length} asset(s) for deletion from the trash`);
|
||||
private async handleBatch(assets: Array<{ id: string; isOffline: boolean }>) {
|
||||
this.logger.debug(`Queueing ${assets.length} asset(s) for deletion from the trash`);
|
||||
await this.jobRepository.queueAll(
|
||||
ids.map((assetId) => ({
|
||||
assets.map(({ id, isOffline }) => ({
|
||||
name: JobName.AssetDelete,
|
||||
data: {
|
||||
id: assetId,
|
||||
deleteOnDisk: true,
|
||||
id,
|
||||
deleteOnDisk: !isOffline,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user