| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- import { Injectable, NotFoundException, BadRequestException, UnauthorizedException } from '@nestjs/common'
- import { CapturesRepository } from './captures.repository'
- import { DevicesRepository } from '../devices/devices.repository'
- import { RealtimeGateway } from '../../realtime/gateways/realtime.gateway'
- import * as fs from 'fs'
- import * as path from 'path'
- import * as crypto from 'crypto'
- export interface UploadedFile {
- filename: string
- originalname: string
- mimetype: string
- size: number
- path: string
- }
- @Injectable()
- export class CapturesService {
- private readonly uploadDir: string
- private readonly uploadBaseUrl: string
- constructor(
- private readonly repo: CapturesRepository,
- private readonly deviceRepo: DevicesRepository,
- private readonly realtime: RealtimeGateway,
- ) {
- this.uploadDir = process.env['UPLOAD_DIR'] || '/uploads/captures'
- this.uploadBaseUrl = process.env['UPLOAD_BASE_URL'] || 'http://localhost:3001'
- fs.mkdirSync(this.uploadDir, { recursive: true })
- }
- private buildImageUrl(fileKey: string | null): string | null {
- if (!fileKey) return null
- return `${this.uploadBaseUrl}/uploads/captures/${fileKey}`
- }
- private async validateDeviceApiKey(deviceId: string, apiKey: string) {
- const device = await this.deviceRepo.findDeviceById(deviceId)
- if (!device) throw new NotFoundException(`Device ${deviceId} not found`)
- if (!device.apiKeyHash) throw new NotFoundException('Device not provisioned')
- const validKey = await this.deviceRepo.verifyApiKey(device.apiKeyHash, apiKey)
- if (!validKey) throw new UnauthorizedException('Invalid API key')
- if (!device.projectId || !device.orgId) throw new BadRequestException('Device not assigned to a project')
- return device as Required<Pick<typeof device, 'id' | 'projectId' | 'orgId' | 'serialNo' | 'name'>>
- }
- async handleUpload(params: {
- file: UploadedFile
- apiKey: string
- deviceId: string
- capturedAt: string
- resolution?: string
- exposureMs?: number
- iso?: number
- aperture?: string
- gpsLat?: string
- gpsLng?: string
- }) {
- const device = await this.validateDeviceApiKey(params.deviceId, params.apiKey)
- // Compute checksum
- const fileBuffer = fs.readFileSync(params.file.path)
- const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex')
- // Storage path: /uploads/captures/{projectId}/{YYYY-MM-DD}/{id}.{ext}
- const dateStr = new Date(params.capturedAt).toISOString().slice(0, 10)
- const ext = path.extname(params.file.originalname) || '.jpg'
- const id = crypto.randomUUID().slice(0, 12)
- const storagePath = path.join(this.uploadDir, device.projectId!, dateStr)
- fs.mkdirSync(storagePath, { recursive: true })
- const fileKey = `${device.projectId!}/${dateStr}/${id}${ext}`
- const destPath = path.join(this.uploadDir, fileKey)
- fs.renameSync(params.file.path, destPath)
- const capture = await this.repo.createCapture({
- projectId: device.projectId!,
- deviceId: params.deviceId,
- capturedAt: new Date(params.capturedAt),
- fileKey,
- checksum,
- resolution: params.resolution ?? undefined,
- fileSizeBytes: params.file.size,
- exposureMs: params.exposureMs ?? undefined,
- iso: params.iso ?? undefined,
- aperture: params.aperture ?? undefined,
- gpsLat: params.gpsLat ?? undefined,
- gpsLng: params.gpsLng ?? undefined,
- metadata: { originalFilename: params.file.originalname, uploadedAt: new Date().toISOString() },
- })
- this.realtime.emitCaptureUploaded(params.deviceId, device.projectId!, capture.id)
- return {
- captureId: capture.id,
- fileKey,
- checksum,
- sizeBytes: params.file.size,
- capturedAt: params.capturedAt,
- }
- }
- async registerCapture(params: {
- apiKey: string
- deviceId: string
- capturedAt: string
- fileKey: string
- checksum?: string
- resolution?: string
- fileSizeBytes?: number
- exposureMs?: number
- iso?: number
- aperture?: string
- gpsLat?: string
- gpsLng?: string
- }) {
- const device = await this.validateDeviceApiKey(params.deviceId, params.apiKey)
- const capture = await this.repo.createCapture({
- projectId: device.projectId!,
- deviceId: params.deviceId,
- capturedAt: new Date(params.capturedAt),
- fileKey: params.fileKey,
- checksum: params.checksum ?? undefined,
- resolution: params.resolution ?? undefined,
- fileSizeBytes: params.fileSizeBytes ?? undefined,
- exposureMs: params.exposureMs ?? undefined,
- iso: params.iso ?? undefined,
- aperture: params.aperture ?? undefined,
- gpsLat: params.gpsLat ?? undefined,
- gpsLng: params.gpsLng ?? undefined,
- })
- this.realtime.emitCaptureUploaded(params.deviceId, device.projectId!, capture.id)
- return { captureId: capture.id, fileKey: params.fileKey }
- }
- async getCapturesByProject(projectId: string, limit = 100) {
- const captures = await this.repo.findByProjectId(projectId, limit)
- return captures.map(c => ({ ...c, imageUrl: this.buildImageUrl(c.fileKey) }))
- }
- async getCapturesByDevice(deviceId: string, limit = 100) {
- const captures = await this.repo.findByDeviceId(deviceId, limit)
- return captures.map(c => ({ ...c, imageUrl: this.buildImageUrl(c.fileKey) }))
- }
- async getCaptureById(id: string) {
- const capture = await this.repo.findById(id)
- if (!capture) throw new NotFoundException(`Capture ${id} not found`)
- return { ...capture, imageUrl: this.buildImageUrl(capture.fileKey) }
- }
- async getCaptureStats(projectId: string) {
- const today = await this.repo.countByProjectToday(projectId)
- return { totalCaptures: today, todayCaptures: today }
- }
- }
|