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
7 changed files with 120 additions and 1 deletions
|
@ -4,10 +4,13 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { AlbumEntity } from '@app/database/entities/album.entity';
|
import { AlbumEntity } from '@app/database/entities/album.entity';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
|
import { IAssetRepository } from '../asset/asset-repository';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||||
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||||
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: '1111',
|
id: '1111',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
|
@ -118,7 +121,22 @@ describe('Album service', () => {
|
||||||
getListByAssetId: jest.fn(),
|
getListByAssetId: jest.fn(),
|
||||||
getCountByUserId: 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 () => {
|
it('creates album', async () => {
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {
|
||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-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 { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
import { timeUtils } from '@app/common/utils';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
|
@ -56,6 +57,18 @@ export class AssetService {
|
||||||
mimeType: string,
|
mimeType: string,
|
||||||
checksum: Buffer,
|
checksum: Buffer,
|
||||||
): Promise<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(modifiedAt)) {
|
||||||
|
createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath);
|
||||||
|
}
|
||||||
|
|
||||||
const assetEntity = await this._assetRepository.create(
|
const assetEntity = await this._assetRepository.create(
|
||||||
createAssetDto,
|
createAssetDto,
|
||||||
authUser.id,
|
authUser.id,
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './config';
|
export * from './config';
|
||||||
export * from './constants';
|
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(|/.*)$": "<rootDir>/libs/database/src/$1",
|
||||||
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
|
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
|
||||||
"@app/database/config": "<rootDir>/libs/database/src/config",
|
"@app/database/config": "<rootDir>/libs/database/src/config",
|
||||||
|
"@app/common": "<rootDir>/libs/common/src",
|
||||||
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
|
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue