Skip to content

Commit e83dd40

Browse files
alextran1502kirill-dev-pro
authored andcommitted
fix(server): double extension when filename has uppercase extension (immich-app#17226)
* fix(server): double extension when filename has uppercase extension * Proper tests
1 parent 8fa4a30 commit e83dd40

File tree

2 files changed

+101
-1
lines changed

2 files changed

+101
-1
lines changed

server/src/services/storage-template.service.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,4 +548,102 @@ describe(StorageTemplateService.name, () => {
548548
expect(mocks.asset.update).not.toHaveBeenCalled();
549549
});
550550
});
551+
552+
describe('file rename correctness', () => {
553+
it('should not create double extensions when filename has lower extension', async () => {
554+
const asset = assetStub.storageAsset({
555+
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic',
556+
originalFileName: 'IMG_7065.HEIC',
557+
});
558+
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
559+
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
560+
mocks.move.create.mockResolvedValue({
561+
id: '123',
562+
entityId: asset.id,
563+
pathType: AssetPathType.ORIGINAL,
564+
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic',
565+
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.heic',
566+
});
567+
568+
await sut.handleMigration();
569+
570+
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
571+
expect(mocks.storage.rename).toHaveBeenCalledWith(
572+
'upload/library/user-id/2022/2022-06-19/IMG_7065.heic',
573+
'upload/library/label-1/2022/2022-06-19/IMG_7065.heic',
574+
);
575+
});
576+
577+
it('should not create double extensions when filename has uppercase extension', async () => {
578+
const asset = assetStub.storageAsset({
579+
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC',
580+
originalFileName: 'IMG_7065.HEIC',
581+
});
582+
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
583+
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
584+
mocks.move.create.mockResolvedValue({
585+
id: '123',
586+
entityId: asset.id,
587+
pathType: AssetPathType.ORIGINAL,
588+
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC',
589+
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.heic',
590+
});
591+
592+
await sut.handleMigration();
593+
594+
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
595+
expect(mocks.storage.rename).toHaveBeenCalledWith(
596+
'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC',
597+
'upload/library/label-1/2022/2022-06-19/IMG_7065.heic',
598+
);
599+
});
600+
601+
it('should normalize the filename to lowercase (JPEG > jpg)', async () => {
602+
const asset = assetStub.storageAsset({
603+
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG',
604+
originalFileName: 'IMG_7065.JPEG',
605+
});
606+
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
607+
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
608+
mocks.move.create.mockResolvedValue({
609+
id: '123',
610+
entityId: asset.id,
611+
pathType: AssetPathType.ORIGINAL,
612+
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG',
613+
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg',
614+
});
615+
616+
await sut.handleMigration();
617+
618+
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
619+
expect(mocks.storage.rename).toHaveBeenCalledWith(
620+
'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG',
621+
'upload/library/label-1/2022/2022-06-19/IMG_7065.jpg',
622+
);
623+
});
624+
625+
it('should normalize the filename to lowercase (JPG > jpg)', async () => {
626+
const asset = assetStub.storageAsset({
627+
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
628+
originalFileName: 'IMG_7065.JPG',
629+
});
630+
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
631+
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
632+
mocks.move.create.mockResolvedValue({
633+
id: '123',
634+
entityId: asset.id,
635+
pathType: AssetPathType.ORIGINAL,
636+
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
637+
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg',
638+
});
639+
640+
await sut.handleMigration();
641+
642+
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
643+
expect(mocks.storage.rename).toHaveBeenCalledWith(
644+
'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
645+
'upload/library/label-1/2022/2022-06-19/IMG_7065.jpg',
646+
);
647+
});
648+
});
551649
});

server/src/services/storage-template.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,11 @@ export class StorageTemplateService extends BaseService {
220220
const { storageLabel, filename } = metadata;
221221

222222
try {
223+
const filenameWithoutExtension = path.basename(filename, path.extname(filename));
224+
223225
const source = asset.originalPath;
224226
let extension = path.extname(source).split('.').pop() as string;
225-
const sanitized = sanitize(path.basename(filename, `.${extension}`));
227+
const sanitized = sanitize(path.basename(filenameWithoutExtension, `.${extension}`));
226228
extension = extension?.toLowerCase();
227229
const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
228230

0 commit comments

Comments
 (0)