import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { MoreThan, Repository } from 'typeorm'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { CreateAssetDto } from './dto/create-asset.dto'; import { UpdateAssetDto } from './dto/update-asset.dto'; import { AssetEntity, AssetType } from './entities/asset.entity'; import _, { result } from 'lodash'; import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto'; import { createReadStream, stat } from 'fs'; import { ServeFileDto } from './dto/serve-file.dto'; import { Response as Res } from 'express'; import { promisify } from 'util'; import { DeleteAssetDto } from './dto/delete-asset.dto'; const fileInfo = promisify(stat); @Injectable() export class AssetService { constructor( @InjectRepository(AssetEntity) private assetRepository: Repository, ) {} public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) { const asset = new AssetEntity(); asset.deviceAssetId = assetInfo.deviceAssetId; asset.userId = authUser.id; asset.deviceId = assetInfo.deviceId; asset.type = assetInfo.assetType || AssetType.OTHER; asset.originalPath = path; asset.createdAt = assetInfo.createdAt; asset.modifiedAt = assetInfo.modifiedAt; asset.isFavorite = assetInfo.isFavorite; asset.mimeType = mimeType; asset.duration = assetInfo.duration; try { const res = await this.assetRepository.save(asset); return res; } catch (e) { Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); } } public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { const rows = await this.assetRepository.find({ where: { userId: authUser.id, deviceId: deviceId, }, select: ['deviceAssetId'], }); const res = []; rows.forEach((v) => res.push(v.deviceAssetId)); return res; } public async getAllAssetsNoPagination(authUser: AuthUserDto) { try { const assets = await this.assetRepository .createQueryBuilder('a') .where('a."userId" = :userId', { userId: authUser.id }) .orderBy('a."createdAt"::date', 'DESC') .getMany(); return assets; } catch (e) { Logger.error(e, 'getAllAssets'); } } public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise { try { const assets = await this.assetRepository .createQueryBuilder('a') .where('a."userId" = :userId', { userId: authUser.id }) .andWhere('a."createdAt" < :lastQueryCreatedAt', { lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(), }) .orderBy('a."createdAt"::date', 'DESC') .take(5000) .getMany(); if (assets.length > 0) { const data = _.groupBy(assets, (a) => new Date(a.createdAt).toISOString().slice(0, 10)); const formattedData = []; Object.keys(data).forEach((v) => formattedData.push({ date: v, assets: data[v] })); const response = new GetAllAssetReponseDto(); response.count = assets.length; response.data = formattedData; response.nextPageKey = assets[assets.length - 1].createdAt; return response; } else { const response = new GetAllAssetReponseDto(); response.count = 0; response.data = []; response.nextPageKey = 'null'; return response; } } catch (e) { Logger.error(e, 'getAllAssets'); } } public async findOne(authUser: AuthUserDto, deviceId: string, assetId: string): Promise { const rows = await this.assetRepository.query( 'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."userId" = $2 AND a."deviceId" = $3', [assetId, authUser.id, deviceId], ); if (rows.lengh == 0) { throw new BadRequestException('Not Found'); } return rows[0] as AssetEntity; } public async getNewAssets(authUser: AuthUserDto, latestDate: string) { return await this.assetRepository.find({ where: { userId: authUser.id, createdAt: MoreThan(latestDate), }, order: { createdAt: 'ASC', // ASC order to add existed asset the latest group first before creating a new date group. }, }); } public async getAssetById(authUser: AuthUserDto, assetId: string) { return await this.assetRepository.findOne({ where: { userId: authUser.id, id: assetId, }, relations: ['exifInfo'], }); } public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) { let file = null; const asset = await this.findOne(authUser, query.did, query.aid); // Handle Sending Images if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { res.set({ 'Content-Type': asset.mimeType, }); if (query.isThumb === 'false' || !query.isThumb) { file = createReadStream(asset.originalPath); } else { file = createReadStream(asset.resizePath); } file.on('error', (error) => { Logger.log(`Cannot create read stream ${error}`); return new BadRequestException('Cannot Create Read Stream'); }); return new StreamableFile(file); } else if (asset.type == AssetType.VIDEO) { // Handle Handling Video const { size } = await fileInfo(asset.originalPath); const range = headers.range; if (range) { /** Extracting Start and End value from Range Header */ let [start, end] = range.replace(/bytes=/, '').split('-'); start = parseInt(start, 10); end = end ? parseInt(end, 10) : size - 1; if (!isNaN(start) && isNaN(end)) { start = start; end = size - 1; } if (isNaN(start) && !isNaN(end)) { start = size - end; end = size - 1; } // Handle unavailable range request if (start >= size || end >= size) { console.error('Bad Request'); // Return the 416 Range Not Satisfiable. res.status(416).set({ 'Content-Range': `bytes */${size}`, }); throw new BadRequestException('Bad Request Range'); } /** Sending Partial Content With HTTP Code 206 */ res.status(206).set({ 'Content-Range': `bytes ${start}-${end}/${size}`, 'Accept-Ranges': 'bytes', 'Content-Length': end - start + 1, 'Content-Type': asset.mimeType, }); const videoStream = createReadStream(asset.originalPath, { start: start, end: end }); return new StreamableFile(videoStream); } else { res.set({ 'Content-Type': asset.mimeType, }); return new StreamableFile(createReadStream(asset.originalPath)); } } } public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) { let result = []; const target = assetIds.ids; for (let assetId of target) { const res = await this.assetRepository.delete({ id: assetId, userId: authUser.id, }); if (res.affected) { result.push({ id: assetId, status: 'success', }); } else { result.push({ id: assetId, status: 'failed', }); } } return result; } async getAssetSearchTerm(authUser: AuthUserDto): Promise { const possibleSearchTerm = new Set(); const rows = await this.assetRepository.query( ` select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type from assets a left join exif e on a.id = e."assetId" left join smart_info si on a.id = si."assetId" where a."userId" = $1; `, [authUser.id], ); rows.forEach((row) => { // tags row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase())); // asset's tyoe possibleSearchTerm.add(row['type']?.toLowerCase()); // image orientation possibleSearchTerm.add(row['orientation']?.toLowerCase()); // Lens model possibleSearchTerm.add(row['lensModel']?.toLowerCase()); // Make and model possibleSearchTerm.add(row['make']?.toLowerCase()); possibleSearchTerm.add(row['model']?.toLowerCase()); }); return Array.from(possibleSearchTerm).filter((x) => x != null); } }