Download mutliple files as zip

Adds the possitibility to download folders
and multiple files as zip.
This commit is contained in:
Sean 2020-08-12 13:55:20 +02:00
parent ccc3e38427
commit e75573e139
6 changed files with 60 additions and 27 deletions

View File

@ -59,6 +59,7 @@
"css-element-queries": "^1.2.3", "css-element-queries": "^1.2.3",
"exceljs": "1.15.0", "exceljs": "1.15.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"jszip": "^3.5.0",
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git", "material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
"moment": "^2.24.0", "moment": "^2.24.0",

View File

@ -273,4 +273,26 @@ export class HttpService {
public async delete<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> { public async delete<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
return await this.send<T>(path, HTTPMethod.DELETE, data, queryParams, header); return await this.send<T>(path, HTTPMethod.DELETE, data, queryParams, header);
} }
/**
* Retrieves a binary file from the url and returns a base64 value
*
* @param url file url
* @returns a promise with a base64 string
*/
public async downloadAsBase64(url: string): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
const headers = new HttpHeaders();
const file = await this.get<Blob>(url, {}, {}, headers, 'blob');
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const resultStr: string = reader.result as string;
resolve(resultStr.split(',')[1]);
};
reader.onerror = error => {
reject(error);
};
});
}
} }

View File

@ -1,4 +1,3 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
@ -100,7 +99,7 @@ export class PdfDocumentService {
); );
const promises = fontPathList.map(fontPath => { const promises = fontPathList.map(fontPath => {
return this.convertUrlToBase64(fontPath).then(base64 => { return this.httpService.downloadAsBase64(fontPath).then(base64 => {
return { return {
[fontPath.split('/').pop()]: base64 [fontPath.split('/').pop()]: base64
}; };
@ -117,29 +116,6 @@ export class PdfDocumentService {
return vfs; return vfs;
} }
/**
* Retrieves a binary file from the url and returns a base64 value
*
* @param url file url
* @returns a promise with a base64 string
*/
private async convertUrlToBase64(url: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const headers = new HttpHeaders();
this.httpService.get<Blob>(url, {}, {}, headers, 'blob').then(file => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const resultStr: string = reader.result as string;
resolve(resultStr.split(',')[1]);
};
reader.onerror = error => {
reject(error);
};
});
});
}
/** /**
* Returns the name of a font from the value of the given * Returns the name of a font from the value of the given
* config variable. * config variable.
@ -665,7 +641,7 @@ export class PdfDocumentService {
} }
if (!vfs[url]) { if (!vfs[url]) {
const base64 = await this.convertUrlToBase64(url); const base64 = await this.httpService.downloadAsBase64(url);
vfs[url] = base64; vfs[url] = base64;
} }
} }

View File

@ -2,6 +2,8 @@ import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
import * as JSZip from 'jszip';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators'; import { first, map } from 'rxjs/operators';
@ -136,6 +138,18 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
return this.httpService.post<Identifiable>('/rest/mediafiles/mediafile/', file, {}, emptyHeader); return this.httpService.post<Identifiable>('/rest/mediafiles/mediafile/', file, {}, emptyHeader);
} }
public async downloadArchive(archiveName: string, files: ViewMediafile[]): Promise<void> {
const zip = new JSZip();
for (const file of files) {
if (!file.is_directory) {
const base64Data = await this.httpService.downloadAsBase64(file.url);
zip.file(file.filename, base64Data, { base64: true });
}
}
const archive = await zip.generateAsync({ type: 'blob' });
saveAs(archive, archiveName);
}
public getDirectoryBehaviorSubject(): BehaviorSubject<ViewMediafile[]> { public getDirectoryBehaviorSubject(): BehaviorSubject<ViewMediafile[]> {
return this.directoryBehaviorSubject; return this.directoryBehaviorSubject;
} }

View File

@ -229,12 +229,20 @@
<!-- Menu for Mediafiles --> <!-- Menu for Mediafiles -->
<mat-menu #mediafilesMenu="matMenu"> <mat-menu #mediafilesMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">
<button mat-menu-item (click)="downloadMultiple()">
<mat-icon>cloud_download</mat-icon>
<span>{{ 'Download folder' | translate }}</span>
</button>
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()"> <button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
<span>{{ 'Multiselect' | translate }}</span> <span>{{ 'Multiselect' | translate }}</span>
</button> </button>
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<button mat-menu-item [disabled]="!selectedRows.length" (click)="downloadMultiple(selectedRows)">
<mat-icon>cloud_download</mat-icon>
<span>{{ 'Download selected' | translate }}</span>
</button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="move(moveDialog, selectedRows)"> <button mat-menu-item [disabled]="!selectedRows.length" (click)="move(moveDialog, selectedRows)">
<mat-icon>near_me</mat-icon> <mat-icon>near_me</mat-icon>
<span>{{ 'Move' | translate }}</span> <span>{{ 'Move' | translate }}</span>

View File

@ -23,6 +23,7 @@ import { OperatorService, Permission } from 'app/core/core-services/operator.ser
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service'; import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { MediaManageService } from 'app/core/ui-services/media-manage.service'; import { MediaManageService } from 'app/core/ui-services/media-manage.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
@ -202,7 +203,8 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
private fb: FormBuilder, private fb: FormBuilder,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private groupRepo: GroupRepositoryService, private groupRepo: GroupRepositoryService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef,
private configService: ConfigService
) { ) {
super(titleService, translate, matSnackBar, storage); super(titleService, translate, matSnackBar, storage);
this.canMultiSelect = true; this.canMultiSelect = true;
@ -459,6 +461,16 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
}); });
} }
public downloadMultiple(mediafiles: ViewMediafile[] = this.dataSource.source): void {
/**
* TODO: naming the files is discussable
*/
const eventName = this.configService.instant<string>('general_event_name');
const dirName = this.directory?.filename ?? this.translate.instant('Files');
const archiveName = `${eventName} - ${dirName}`.trim();
this.repo.downloadArchive(archiveName, mediafiles);
}
public move(templateRef: TemplateRef<string>, mediafiles: ViewMediafile[]): void { public move(templateRef: TemplateRef<string>, mediafiles: ViewMediafile[]): void {
this.moveForm.reset(); this.moveForm.reset();