mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	chore(server) harden EXIF extraction (#1347)
* chore(server) Harden EXIF extraction * Remove unused function in timeutil * Remove deadcode
This commit is contained in:
		
							parent
							
								
									dff10e89fe
								
							
						
					
					
						commit
						3e4a14b299
					
				@ -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}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user