mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	chore(server) harden EXIF extraction (#1347)
* chore(server) Harden EXIF extraction * Remove unused function in timeutil * Remove deadcode
This commit is contained in:
		| @@ -188,16 +188,12 @@ export class AssetService { | ||||
|     isVisible: boolean, | ||||
|     livePhotoAssetEntity?: AssetEntity, | ||||
|   ): Promise<AssetEntity> { | ||||
|     // Check valid time. | ||||
|     const createdAt = createAssetDto.createdAt; | ||||
|     const modifiedAt = createAssetDto.modifiedAt; | ||||
|  | ||||
|     if (!timeUtils.checkValidTimestamp(createdAt)) { | ||||
|       createAssetDto.createdAt = await timeUtils.getTimestampFromExif(originalPath); | ||||
|     if (!timeUtils.checkValidTimestamp(createAssetDto.createdAt)) { | ||||
|       createAssetDto.createdAt = new Date().toISOString(); | ||||
|     } | ||||
|  | ||||
|     if (!timeUtils.checkValidTimestamp(modifiedAt)) { | ||||
|       createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath); | ||||
|     if (!timeUtils.checkValidTimestamp(createAssetDto.modifiedAt)) { | ||||
|       createAssetDto.modifiedAt = new Date().toISOString(); | ||||
|     } | ||||
|  | ||||
|     const assetEntity = await this._assetRepository.create( | ||||
|   | ||||
| @@ -18,8 +18,7 @@ import { Repository } from 'typeorm/repository/Repository'; | ||||
| import geocoder, { InitOptions } from 'local-reverse-geocoder'; | ||||
| import { getName } from 'i18n-iso-countries'; | ||||
| import fs from 'node:fs'; | ||||
| import { ExifDateTime, ExifTool } from 'exiftool-vendored'; | ||||
| import { timeUtils } from '@app/common'; | ||||
| import { ExifDateTime, exiftool } from 'exiftool-vendored'; | ||||
|  | ||||
| function geocoderInit(init: InitOptions) { | ||||
|   return new Promise<void>(function (resolve) { | ||||
| @@ -140,43 +139,52 @@ export class MetadataExtractionProcessor { | ||||
|   async extractExifInfo(job: Job<IExifExtractionProcessor>) { | ||||
|     try { | ||||
|       const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; | ||||
|       const exiftool = new ExifTool(); | ||||
|  | ||||
|       const exifData = await exiftool.read(asset.originalPath).catch((e) => { | ||||
|         this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`); | ||||
|         return null; | ||||
|       }); | ||||
|  | ||||
|       const exifToDate = (exifDate: string | ExifDateTime | undefined) => | ||||
|         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||
|         exifDate ? new Date(exifDate.toString()!) : null; | ||||
|       const exifToDate = (exifDate: string | ExifDateTime | undefined) => { | ||||
|         if (!exifDate) return null; | ||||
|  | ||||
|       let createdAt = exifToDate(asset.createdAt); | ||||
|       const newExif = new ExifEntity(); | ||||
|       if (exifData) { | ||||
|         createdAt = exifToDate(exifData.DateTimeOriginal ?? exifData.CreateDate ?? asset.createdAt); | ||||
|         const modifyDate = exifToDate(exifData.ModifyDate); | ||||
|         newExif.make = exifData['Make'] || null; | ||||
|         newExif.model = exifData['Model'] || null; | ||||
|         newExif.exifImageHeight = exifData['ExifImageHeight'] || exifData['ImageHeight'] || null; | ||||
|         newExif.exifImageWidth = exifData['ExifImageWidth'] || exifData['ImageWidth'] || null; | ||||
|         newExif.exposureTime = (await timeUtils.parseStringToNumber(exifData['ExposureTime'])) || null; | ||||
|         newExif.orientation = exifData['Orientation']?.toString() || null; | ||||
|         newExif.dateTimeOriginal = createdAt; | ||||
|         newExif.modifyDate = modifyDate || null; | ||||
|         newExif.lensModel = exifData['LensModel'] || null; | ||||
|         newExif.fNumber = exifData['FNumber'] || null; | ||||
|         newExif.focalLength = (await timeUtils.parseStringToNumber(exifData['FocalLength'])) || null; | ||||
|         newExif.iso = exifData['ISO'] || null; | ||||
|         newExif.latitude = exifData['GPSLatitude'] || null; | ||||
|         newExif.longitude = exifData['GPSLongitude'] || null; | ||||
|       } else { | ||||
|         newExif.dateTimeOriginal = createdAt; | ||||
|         newExif.modifyDate = exifToDate(asset.modifiedAt); | ||||
|         if (typeof exifDate === 'string') { | ||||
|           return new Date(exifDate); | ||||
|         } | ||||
|  | ||||
|         return exifDate.toDate(); | ||||
|       }; | ||||
|  | ||||
|       const getExposureTimeDenominator = (exposureTime: string | undefined) => { | ||||
|         if (!exposureTime) return null; | ||||
|  | ||||
|         const exposureTimeSplit = exposureTime.split('/'); | ||||
|         return exposureTimeSplit.length === 2 ? parseInt(exposureTimeSplit[1]) : null; | ||||
|       }; | ||||
|  | ||||
|       const createdAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.createdAt); | ||||
|       const modifyDate = exifToDate(exifData?.ModifyDate ?? asset.modifiedAt); | ||||
|       const fileStats = fs.statSync(asset.originalPath); | ||||
|       const fileSizeInBytes = fileStats.size; | ||||
|  | ||||
|       const newExif = new ExifEntity(); | ||||
|       newExif.assetId = asset.id; | ||||
|       newExif.imageName = path.parse(fileName).name || null; | ||||
|       newExif.fileSizeInByte = fileSizeInBytes || null; | ||||
|       newExif.imageName = path.parse(fileName).name; | ||||
|       newExif.fileSizeInByte = fileSizeInBytes; | ||||
|       newExif.make = exifData?.Make || null; | ||||
|       newExif.model = exifData?.Model || null; | ||||
|       newExif.exifImageHeight = exifData?.ExifImageHeight || exifData?.ImageHeight || null; | ||||
|       newExif.exifImageWidth = exifData?.ExifImageWidth || exifData?.ImageWidth || null; | ||||
|       newExif.exposureTime = getExposureTimeDenominator(exifData?.ExposureTime); | ||||
|       newExif.orientation = exifData?.Orientation?.toString() || null; | ||||
|       newExif.dateTimeOriginal = createdAt; | ||||
|       newExif.modifyDate = modifyDate; | ||||
|       newExif.lensModel = exifData?.LensModel || null; | ||||
|       newExif.fNumber = exifData?.FNumber || null; | ||||
|       newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null; | ||||
|       newExif.iso = exifData?.ISO || null; | ||||
|       newExif.latitude = exifData?.GPSLatitude || null; | ||||
|       newExif.longitude = exifData?.GPSLongitude || null; | ||||
|  | ||||
|       await this.assetRepository.save({ | ||||
|         id: asset.id, | ||||
| @@ -217,7 +225,6 @@ export class MetadataExtractionProcessor { | ||||
|       } | ||||
|  | ||||
|       await this.exifRepository.save(newExif); | ||||
|       await exiftool.end(); | ||||
|     } catch (error: any) { | ||||
|       this.logger.error(`Error extracting EXIF ${error}`, error?.stack); | ||||
|     } | ||||
|   | ||||
| @@ -1,12 +1,4 @@ | ||||
| // This is needed as resolving for the vendored | ||||
| // exiftool fails in tests otherwise but as it's not meant to be a requirement | ||||
| // of a project directly I had to include the line below the comment. | ||||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||
| // @ts-ignore | ||||
| import { exiftool } from 'exiftool-vendored.pl'; | ||||
|  | ||||
| function createTimeUtils() { | ||||
|   const floatRegex = /[+-]?([0-9]*[.])?[0-9]+/; | ||||
|   const checkValidTimestamp = (timestamp: string): boolean => { | ||||
|     const parsedTimestamp = Date.parse(timestamp); | ||||
|  | ||||
| @@ -23,32 +15,7 @@ function createTimeUtils() { | ||||
|     return date.getFullYear() > 0; | ||||
|   }; | ||||
|  | ||||
|   const getTimestampFromExif = async (originalPath: string): Promise<string> => { | ||||
|     try { | ||||
|       const exifData = await exiftool.read(originalPath); | ||||
|  | ||||
|       if (exifData && exifData['DateTimeOriginal']) { | ||||
|         await exiftool.end(); | ||||
|         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||
|         return exifData['DateTimeOriginal'].toString()!; | ||||
|       } else { | ||||
|         return new Date().toISOString(); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       return new Date().toISOString(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const parseStringToNumber = async (original: string | undefined): Promise<number | null> => { | ||||
|     const match = original?.match(floatRegex)?.[0]; | ||||
|     if (match) { | ||||
|       return parseFloat(match); | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return { checkValidTimestamp, getTimestampFromExif, parseStringToNumber }; | ||||
|   return { checkValidTimestamp }; | ||||
| } | ||||
|  | ||||
| export const timeUtils = createTimeUtils(); | ||||
|   | ||||
| @@ -152,7 +152,7 @@ | ||||
| 						<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString(locale)}` || ''}</p> | ||||
|  | ||||
| 						{#if asset.exifInfo.exposureTime} | ||||
| 							<p>{`1/${Math.floor(1 / asset.exifInfo.exposureTime)}`}</p> | ||||
| 							<p>{`1/${asset.exifInfo.exposureTime}`}</p> | ||||
| 						{/if} | ||||
|  | ||||
| 						{#if asset.exifInfo.focalLength} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user