chore!: remove deviceId and deviceAssetId (#27818)

chore: remove deviceId and deviceAssetId
This commit is contained in:
Daniel Dietzler
2026-04-15 21:00:33 +02:00
committed by GitHub
parent d410131312
commit 8ee5d3039a
55 changed files with 31 additions and 1102 deletions
@@ -9,8 +9,6 @@ import { automock, ControllerContext, controllerSetup, mockBaseService } from 't
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
const dto: Record<string, any> = {
deviceAssetId: 'example-image',
deviceId: 'TEST',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
isFavorite: 'false',
@@ -87,28 +85,6 @@ describe(AssetMediaController.name, () => {
);
});
it('should require `deviceAssetId`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'deviceAssetId' }) });
expect(status).toBe(400);
expect(body).toEqual(
factory.responses.badRequest(['[deviceAssetId] Invalid input: expected string, received undefined']),
);
});
it('should require `deviceId`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'deviceId' }) });
expect(status).toBe(400);
expect(body).toEqual(
factory.responses.badRequest(['[deviceId] Invalid input: expected string, received undefined']),
);
});
it('should require `fileCreatedAt`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
@@ -21,14 +21,12 @@ import {
AssetBulkUploadCheckResponseDto,
AssetMediaResponseDto,
AssetMediaStatus,
CheckExistingAssetsResponseDto,
} from 'src/dtos/asset-media-response.dto';
import {
AssetBulkUploadCheckDto,
AssetMediaCreateDto,
AssetMediaOptionsDto,
AssetMediaSize,
CheckExistingAssetsDto,
} from 'src/dtos/asset-media.dto';
import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -179,21 +177,6 @@ export class AssetMediaController {
await sendFile(res, next, () => this.service.playbackVideo(auth, id), this.logger);
}
@Post('exist')
@Authenticated({ permission: Permission.AssetUpload })
@Endpoint({
summary: 'Check existing assets',
description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
})
@HttpCode(HttpStatus.OK)
checkExistingAssets(
@Auth() auth: AuthDto,
@Body() dto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return this.service.checkExistingAssets(auth, dto);
}
@Post('bulk-upload-check')
@Authenticated({ permission: Permission.AssetUpload })
@Endpoint({
@@ -15,7 +15,6 @@ import {
AssetMetadataUpsertDto,
AssetStatsDto,
AssetStatsResponseDto,
DeviceIdDto,
UpdateAssetDto,
} from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -31,17 +30,6 @@ import { UUIDParamDto } from 'src/validation';
export class AssetController {
constructor(private service: AssetService) {}
@Get('/device/:deviceId')
@Endpoint({
summary: 'Retrieve assets by device ID',
description: 'Get all asset of a device that are in the database, ID only.',
history: new HistoryBuilder().added('v1').deprecated('v2'),
})
@Authenticated()
getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(auth, deviceId);
}
@Get('statistics')
@Authenticated({ permission: Permission.AssetStatistics })
@Endpoint({
-4
View File
@@ -114,8 +114,6 @@ export type Asset = {
id: string;
checksum: Buffer<ArrayBufferLike>;
checksumAlgorithm: ChecksumAlgorithm;
deviceAssetId: string;
deviceId: string;
fileCreatedAt: Date;
fileModifiedAt: Date;
isExternal: boolean;
@@ -333,8 +331,6 @@ export const columns = {
'asset.id',
'asset.checksum',
'asset.checksumAlgorithm',
'asset.deviceAssetId',
'asset.deviceId',
'asset.fileCreatedAt',
'asset.fileModifiedAt',
'asset.isExternal',
@@ -49,12 +49,5 @@ const AssetBulkUploadCheckResponseSchema = z
})
.meta({ id: 'AssetBulkUploadCheckResponseDto' });
const CheckExistingAssetsResponseSchema = z
.object({
existingIds: z.array(z.string()).describe('Existing asset IDs'),
})
.meta({ id: 'CheckExistingAssetsResponseDto' });
export class AssetMediaResponseDto extends createZodDto(AssetMediaResponseSchema) {}
export class AssetBulkUploadCheckResponseDto extends createZodDto(AssetBulkUploadCheckResponseSchema) {}
export class CheckExistingAssetsResponseDto extends createZodDto(CheckExistingAssetsResponseSchema) {}
-10
View File
@@ -36,8 +36,6 @@ export enum UploadFieldName {
}
const AssetMediaBaseSchema = z.object({
deviceAssetId: z.string().describe('Device asset ID'),
deviceId: z.string().describe('Device ID'),
fileCreatedAt: isoDatetimeToDate.describe('File creation date'),
fileModifiedAt: isoDatetimeToDate.describe('File modification date'),
duration: z.string().optional().describe('Duration (for videos)'),
@@ -71,14 +69,6 @@ const AssetBulkUploadCheckSchema = z
})
.meta({ id: 'AssetBulkUploadCheckDto' });
const CheckExistingAssetsSchema = z
.object({
deviceAssetIds: z.array(z.string()).min(1).describe('Device asset IDs to check'),
deviceId: z.string().describe('Device ID'),
})
.meta({ id: 'CheckExistingAssetsDto' });
export class AssetMediaOptionsDto extends createZodDto(AssetMediaOptionsSchema) {}
export class AssetMediaCreateDto extends createZodDto(AssetMediaCreateSchema) {}
export class AssetBulkUploadCheckDto extends createZodDto(AssetBulkUploadCheckSchema) {}
export class CheckExistingAssetsDto extends createZodDto(CheckExistingAssetsSchema) {}
-6
View File
@@ -72,8 +72,6 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
.string()
.meta({ format: 'date-time' })
.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
deviceAssetId: z.string().describe('Device asset ID'),
deviceId: z.string().describe('Device ID'),
ownerId: z.string().describe('Owner user ID'),
owner: UserResponseSchema.optional(),
libraryId: z
@@ -137,8 +135,6 @@ export type MapAsset = {
status: AssetStatus;
checksum: Buffer<ArrayBufferLike>;
checksumAlgorithm: ChecksumAlgorithm;
deviceAssetId: string;
deviceId: string;
duplicateId: string | null;
duration: string | null;
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
@@ -239,10 +235,8 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
return {
id: entity.id,
createdAt: asDateString(entity.createdAt),
deviceAssetId: entity.deviceAssetId,
ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined,
deviceId: entity.deviceId,
libraryId: entity.libraryId,
type: entity.type,
originalPath: entity.originalPath,
-7
View File
@@ -6,12 +6,6 @@ import { AssetStats } from 'src/repositories/asset.repository';
import { IsNotSiblingOf, isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation';
import z from 'zod';
const DeviceIdSchema = z
.object({
deviceId: z.string().describe('Device ID'),
})
.meta({ id: 'DeviceIdDto' });
const UpdateAssetBaseSchema = z
.object({
isFavorite: z.boolean().optional().describe('Mark as favorite'),
@@ -182,7 +176,6 @@ export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
};
};
export class DeviceIdDto extends createZodDto(DeviceIdSchema) {}
export class AssetBulkUpdateDto extends createZodDto(AssetBulkUpdateSchema) {}
export class UpdateAssetDto extends createZodDto(UpdateAssetSchema) {}
export class AssetBulkDeleteDto extends createZodDto(AssetBulkDeleteSchema) {}
-2
View File
@@ -9,7 +9,6 @@ import z from 'zod';
const BaseSearchSchema = z.object({
libraryId: z.uuidv4().nullish().describe('Library ID to filter by'),
deviceId: z.string().optional().describe('Device ID to filter by'),
type: AssetTypeSchema.optional(),
isEncoded: z.boolean().optional().describe('Filter by encoded status'),
isFavorite: z.boolean().optional().describe('Filter by favorite status'),
@@ -68,7 +67,6 @@ const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({
const MetadataSearchSchema = RandomSearchSchema.extend({
id: z.uuidv4().optional().describe('Filter by asset ID'),
deviceAssetId: z.string().optional().describe('Filter by device asset ID'),
description: z.string().trim().optional().describe('Filter by description text'),
checksum: z.string().optional().describe('Filter by file checksum'),
originalFileName: z.string().trim().optional().describe('Filter by original file name'),
@@ -250,8 +250,6 @@ select
"asset"."id",
"asset"."checksum",
"asset"."checksumAlgorithm",
"asset"."deviceAssetId",
"asset"."deviceId",
"asset"."fileCreatedAt",
"asset"."fileModifiedAt",
"asset"."isExternal",
+1 -13
View File
@@ -242,17 +242,6 @@ where
limit
$3
-- AssetRepository.getAllByDeviceId
select
"deviceAssetId"
from
"asset"
where
"ownerId" = $1::uuid
and "deviceId" = $2
and "visibility" != $3
and "deletedAt" is null
-- AssetRepository.getLivePhotoCount
select
count(*) as "count"
@@ -312,9 +301,8 @@ limit
-- AssetRepository.updateAll
update "asset"
set
"deviceId" = $1
where
"id" = any ($2::uuid[])
"id" = any ($1::uuid[])
-- AssetRepository.getByChecksum
select
+1 -34
View File
@@ -448,18 +448,6 @@ export class AssetRepository {
await this.db.deleteFrom('asset').where('ownerId', '=', ownerId).execute();
}
async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]> {
const assets = await this.db
.selectFrom('asset')
.select(['deviceAssetId'])
.where('deviceAssetId', 'in', deviceAssetIds)
.where('deviceId', '=', deviceId)
.where('ownerId', '=', asUuid(ownerId))
.execute();
return assets.map((asset) => asset.deviceAssetId);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) {
return this.db
@@ -471,27 +459,6 @@ export class AssetRepository {
.executeTakeFirst();
}
/**
* Get assets by device's Id on the database
* @param ownerId
* @param deviceId
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
const items = await this.db
.selectFrom('asset')
.select(['deviceAssetId'])
.where('ownerId', '=', asUuid(ownerId))
.where('deviceId', '=', deviceId)
.where('visibility', '!=', AssetVisibility.Hidden)
.where('deletedAt', 'is', null)
.execute();
return items.map((asset) => asset.deviceAssetId);
}
@GenerateSql({ params: [DummyValue.UUID] })
async getLivePhotoCount(motionId: string): Promise<number> {
const [{ count }] = await this.db
@@ -568,7 +535,7 @@ export class AssetRepository {
.executeTakeFirst();
}
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
@GenerateSql({ params: [[DummyValue.UUID], {}] })
@Chunked()
async updateAll(ids: string[], options: Updateable<AssetTable>): Promise<void> {
if (ids.length === 0) {
@@ -14,12 +14,10 @@ import { isValidInteger } from 'src/validation';
export interface SearchAssetIdOptions {
checksum?: Buffer;
deviceAssetId?: string;
id?: string;
}
export interface SearchUserIdOptions {
deviceId?: string;
libraryId?: string | null;
userIds?: string[];
}
@@ -0,0 +1,11 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "asset" DROP COLUMN "deviceAssetId";`.execute(db);
await sql`ALTER TABLE "asset" DROP COLUMN "deviceId";`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "asset" ADD "deviceAssetId" character varying NOT NULL;`.execute(db);
await sql`ALTER TABLE "asset" ADD "deviceId" character varying NOT NULL;`.execute(db);
}
-6
View File
@@ -65,15 +65,9 @@ export class AssetTable {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@Column()
deviceAssetId!: string;
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
ownerId!: string;
@Column()
deviceId!: string;
@Column()
type!: AssetType;
@@ -145,8 +145,6 @@ const uploadTests = [
];
const createDto = Object.freeze({
deviceAssetId: 'deviceAssetId',
deviceId: 'deviceId',
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false,
@@ -156,8 +154,6 @@ const createDto = Object.freeze({
const assetEntity = Object.freeze({
id: 'id_1',
ownerId: 'user_id_1',
deviceAssetId: 'device_asset_id_1',
deviceId: 'device_id_1',
type: AssetType.Video,
originalPath: 'fake_path/asset_1.jpeg',
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
@@ -765,17 +761,6 @@ describe(AssetMediaService.name, () => {
});
});
describe('checkExistingAssets', () => {
it('should get existing asset ids', async () => {
mocks.asset.getByDeviceIds.mockResolvedValue(['42']);
await expect(
sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }),
).resolves.toEqual({ existingIds: ['42'] });
expect(mocks.asset.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
});
});
describe('bulkUploadCheck', () => {
it('should accept hex and base64 checksums', async () => {
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
@@ -9,14 +9,12 @@ import {
AssetMediaStatus,
AssetRejectReason,
AssetUploadAction,
CheckExistingAssetsResponseDto,
} from 'src/dtos/asset-media-response.dto';
import {
AssetBulkUploadCheckDto,
AssetMediaCreateDto,
AssetMediaOptionsDto,
AssetMediaSize,
CheckExistingAssetsDto,
UploadFieldName,
} from 'src/dtos/asset-media.dto';
import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto';
@@ -251,18 +249,6 @@ export class AssetMediaService extends BaseService {
});
}
async checkExistingAssets(
auth: AuthDto,
checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
const existingIds = await this.assetRepository.getByDeviceIds(
auth.user.id,
checkExistingAssetsDto.deviceId,
checkExistingAssetsDto.deviceAssetIds,
);
return { existingIds };
}
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
const results = await this.assetRepository.getByChecksums(auth.user.id, checksums);
@@ -340,9 +326,6 @@ export class AssetMediaService extends BaseService {
checksumAlgorithm: ChecksumAlgorithm.sha1File,
originalPath: file.originalPath,
deviceAssetId: dto.deviceAssetId,
deviceId: dto.deviceId,
fileCreatedAt: dto.fileCreatedAt,
fileModifiedAt: dto.fileModifiedAt,
localDateTime: dto.fileCreatedAt,
-14
View File
@@ -685,20 +685,6 @@ describe(AssetService.name, () => {
});
});
describe('getUserAssetsByDeviceId', () => {
it('get assets by device id', async () => {
const assets = [AssetFactory.create(), AssetFactory.create()];
mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
const deviceId = 'device-id';
const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
expect(result.length).toEqual(2);
expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
});
});
describe('upsertMetadata', () => {
it('should throw a bad request exception if duplicate keys are sent', async () => {
const asset = AssetFactory.create();
-4
View File
@@ -59,10 +59,6 @@ export class AssetService extends BaseService {
return mapStats(stats);
}
async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
}
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
@@ -570,7 +570,6 @@ describe(LibraryService.name, () => {
ownerId: library.ownerId,
libraryId: library.id,
originalPath: '/data/user1/photo.jpg',
deviceId: 'Library Import',
type: AssetType.Image,
originalFileName: 'photo.jpg',
isExternal: true,
+1 -4
View File
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { Insertable } from 'kysely';
import { R_OK } from 'node:constants';
import { Stats } from 'node:fs';
import path, { basename, isAbsolute, parse } from 'node:path';
import path, { isAbsolute, parse } from 'node:path';
import picomatch from 'picomatch';
import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
@@ -411,9 +411,6 @@ export class LibraryService extends BaseService {
fileCreatedAt: stat.mtime,
fileModifiedAt: stat.mtime,
localDateTime: stat.mtime,
// TODO: device asset id is deprecated, remove it
deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''),
deviceId: 'Library Import',
type: mimeTypes.isVideo(assetPath) ? AssetType.Video : AssetType.Image,
originalFileName: parse(assetPath).base,
isExternal: true,
@@ -654,8 +654,6 @@ describe(MetadataService.name, () => {
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
id: motionAsset.id,
@@ -708,8 +706,6 @@ describe(MetadataService.name, () => {
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
id: motionAsset.id,
@@ -762,8 +758,6 @@ describe(MetadataService.name, () => {
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
id: motionAsset.id,
-2
View File
@@ -681,8 +681,6 @@ export class MetadataService extends BaseService {
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
originalFileName: `${parse(asset.originalFileName).name}.mp4`,
visibility: AssetVisibility.Hidden,
deviceAssetId: 'NONE',
deviceId: 'NONE',
});
isNewMotionAsset = true;
-2
View File
@@ -351,8 +351,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.where('asset_exif.rating', options.rating === null ? 'is' : '=', options.rating!),
)
.$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!))
.$if(!!options.deviceAssetId, (qb) => qb.where('asset.deviceAssetId', '=', options.deviceAssetId!))
.$if(!!options.deviceId, (qb) => qb.where('asset.deviceId', '=', options.deviceId!))
.$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!)))
.$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!)))
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
-2
View File
@@ -21,8 +21,6 @@ const createAsset = (
width: 1920,
height: 1080,
createdAt: new Date().toISOString(),
deviceAssetId: 'device-asset-1',
deviceId: 'device-1',
ownerId: 'owner-1',
originalPath: '/path/to/asset',
originalFileName: 'asset.jpg',