hunk.code_window:       newExif.longitude = exifData?.GPSLongitude || null;
         newExif.livePhotoCID = exifData?.MediaGroupUUID || null;
         if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
           const motionAsset = await this.assetCore.findLivePhotoMatch(newExif.livePhotoCID, asset.id, AssetType.VIDEO);
           if (motionAsset) {
             await this.assetCore.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
             await this.assetCore.save({ id: motionAsset.id, isVisible: false });
           }
         }
  
hunk.labels: keep keep keep keep replace keep keep keep keep keep
hunk.after_edit:         const motionAsset = await this.assetCore.findLivePhotoMatch({
             livePhotoCID: newExif.livePhotoCID,
             otherAssetId: asset.id,
             ownerId: asset.ownerId,
             type: AssetType.VIDEO,
           });
  
hunk.file_path: server/apps/microservices/src/processors/metadata-extraction.processor.ts
hunk.type: replace
file: import { AssetEntity, AssetType, SystemConfig } from '@app/infra/entities';
  import { Logger } from '@nestjs/common';
  import handlebar from 'handlebars';
  import * as luxon from 'luxon';
  import path from 'node:path';
  import sanitize from 'sanitize-filename';
  import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
  import {
    ISystemConfigRepository,
    supportedDayTokens,
    supportedHourTokens,
    supportedMinuteTokens,
    supportedMonthTokens,
    supportedSecondTokens,
    supportedYearTokens,
  } from '../system-config';
  import { SystemConfigCore } from '../system-config/system-config.core';
  
  export class StorageTemplateCore {
    private logger = new Logger(StorageTemplateCore.name);
    private configCore: SystemConfigCore;
    private storageTemplate: HandlebarsTemplateDelegate<any>;
    private storageCore = new StorageCore();
  
    constructor(
      configRepository: ISystemConfigRepository,
      config: SystemConfig,
      private storageRepository: IStorageRepository,
    ) {
      this.storageTemplate = this.compile(config.storageTemplate.template);
      this.configCore = new SystemConfigCore(configRepository);
      this.configCore.addValidator((config) => this.validateConfig(config));
      this.configCore.config$.subscribe((config) => this.onConfig(config));
    }
  
    public async getTemplatePath(asset: AssetEntity, filename: string): Promise<string> {
      try {
        const source = asset.originalPath;
        const ext = path.extname(source).split('.').pop() as string;
        const sanitized = sanitize(path.basename(filename, `.${ext}`));
        const rootPath = this.storageCore.getFolderLocation(StorageFolder.LIBRARY, asset.ownerId);
        const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
        const fullPath = path.normalize(path.join(rootPath, storagePath));
        let destination = `${fullPath}.${ext}`;
  
        if (!fullPath.startsWith(rootPath)) {
          this.logger.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`);
          return source;
        }
  
        if (source === destination) {
          return source;
        }
  
        /**
         * In case of migrating duplicate filename to a new path, we need to check if it is already migrated
         * Due to the mechanism of appending +1, +2, +3, etc to the filename
         *
         * Example:
         * Source = upload/abc/def/FullSizeRender+7.heic
         * Expected Destination = upload/abc/def/FullSizeRender.heic
         *
         * The file is already at the correct location, but since there are other FullSizeRender.heic files in the
         * destination, it was renamed to FullSizeRender+7.heic.
         *
         * The lines below will be used to check if the differences between the source and destination is only the
         * +7 suffix, and if so, it will be considered as already migrated.
         */
        if (source.startsWith(fullPath) && source.endsWith(`.${ext}`)) {
          const diff = source.replace(fullPath, '').replace(`.${ext}`, '');
          const hasDuplicationAnnotation = /^\+\d+$/.test(diff);
          if (hasDuplicationAnnotation) {
            return source;
          }
        }
  
        let duplicateCount = 0;
  
        while (true) {
          const exists = await this.storageRepository.checkFileExists(destination);
          if (!exists) {
            break;
          }
  
          duplicateCount++;
          destination = `${fullPath}+${duplicateCount}.${ext}`;
        }
  
        return destination;
      } catch (error: any) {
        this.logger.error(`Unable to get template path for ${filename}`, error);
        return asset.originalPath;
      }
    }
  
    private validateConfig(config: SystemConfig) {
      this.validateStorageTemplate(config.storageTemplate.template);
    }
  
    private validateStorageTemplate(templateString: string) {
      try {
        const template = this.compile(templateString);
        // test render an asset
        this.render(
          template,
          {
            fileCreatedAt: new Date().toISOString(),
            originalPath: '/upload/test/IMG_123.jpg',
            type: AssetType.IMAGE,
          } as AssetEntity,
          'IMG_123',
          'jpg',
        );
      } catch (e) {
        this.logger.warn(`Storage template validation failed: ${JSON.stringify(e)}`);
        throw new Error(`Invalid storage template: ${e}`);
      }
    }
  
    private onConfig(config: SystemConfig) {
      this.logger.debug(`Received new config, recompiling storage template: ${config.storageTemplate.template}`);
      this.storageTemplate = this.compile(config.storageTemplate.template);
    }
  
    private compile(template: string) {
      return handlebar.compile(template, {
        knownHelpers: undefined,
        strict: true,
      });
    }
  
    private render(template: HandlebarsTemplateDelegate<any>, asset: AssetEntity, filename: string, ext: string) {
      const substitutions: Record<string, string> = {
        filename,
        ext,
        filetype: asset.type == AssetType.IMAGE ? 'IMG' : 'VID',
        filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
      };
  
      const dt = luxon.DateTime.fromISO(new Date(asset.fileCreatedAt).toISOString());
  
      const dateTokens = [
        ...supportedYearTokens,
        ...supportedMonthTokens,
        ...supportedDayTokens,
        ...supportedHourTokens,
        ...supportedMinuteTokens,
        ...supportedSecondTokens,
      ];
  
      for (const token of dateTokens) {
        substitutions[token] = dt.toFormat(token);
      }
  
      return template(substitutions);
    }
  }
  