Feature - Add upload functionality on Web (#231)

* Added file selector

* Extract metadata to upload files to the web

* Added request for uploading

* Generate jpeg/Webp thumbnail for asset uploaded without thumbnail data

* Added generating thumbnail for video and WebSocket broadcast after thumbnail is generated

* Added video length extraction

* Added Uploading Panel

* Added upload progress store and styling the uploaded asset

* Added condition to only show upload panel when there is upload in progress

* Remove asset from the upload list after successfully uploading

* Added WebSocket to listen to upload event on the web

* Added mechanism to check for existing assets before uploading on the web

* Added test workflow

* Update readme
This commit is contained in:
Alex
2022-06-19 08:16:35 -05:00
committed by GitHub
parent b7603fd150
commit 1e3464fe47
33 changed files with 859 additions and 220 deletions

View File

@@ -15,6 +15,7 @@ import {
Delete,
Logger,
Patch,
HttpCode,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service';
@@ -76,6 +77,10 @@ export class AssetController {
{ asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true },
{ jobId: savedAsset.id },
);
this.wsCommunicateionGateway.server
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(assetWithThumbnail));
} else {
await this.assetUploadedQueue.add(
'asset-uploaded',
@@ -83,8 +88,6 @@ export class AssetController {
{ jobId: savedAsset.id },
);
}
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
} catch (e) {
Logger.error(`Error receiving upload file ${e}`);
}
@@ -171,4 +174,20 @@ export class AssetController {
return result;
}
/**
* Check duplicated asset before uploading - for Web upload used
*/
@Post('/check')
@HttpCode(200)
async checkDuplicateAsset(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) { deviceAssetId }: { deviceAssetId: string },
) {
const res = await this.assetService.checkDuplicatedAsset(authUser, deviceAssetId);
return {
isExist: res,
};
}
}

View File

@@ -24,6 +24,6 @@ import { CommunicationModule } from '../communication/communication.module';
],
controllers: [AssetController],
providers: [AssetService, BackgroundTaskService],
exports: [],
exports: [AssetService],
})
export class AssetModule {}

View File

@@ -1,6 +1,6 @@
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
@@ -72,6 +72,7 @@ export class AssetService {
return await this.assetRepository.find({
where: {
userId: authUser.id,
resizePath: Not(IsNull()),
},
relations: ['exifInfo'],
order: {
@@ -381,4 +382,15 @@ export class AssetService {
[authUser.id],
);
}
async checkDuplicatedAsset(authUser: AuthUserDto, deviceAssetId: string) {
const res = await this.assetRepository.findOne({
where: {
deviceAssetId,
userId: authUser.id,
},
});
return res ? true : false;
}
}

View File

@@ -6,8 +6,9 @@ import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '@app/database/entities/user.entity';
import { Repository } from 'typeorm';
import { query } from 'express';
@WebSocketGateway()
@WebSocketGateway({ cors: true })
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(
private immichJwtService: ImmichJwtService,
@@ -21,27 +22,33 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
handleDisconnect(client: Socket) {
client.leave(client.nsp.name);
Logger.log(`Client ${client.id} disconnected`);
Logger.log(`Client ${client.id} disconnected from Websocket`, 'WebsocketConnectionEvent');
}
async handleConnection(client: Socket, ...args: any[]) {
Logger.log(`New websocket connection: ${client.id}`, 'NewWebSocketConnection');
const accessToken = client.handshake.headers.authorization.split(' ')[1];
const res = await this.immichJwtService.validateToken(accessToken);
try {
Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent');
if (!res.status) {
client.emit('error', 'unauthorized');
client.disconnect();
return;
const accessToken = client.handshake.headers.authorization.split(' ')[1];
const res = await this.immichJwtService.validateToken(accessToken);
if (!res.status) {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
const user = await this.userRepository.findOne({ where: { id: res.userId } });
if (!user) {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
client.join(user.id);
} catch (e) {
// Logger.error(`Error establish websocket conneciton ${e}`, 'HandleWebscoketConnection');
}
const user = await this.userRepository.findOne({ where: { id: res.userId } });
if (!user) {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
client.join(user.id);
}
}