mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
fix(server): harden inserting process, self-healing timestamp info on bad timestamp (#682)
* fix(server): harden inserting process, self-healing timestamp info
This commit is contained in:
parent
5761765ea7
commit
858ad43d3b
@ -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<IAlbumRepository>;
|
||||
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||
|
||||
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 () => {
|
||||
|
@ -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<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(modifiedAt)) {
|
||||
createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath);
|
||||
}
|
||||
|
||||
const assetEntity = await this._assetRepository.create(
|
||||
createAssetDto,
|
||||
authUser.id,
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './config';
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
|
1
server/libs/common/src/utils/index.ts
Normal file
1
server/libs/common/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './time-utils';
|
37
server/libs/common/src/utils/time-utils.spec.ts
Normal file
37
server/libs/common/src/utils/time-utils.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
48
server/libs/common/src/utils/time-utils.ts
Normal file
48
server/libs/common/src/utils/time-utils.ts
Normal file
@ -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<string> => {
|
||||
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();
|
@ -129,6 +129,7 @@
|
||||
"^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
|
||||
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
|
||||
"@app/database/config": "<rootDir>/libs/database/src/config",
|
||||
"@app/common": "<rootDir>/libs/common/src",
|
||||
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user