feat: download original asset (#25302)

Co-authored-by: bwees <brandonwees@gmail.com>
This commit is contained in:
Daniel Dietzler
2026-01-16 13:05:13 -06:00
committed by GitHub
parent a2b03f7650
commit 07675a2de4
29 changed files with 354 additions and 11 deletions
+28
View File
@@ -86,6 +86,7 @@ export const assetStub = {
make: 'FUJIFILM',
model: 'X-T50',
lensModel: 'XF27mm F2.8 R WR',
editCount: 0,
...asset,
}),
noResizePath: Object.freeze({
@@ -125,6 +126,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
noWebpPath: Object.freeze({
@@ -166,6 +168,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
noThumbhash: Object.freeze({
@@ -204,6 +207,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
primaryImage: Object.freeze({
@@ -252,6 +256,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
image: Object.freeze({
@@ -298,6 +303,7 @@ export const assetStub = {
width: null,
visibility: AssetVisibility.Timeline,
edits: [],
editCount: 0,
}),
trashed: Object.freeze({
@@ -341,6 +347,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
trashedOffline: Object.freeze({
@@ -384,6 +391,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
archived: Object.freeze({
id: 'asset-id',
@@ -426,6 +434,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
external: Object.freeze({
@@ -468,6 +477,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
image1: Object.freeze({
@@ -510,6 +520,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
imageFrom2015: Object.freeze({
@@ -551,6 +562,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
video: Object.freeze({
@@ -594,6 +606,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
livePhotoMotionAsset: Object.freeze({
@@ -614,6 +627,7 @@ export const assetStub = {
width: null,
height: null,
edits: [] as AssetEditActionItem[],
editCount: 0,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: AssetEditActionItem[] }),
livePhotoStillAsset: Object.freeze({
@@ -635,6 +649,7 @@ export const assetStub = {
width: null,
height: null,
edits: [] as AssetEditActionItem[],
editCount: 0,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
livePhotoWithOriginalFileName: Object.freeze({
@@ -658,6 +673,7 @@ export const assetStub = {
width: null,
height: null,
edits: [] as AssetEditActionItem[],
editCount: 0,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
withLocation: Object.freeze({
@@ -705,6 +721,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
sidecar: Object.freeze({
@@ -743,6 +760,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
sidecarWithoutExt: Object.freeze({
@@ -778,6 +796,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
hasEncodedVideo: Object.freeze({
@@ -820,6 +839,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
hasFileExtension: Object.freeze({
@@ -859,6 +879,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
imageDng: Object.freeze({
@@ -902,6 +923,7 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
imageHif: Object.freeze({
@@ -945,7 +967,9 @@ export const assetStub = {
width: null,
height: null,
edits: [],
editCount: 0,
}),
panoramaTif: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
@@ -988,6 +1012,7 @@ export const assetStub = {
height: null,
edits: [],
}),
withCropEdit: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
@@ -1043,7 +1068,9 @@ export const assetStub = {
},
},
] as AssetEditActionItem[],
editCount: 1,
}),
withoutEdits: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
@@ -1089,5 +1116,6 @@ export const assetStub = {
width: 2160,
visibility: AssetVisibility.Timeline,
edits: [],
editCount: 0,
}),
};
+1
View File
@@ -159,6 +159,7 @@ export const sharedLinkStub = {
visibility: AssetVisibility.Timeline,
width: 500,
height: 500,
editCount: 0,
},
],
albumId: null,
+3
View File
@@ -19,6 +19,7 @@ import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
@@ -384,6 +385,7 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
case AlbumUserRepository:
case ActivityRepository:
case AssetRepository:
case AssetEditRepository:
case AssetJobRepository:
case MemoryRepository:
case NotificationRepository:
@@ -535,6 +537,7 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
fileModifiedAt: now,
localDateTime: now,
visibility: AssetVisibility.Timeline,
editCount: 0,
};
return {
@@ -0,0 +1,111 @@
import { Kysely } from 'kysely';
import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto';
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { DB } from 'src/schema';
import { BaseService } from 'src/services/base.service';
import { newMediumService } from 'test/medium.factory';
import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>;
const setup = (db?: Kysely<DB>) => {
const { ctx } = newMediumService(BaseService, {
database: db || defaultDatabase,
real: [],
mock: [LoggingRepository],
});
return { ctx, sut: ctx.get(AssetEditRepository) };
};
beforeAll(async () => {
defaultDatabase = await getKyselyDB();
});
describe(AssetEditRepository.name, () => {
describe('replaceAll', () => {
it('should increment editCount on insert', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 0 });
await sut.replaceAll(asset.id, [
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
]);
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 1 });
});
it('should increment editCount when inserting multiple edits', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 0 });
await sut.replaceAll(asset.id, [
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
]);
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 3 });
});
it('should decrement editCount', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 0 });
await sut.replaceAll(asset.id, [
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
]);
await sut.replaceAll(asset.id, [
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
]);
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 1 });
});
it('should set editCount to 0 if all edits are deleted', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 0 });
await sut.replaceAll(asset.id, [
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
]);
await sut.replaceAll(asset.id, []);
await expect(
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
).resolves.toEqual({ editCount: 0 });
});
});
});
@@ -83,6 +83,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
libraryId: asset.libraryId,
width: asset.width,
height: asset.height,
editCount: asset.editCount,
},
type: SyncEntityType.AlbumAssetCreateV1,
},
@@ -64,6 +64,7 @@ describe(SyncEntityType.AssetV1, () => {
libraryId: asset.libraryId,
width: asset.width,
height: asset.height,
editCount: asset.editCount,
},
type: 'AssetV1',
},
@@ -63,6 +63,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
type: asset.type,
visibility: asset.visibility,
duration: asset.duration,
editCount: asset.editCount,
stackId: null,
livePhotoVideoId: null,
libraryId: asset.libraryId,
+1
View File
@@ -253,6 +253,7 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
visibility: AssetVisibility.Timeline,
width: null,
height: null,
editCount: 0,
...asset,
});