diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index 46c7d20ef9..672d39f9af 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -4,10 +4,13 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; import { AlbumEntity } from '@app/database/entities/album.entity'; import { AlbumResponseDto } from './response-dto/album-response.dto'; +import { IAssetRepository } from '../asset/asset-repository'; describe('Album service', () => { let sut: AlbumService; let albumRepositoryMock: jest.Mocked; + let assetRepositoryMock: jest.Mocked; + const authUser: AuthUserDto = Object.freeze({ id: '1111', email: 'auth@test.com', @@ -118,7 +121,22 @@ describe('Album service', () => { getListByAssetId: jest.fn(), getCountByUserId: jest.fn(), }; - sut = new AlbumService(albumRepositoryMock); + + assetRepositoryMock = { + create: jest.fn(), + getAllByUserId: jest.fn(), + getAllByDeviceId: jest.fn(), + getAssetCountByTimeBucket: jest.fn(), + getById: jest.fn(), + getDetectedObjectsByUserId: jest.fn(), + getLocationsByUserId: jest.fn(), + getSearchPropertiesByUserId: jest.fn(), + getAssetByTimeBucket: jest.fn(), + getAssetByChecksum: jest.fn(), + getAssetCountByUserId: jest.fn(), + }; + + sut = new AlbumService(albumRepositoryMock, assetRepositoryMock); }); it('creates album', async () => { diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 7391719f4b..bad4a8b14b 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -36,6 +36,7 @@ import { import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; +import { timeUtils } from '@app/common/utils'; const fileInfo = promisify(stat); @@ -56,6 +57,18 @@ export class AssetService { mimeType: string, checksum: Buffer, ): Promise { + // Check valid time. + const createdAt = createAssetDto.createdAt; + const modifiedAt = createAssetDto.modifiedAt; + + if (!timeUtils.checkValidTimestamp(createdAt)) { + createAssetDto.createdAt = await timeUtils.getTimestampFromExif(originalPath); + } + + if (!timeUtils.checkValidTimestamp(modifiedAt)) { + createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath); + } + const assetEntity = await this._assetRepository.create( createAssetDto, authUser.id, diff --git a/server/libs/common/src/index.ts b/server/libs/common/src/index.ts index a1be68099c..9128d1af46 100644 --- a/server/libs/common/src/index.ts +++ b/server/libs/common/src/index.ts @@ -1,2 +1,3 @@ export * from './config'; export * from './constants'; +export * from './utils'; diff --git a/server/libs/common/src/utils/index.ts b/server/libs/common/src/utils/index.ts new file mode 100644 index 0000000000..c276308273 --- /dev/null +++ b/server/libs/common/src/utils/index.ts @@ -0,0 +1 @@ +export * from './time-utils'; diff --git a/server/libs/common/src/utils/time-utils.spec.ts b/server/libs/common/src/utils/time-utils.spec.ts new file mode 100644 index 0000000000..fd1ed933ec --- /dev/null +++ b/server/libs/common/src/utils/time-utils.spec.ts @@ -0,0 +1,37 @@ +// create unit test for time utils + +import { timeUtils } from './time-utils'; + +describe('Time Utilities', () => { + describe('checkValidTimestamp', () => { + it('check for year 0000', () => { + const result = timeUtils.checkValidTimestamp('0000-00-00T00:00:00.000Z'); + expect(result).toBeFalsy(); + }); + + it('check for 6-digits year with plus sign', () => { + const result = timeUtils.checkValidTimestamp('+12345-00-00T00:00:00.000Z'); + expect(result).toBeFalsy(); + }); + + it('check for 6-digits year with negative sign', () => { + const result = timeUtils.checkValidTimestamp('-12345-00-00T00:00:00.000Z'); + expect(result).toBeFalsy(); + }); + + it('check for current date', () => { + const result = timeUtils.checkValidTimestamp(new Date().toISOString()); + expect(result).toBeTruthy(); + }); + + it('check for year before 1583', () => { + const result = timeUtils.checkValidTimestamp('1582-12-31T23:59:59.999Z'); + expect(result).toBeFalsy(); + }); + + it('check for year after 9999', () => { + const result = timeUtils.checkValidTimestamp('10000-00-00T00:00:00.000Z'); + expect(result).toBeFalsy(); + }); + }); +}); diff --git a/server/libs/common/src/utils/time-utils.ts b/server/libs/common/src/utils/time-utils.ts new file mode 100644 index 0000000000..23c5917aa0 --- /dev/null +++ b/server/libs/common/src/utils/time-utils.ts @@ -0,0 +1,48 @@ +import exifr from 'exifr'; + +function createTimeUtils() { + const checkValidTimestamp = (timestamp: string): boolean => { + const parsedTimestamp = Date.parse(timestamp); + + if (isNaN(parsedTimestamp)) { + return false; + } + + const date = new Date(parsedTimestamp); + + if (date.getFullYear() < 1583 || date.getFullYear() > 9999) { + return false; + } + + return date.getFullYear() > 0; + }; + + const getTimestampFromExif = async (originalPath: string): Promise => { + try { + const exifData = await exifr.parse(originalPath, { + tiff: true, + ifd0: true as any, + ifd1: true, + exif: true, + gps: true, + interop: true, + xmp: true, + icc: true, + iptc: true, + jfif: true, + ihdr: true, + }); + + if (exifData && exifData['DateTimeOriginal']) { + return exifData['DateTimeOriginal']; + } else { + return new Date().toISOString(); + } + } catch (error) { + return new Date().toISOString(); + } + }; + return { checkValidTimestamp, getTimestampFromExif }; +} + +export const timeUtils = createTimeUtils(); diff --git a/server/package.json b/server/package.json index fb90a9af74..ede5b8d625 100644 --- a/server/package.json +++ b/server/package.json @@ -129,6 +129,7 @@ "^@app/database(|/.*)$": "/libs/database/src/$1", "@app/database/config/(.*)": "/libs/database/src/config/$1", "@app/database/config": "/libs/database/src/config", + "@app/common": "/libs/common/src", "^@app/job(|/.*)$": "/libs/job/src/$1" } }