feat(server/web): download entire album as zip archive (#897)

* feat(server/web): download entire album as zip archive

* fix: remove duplicate API call

* disable ZIP compression (images are already compressed)
This commit is contained in:
Fynn Petersen-Frey
2022-10-30 18:38:04 +01:00
committed by GitHub
parent b7f1a1ad4b
commit dc2c92e721
12 changed files with 695 additions and 14 deletions

View File

@@ -10,6 +10,7 @@ import {
ParseUUIDPipe,
Put,
Query,
Response,
} from '@nestjs/common';
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
import { AlbumService } from './album.service';
@@ -25,6 +26,7 @@ import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from './response-dto/album-response.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto";
import { Response as Res } from 'express';
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@Authenticated()
@@ -112,4 +114,13 @@ export class AlbumController {
) {
return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId);
}
@Get('/:albumId/download')
async downloadArchive(
@GetAuthUser() authUser: AuthUserDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
@Response({ passthrough: true }) res: Res,
): Promise<any> {
return this.albumService.downloadArchive(authUser, albumId, res);
}
}

View File

@@ -1,4 +1,12 @@
import { BadRequestException, Inject, Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import {
BadRequestException,
Inject,
Injectable,
NotFoundException,
ForbiddenException,
Logger,
InternalServerErrorException,
} from '@nestjs/common';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAlbumDto } from './dto/create-album.dto';
import { AlbumEntity } from '@app/database/entities/album.entity';
@@ -10,8 +18,10 @@ import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository';
import { AddAssetsResponseDto } from "./response-dto/add-assets-response.dto";
import {AddAssetsDto} from "./dto/add-assets.dto";
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
import { AddAssetsDto } from './dto/add-assets.dto';
import { Response as Res } from 'express';
import archiver from 'archiver';
@Injectable()
export class AlbumService {
@@ -116,7 +126,7 @@ export class AlbumService {
return {
...result,
album: mapAlbum(newAlbum)
album: mapAlbum(newAlbum),
};
}
@@ -139,6 +149,23 @@ export class AlbumService {
return this._albumRepository.getCountByUserId(authUser.id);
}
async downloadArchive(authUser: AuthUserDto, albumId: string, res: Res) {
try {
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
const archive = archiver('zip', { store: true });
res.attachment(`${album.albumName}.zip`);
archive.pipe(res);
album.assets?.forEach((a) => {
const name = `${a.assetInfo.exifInfo?.imageName || a.assetInfo.id}.${a.assetInfo.originalPath.split('.')[1]}`;
archive.file(a.assetInfo.originalPath, { name });
});
return archive.finalize();
} catch (e) {
Logger.error(`Error downloading album ${e}`, 'downloadArchive');
throw new InternalServerErrorException(`Failed to download album ${e}`, 'DownloadArchive');
}
}
async _checkValidThumbnail(album: AlbumEntity): Promise<AlbumEntity> {
const assetId = album.albumThumbnailAssetId;
if (assetId) {