feat(web) Individual assets shared mechanism (#1317)

* Create shared link modal for individual asset

* Added API to create asset shared link

* Added viewer for individual shared link

* Added multiselection app bar

* Refactor gallery viewer to its own component

* Refactor

* Refactor

* Add and remove asset from shared link

* Fixed test

* Fixed notification card doesn't wrap

* Add check asset access when created asset shared link

* pr feedback
This commit is contained in:
Alex
2023-01-14 23:49:47 -06:00
committed by GitHub
parent b9b2b559a1
commit e9fda40b2b
66 changed files with 2085 additions and 242 deletions

View File

@@ -14,6 +14,7 @@ import {
Header,
Put,
UploadedFiles,
Patch,
} from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service';
@@ -50,6 +51,9 @@ import {
IMMICH_CONTENT_LENGTH_HINT,
} from '../../constants/download.constant';
import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
@ApiBearerAuth()
@ApiTags('Asset')
@@ -321,4 +325,22 @@ export class AssetController {
): Promise<CheckExistingAssetsResponseDto> {
return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
}
@Authenticated()
@Post('/shared-link')
async createAssetsSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: CreateAssetsShareLinkDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.createAssetsSharedLink(authUser, dto);
}
@Authenticated({ isShared: true })
@Patch('/shared-link')
async updateAssetsInSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.updateAssetsInSharedLink(authUser, dto);
}
}

View File

@@ -13,7 +13,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { createHash, randomUUID } from 'node:crypto';
import { QueryFailedError, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType } from '@app/infra';
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
import { constants, createReadStream, ReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express';
@@ -59,6 +59,9 @@ import { StorageService } from '@app/storage';
import { ShareCore } from '../share/share.core';
import { ISharedLinkRepository } from '../share/shared-link.repository';
import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
const fileInfo = promisify(stat);
@@ -699,6 +702,42 @@ export class AssetService {
throw new ForbiddenException();
}
}
async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> {
const assets = [];
await this.checkAssetsAccess(authUser, dto.assetIds);
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const sharedLink = await this.shareCore.createSharedLink(authUser.id, {
sharedType: SharedLinkType.INDIVIDUAL,
expiredAt: dto.expiredAt,
allowUpload: dto.allowUpload,
assets: assets,
description: dto.description,
});
return mapSharedLinkToResponseDto(sharedLink);
}
async updateAssetsInSharedLink(
authUser: AuthUserDto,
dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) throw new ForbiddenException();
const assets = [];
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets);
return mapSharedLinkToResponseDto(updatedLink);
}
}
async function processETag(path: string, res: Res, headers: Record<string, string>): Promise<boolean> {

View File

@@ -0,0 +1,6 @@
import { IsNotEmpty } from 'class-validator';
export class UpdateAssetsToSharedLinkDto {
@IsNotEmpty()
assetIds!: string[];
}

View File

@@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateAssetsShareLinkDto {
@IsArray()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ApiProperty({
isArray: true,
type: String,
title: 'Array asset IDs to be shared',
example: [
'bf973405-3f2a-48d2-a687-2ed4167164be',
'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
],
})
assetIds!: string[];
@IsString()
@IsOptional()
expiredAt?: string;
@IsBoolean()
@IsOptional()
allowUpload?: boolean;
@IsString()
@IsOptional()
description?: string;
}