src/app/features/file/couchdb-file.service.ts
Stores the files in the CouchDB. See https://docs.couchdb.org/en/3.2.2-docs/intro/api.html?highlight=attachments#attachments Running upload and download processes are shown with progress bars.
Properties |
|
Methods |
|
constructor(sanitizer: DomSanitizer, databaseResolver: DatabaseResolverService, entityMapper: EntityMapperService, entities: EntityRegistry, syncState: SyncStateSubject, navigator: Navigator)
|
|||||||||||||||||||||
Parameters :
|
Protected getShowFileUrl |
getShowFileUrl(entity: Entity, property: string)
|
Inherited from
FileService
|
Defined in
FileService:156
|
Returns :
string
|
loadFile | ||||||||||||||||
loadFile(entity: Entity, property: string, throwErrors: boolean)
|
||||||||||||||||
Inherited from
FileService
|
||||||||||||||||
Defined in
FileService:160
|
||||||||||||||||
Parameters :
Returns :
Observable<SafeUrl>
|
removeAllFiles | ||||||
removeAllFiles(entity: Entity)
|
||||||
Inherited from
FileService
|
||||||
Defined in
FileService:141
|
||||||
Parameters :
Returns :
Observable<any>
|
removeFile |
removeFile(entity: Entity, property: string)
|
Inherited from
FileService
|
Defined in
FileService:114
|
Returns :
any
|
uploadFile |
uploadFile(file: File, entity: Entity, property: string)
|
Inherited from
FileService
|
Defined in
FileService:50
|
Returns :
Observable<any>
|
Protected reportProgress | |||||||||
reportProgress(message: string, obs: Observable
|
|||||||||
Inherited from
FileService
|
|||||||||
Defined in
FileService:149
|
|||||||||
Parameters :
Returns :
void
|
showFile | ||||||||||||
showFile(entity: Entity, property: string)
|
||||||||||||
Inherited from
FileService
|
||||||||||||
Defined in
FileService:104
|
||||||||||||
If a file is available, downloads this file and shows it in a new tab.
Parameters :
Returns :
void
|
Protected dialog |
Type : MatDialog
|
Default value : inject(MatDialog)
|
Inherited from
FileService
|
Defined in
FileService:31
|
Protected httpClient |
Type : HttpClient
|
Default value : inject(HttpClient, {
optional: true,
})
|
Inherited from
FileService
|
Defined in
FileService:32
|
Protected snackbar |
Type : MatSnackBar
|
Default value : inject(MatSnackBar)
|
Inherited from
FileService
|
Defined in
FileService:30
|
import { Inject, Injectable } from "@angular/core";
import { HttpStatusCode } from "@angular/common/http";
import {
catchError,
concatMap,
last,
map,
shareReplay,
tap,
} from "rxjs/operators";
import { from, Observable, of, throwError } from "rxjs";
import { Entity } from "../../core/entity/model/entity";
import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service";
import { FileService } from "./file.service";
import { EntityRegistry } from "../../core/entity/database-entity.decorator";
import { Logging } from "../../core/logging/logging.service";
import { ObservableQueue } from "./observable-queue/observable-queue";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { SyncStateSubject } from "../../core/session/session-type";
import { SyncState } from "../../core/session/session-states/sync-state.enum";
import { environment } from "../../../environments/environment";
import { NAVIGATOR_TOKEN } from "../../utils/di-tokens";
import { NotAvailableOfflineError } from "../../core/session/not-available-offline.error";
import { DatabaseResolverService } from "../../core/database/database-resolver.service";
import { SyncedPouchDatabase } from "app/core/database/pouchdb/synced-pouch-database";
/**
* Stores the files in the CouchDB.
* See {@link https://docs.couchdb.org/en/3.2.2-docs/intro/api.html?highlight=attachments#attachments}
* Running upload and download processes are shown with progress bars.
*/
@Injectable()
export class CouchdbFileService extends FileService {
private attachmentsUrl = `${environment.DB_PROXY_PREFIX}/${Entity.DATABASE}-attachments`;
// TODO it seems like failed requests are executed again when a new one is done
private requestQueue = new ObservableQueue();
private cache: { [key: string]: Observable<string> } = {};
constructor(
private sanitizer: DomSanitizer,
private databaseResolver: DatabaseResolverService,
entityMapper: EntityMapperService,
entities: EntityRegistry,
syncState: SyncStateSubject,
@Inject(NAVIGATOR_TOKEN) private navigator: Navigator,
) {
super(entityMapper, entities, syncState);
}
uploadFile(file: File, entity: Entity, property: string): Observable<any> {
if (!this.navigator.onLine) {
return throwError(() => new NotAvailableOfflineError("File Attachments"));
}
const obs = this.requestQueue.add(
this.runFileUpload(file, entity, property),
);
this.reportProgress($localize`Uploading "${file.name}"`, obs);
this.cache[`${entity.getId()}/${property}`] = obs.pipe(
last(),
map(() => URL.createObjectURL(file)),
shareReplay(),
);
return obs;
}
private runFileUpload(file: File, entity: Entity, property: string) {
const attachmentPath = `${this.attachmentsUrl}/${entity.getId()}`;
return this.ensureDocIsSynced().pipe(
concatMap(() => this.getAttachmentsDocument(attachmentPath)),
concatMap(({ _rev }) =>
this.httpClient.put(`${attachmentPath}/${property}?rev=${_rev}`, file, {
headers: { "ngsw-bypass": "" },
reportProgress: true,
observe: "events",
}),
),
// prevent http request to be executed multiple times (whenever .subscribe is called)
shareReplay(),
);
}
/**
* For permission checks to work correctly, the Entity must be available server-side already.
* Manually send this doc to the DB here because sync is only happening at slower intervals.
* @private
*/
private ensureDocIsSynced(): Observable<SyncState> {
const mainDb = this.databaseResolver.getDatabase();
let sync: () => Promise<any> = (mainDb as SyncedPouchDatabase).sync
? () => (mainDb as SyncedPouchDatabase).sync()
: () => Promise.resolve();
return from(sync()).pipe(map(() => this.syncState.value));
}
private getAttachmentsDocument(
attachmentPath: string,
): Observable<{ _rev: string }> {
return this.httpClient
.get<{ _id: string; _rev: string }>(attachmentPath)
.pipe(
catchError((err) => {
if (err.status === HttpStatusCode.NotFound) {
return this.httpClient
.put<{ rev: string }>(attachmentPath, {})
.pipe(map((res) => ({ _rev: res.rev })));
}
throw err;
}),
);
}
removeFile(entity: Entity, property: string) {
if (!this.navigator.onLine) {
return throwError(() => new NotAvailableOfflineError("File Attachments"));
}
return this.requestQueue.add(this.runFileRemoval(entity, property));
}
private runFileRemoval(entity: Entity, property: string) {
const path = `${entity.getId()}/${property}`;
return this.httpClient
.get<{ _rev: string }>(`${this.attachmentsUrl}/${entity.getId()}`)
.pipe(
concatMap(({ _rev }) =>
this.httpClient.delete(`${this.attachmentsUrl}/${path}?rev=${_rev}`),
),
tap(() => delete this.cache[path]),
catchError((err) => {
if (err.status === HttpStatusCode.NotFound) {
return of({ ok: true });
} else {
throw err;
}
}),
);
}
removeAllFiles(entity: Entity): Observable<any> {
return this.requestQueue.add(this.runAllFilesRemoval(entity));
}
private runAllFilesRemoval(entity: Entity) {
const attachmentPath = `${this.attachmentsUrl}/${entity.getId()}`;
return this.httpClient
.get<{ _rev: string }>(attachmentPath)
.pipe(
concatMap(({ _rev }) =>
this.httpClient.delete(`${attachmentPath}?rev=${_rev}`),
),
);
}
protected override getShowFileUrl(entity: Entity, property: string): string {
return `${this.attachmentsUrl}/${entity.getId()}/${property}`;
}
loadFile(
entity: Entity,
property: string,
throwErrors: boolean = false,
): Observable<SafeUrl> {
const path = `${entity.getId()}/${property}`;
if (!this.cache[path]) {
this.cache[path] = this.httpClient
.get(`${this.attachmentsUrl}/${path}`, {
responseType: "blob",
})
.pipe(
map((blob) => URL.createObjectURL(blob)),
catchError((err) => {
Logging.warn("Could not load file", entity?.getId(), property, err);
if (throwErrors) {
throw err;
} else {
return of("");
}
}),
shareReplay(),
);
}
return this.cache[path].pipe(
map((url) => this.sanitizer.bypassSecurityTrustUrl(url)),
);
}
}