mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
fix(server): add missing file extensions to library files (#8342)
* fix file extensions * fix tests * fix formatting * fixed bug * fix merts comments
This commit is contained in:
parent
3f61019ca1
commit
ec48fccb30
@ -501,17 +501,17 @@ describe(LibraryService.name, () => {
|
|||||||
const mockLibraryJob: ILibraryFileJob = {
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
ownerId: mockUser.id,
|
ownerId: mockUser.id,
|
||||||
assetPath: '/data/user1/photo.jpg',
|
assetPath: assetStub.hasFileExtension.originalPath,
|
||||||
force: false,
|
force: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
storageMock.stat.mockResolvedValue({
|
storageMock.stat.mockResolvedValue({
|
||||||
size: 100,
|
size: 100,
|
||||||
mtime: assetStub.image.fileModifiedAt,
|
mtime: assetStub.hasFileExtension.fileModifiedAt,
|
||||||
ctime: new Date('2023-01-01'),
|
ctime: new Date('2023-01-01'),
|
||||||
} as Stats);
|
} as Stats);
|
||||||
|
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
||||||
|
|
||||||
@ -548,6 +548,26 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should import an asset that is missing a file extension', async () => {
|
||||||
|
// This tests for the case where the file extension is missing from the asset path.
|
||||||
|
// This happened in previous versions of Immich
|
||||||
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
|
id: libraryStub.externalLibrary1.id,
|
||||||
|
ownerId: mockUser.id,
|
||||||
|
assetPath: assetStub.missingFileExtension.originalPath,
|
||||||
|
force: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.missingFileExtension);
|
||||||
|
|
||||||
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(
|
||||||
|
[assetStub.missingFileExtension.id],
|
||||||
|
expect.objectContaining({ originalFileName: 'photo.jpg' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should set a missing asset to offline', async () => {
|
it('should set a missing asset to offline', async () => {
|
||||||
storageMock.stat.mockRejectedValue(new Error('Path not found'));
|
storageMock.stat.mockRejectedValue(new Error('Path not found'));
|
||||||
|
|
||||||
@ -618,19 +638,20 @@ describe(LibraryService.name, () => {
|
|||||||
it('should refresh an existing asset if forced', async () => {
|
it('should refresh an existing asset if forced', async () => {
|
||||||
const mockLibraryJob: ILibraryFileJob = {
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
ownerId: assetStub.image.ownerId,
|
ownerId: assetStub.hasFileExtension.ownerId,
|
||||||
assetPath: '/data/user1/photo.jpg',
|
assetPath: assetStub.hasFileExtension.originalPath,
|
||||||
force: true,
|
force: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.hasFileExtension);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], {
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasFileExtension.id], {
|
||||||
fileCreatedAt: new Date('2023-01-01'),
|
fileCreatedAt: new Date('2023-01-01'),
|
||||||
fileModifiedAt: new Date('2023-01-01'),
|
fileModifiedAt: new Date('2023-01-01'),
|
||||||
|
originalFileName: assetStub.hasFileExtension.originalFileName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -443,6 +443,8 @@ export class LibraryService extends EventEmitter {
|
|||||||
doRefresh = true;
|
doRefresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalFileName = parse(assetPath).base;
|
||||||
|
|
||||||
if (!existingAssetEntity) {
|
if (!existingAssetEntity) {
|
||||||
// This asset is new to us, read it from disk
|
// This asset is new to us, read it from disk
|
||||||
this.logger.debug(`Importing new asset: ${assetPath}`);
|
this.logger.debug(`Importing new asset: ${assetPath}`);
|
||||||
@ -453,6 +455,12 @@ export class LibraryService extends EventEmitter {
|
|||||||
`File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`,
|
`File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`,
|
||||||
);
|
);
|
||||||
doRefresh = true;
|
doRefresh = true;
|
||||||
|
} else if (existingAssetEntity.originalFileName !== originalFileName) {
|
||||||
|
// TODO: We can likely remove this check in the second half of 2024 when all assets have likely been re-imported by all users
|
||||||
|
this.logger.debug(
|
||||||
|
`Asset is missing file extension, re-importing: ${assetPath}. Current incorrect filename: ${existingAssetEntity.originalFileName}.`,
|
||||||
|
);
|
||||||
|
doRefresh = true;
|
||||||
} else if (!job.force && stats && !existingAssetEntity.isOffline) {
|
} else if (!job.force && stats && !existingAssetEntity.isOffline) {
|
||||||
// Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing
|
// Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing
|
||||||
this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`);
|
this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`);
|
||||||
@ -510,7 +518,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
localDateTime: stats.mtime,
|
localDateTime: stats.mtime,
|
||||||
type: assetType,
|
type: assetType,
|
||||||
originalFileName: parse(assetPath).base,
|
originalFileName,
|
||||||
sidecarPath,
|
sidecarPath,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@ -521,6 +529,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
await this.assetRepository.updateAll([existingAssetEntity.id], {
|
await this.assetRepository.updateAll([existingAssetEntity.id], {
|
||||||
fileCreatedAt: stats.mtime,
|
fileCreatedAt: stats.mtime,
|
||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
|
originalFileName,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Not importing and not refreshing, do nothing
|
// Not importing and not refreshing, do nothing
|
||||||
|
78
server/test/fixtures/asset.stub.ts
vendored
78
server/test/fixtures/asset.stub.ts
vendored
@ -639,4 +639,82 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
}),
|
}),
|
||||||
|
missingFileExtension: Object.freeze<AssetEntity>({
|
||||||
|
id: 'asset-id',
|
||||||
|
deviceAssetId: 'device-asset-id',
|
||||||
|
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
owner: userStub.user1,
|
||||||
|
ownerId: 'user-id',
|
||||||
|
deviceId: 'device-id',
|
||||||
|
originalPath: '/data/user1/photo.jpg',
|
||||||
|
resizePath: '/uploads/user-id/thumbs/path.jpg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
webpPath: '/uploads/user-id/webp/path.ext',
|
||||||
|
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||||
|
encodedVideoPath: null,
|
||||||
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
isFavorite: true,
|
||||||
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isExternal: true,
|
||||||
|
duration: null,
|
||||||
|
isVisible: true,
|
||||||
|
livePhotoVideo: null,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
isOffline: false,
|
||||||
|
libraryId: 'library-id',
|
||||||
|
library: libraryStub.externalLibrary1,
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
originalFileName: 'photo',
|
||||||
|
faces: [],
|
||||||
|
deletedAt: null,
|
||||||
|
sidecarPath: null,
|
||||||
|
exifInfo: {
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
} as ExifEntity,
|
||||||
|
}),
|
||||||
|
hasFileExtension: Object.freeze<AssetEntity>({
|
||||||
|
id: 'asset-id',
|
||||||
|
deviceAssetId: 'device-asset-id',
|
||||||
|
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
owner: userStub.user1,
|
||||||
|
ownerId: 'user-id',
|
||||||
|
deviceId: 'device-id',
|
||||||
|
originalPath: '/data/user1/photo.jpg',
|
||||||
|
resizePath: '/uploads/user-id/thumbs/path.jpg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
webpPath: '/uploads/user-id/webp/path.ext',
|
||||||
|
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||||
|
encodedVideoPath: null,
|
||||||
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
isFavorite: true,
|
||||||
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isExternal: true,
|
||||||
|
duration: null,
|
||||||
|
isVisible: true,
|
||||||
|
livePhotoVideo: null,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
isOffline: false,
|
||||||
|
libraryId: 'library-id',
|
||||||
|
library: libraryStub.externalLibrary1,
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
originalFileName: 'photo.jpg',
|
||||||
|
faces: [],
|
||||||
|
deletedAt: null,
|
||||||
|
sidecarPath: null,
|
||||||
|
exifInfo: {
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
} as ExifEntity,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user