mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05: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