File: src/lib/DAV.ts

Recommend this page to a friend!
  Classes of Dom Hastings   JS Webdav Client   src/lib/DAV.ts   Download  
File: src/lib/DAV.ts
Role: Class source
Content type: text/plain
Description: Class source
Class: JS Webdav Client
Access files of a Webdav server
Author: By
Last change:
Date: 6 months ago
Size: 7,528 bytes
 

Contents

Class file image Download
import joinPath, { trailingSlash } from './joinPath'; import Collection from './Collection'; import DAVResponse from './Response'; import Entry from './Entry'; import HTTP from './HTTP'; import RequestFailure from './HTTP/RequestFailure'; import { error } from 'melba-toast'; import { t } from 'i18next'; type ConstructorOptions = { bypassCheck?: boolean; sortDirectoriesFirst?: boolean; }; type CachedRequests = { GET: string; PROPFIND: Collection; }; export type RequestCache< K extends keyof CachedRequests = keyof CachedRequests, T extends CachedRequests[K] = CachedRequests[K] > = Map<K, Map<string, T>>; const emptyCache = (): RequestCache => { const cache = new Map< keyof CachedRequests, Map<string, Collection | string> >(); cache.set('GET', new Map<string, string>()); cache.set('PROPFIND', new Map<string, Collection>()); return cache; }; export class DAV { #bypassCheck: boolean; #cache: RequestCache; #http: HTTP; #sortDirectoriesFirst: boolean; #dropCache = <K extends keyof CachedRequests = keyof CachedRequests>( type: K, uri: string ): void | null => { const lookup = this.#cache.get(type); if (lookup.has(uri)) { lookup.delete(uri); } }; #getCache = <K extends keyof CachedRequests = keyof CachedRequests>( type: K, uri: string ): CachedRequests[K] | null => { const lookup = this.#cache.get(type); if (!lookup.has(uri)) { return null; } return lookup.get(uri) as CachedRequests[K]; }; #hasCache = <K extends keyof CachedRequests = keyof CachedRequests>( type: K, uri: string ): boolean => { const lookup = this.#cache.get(type); return lookup.has(uri); }; #setCache = <K extends keyof CachedRequests = keyof CachedRequests>( type: K, uri: string, value: CachedRequests[K] ): void | null => { const lookup = this.#cache.get(type); lookup.set(uri, value); }; #toastOnFailure = async ( func: () => Promise<Response>, quiet: boolean = false ): Promise<Response> => { try { return await func(); } catch (e) { if (!(e instanceof RequestFailure)) { throw e; } if (!quiet) { error( t('failure', { interpolation: { escapeValue: false, }, method: e.method(), url: e.url(), statusText: e.statusText(), status: e.status(), }) ); } return e.response(); } }; #validDestination = (destination: string): string => { const hostname = `${location.protocol}//${location.hostname}${ location.port ? `:${location.port}` : '' }`, hostnameRegExp = new RegExp(`^${hostname}`); if (!destination.match(hostnameRegExp)) { if (destination.match(/^http/)) { throw new TypeError(`Invalid destination host: '${destination}'.`); } return `${hostname}${destination}`; } return destination; }; constructor( { bypassCheck, sortDirectoriesFirst }: ConstructorOptions, cache: RequestCache = emptyCache(), http = new HTTP() ) { this.#bypassCheck = bypassCheck; this.#sortDirectoriesFirst = sortDirectoriesFirst; this.#cache = cache; this.#http = http; } /** * @param uri The URI to perform a HEAD request for * @param quiet Whether or not to invoke the error toast */ async check( uri: string, quiet: boolean = false ): Promise<{ ok: boolean; status: number; }> { if (this.#bypassCheck) { return Promise.resolve({ ok: true, status: 200, }); } return this.#toastOnFailure( (): Promise<Response> => this.#http.HEAD(uri), quiet ); } /** * @param from The path to the file or directory to copy * @param to The path of the directory to copy to * @param entry The Entry for the file to copy * @param overwrite */ async copy( from: string, to: string, entry: Entry, overwrite: boolean = false ): Promise<Response> { const headers: HeadersInit = { Destination: this.#validDestination(to), Overwrite: overwrite ? 'T' : 'F', }; if (entry.directory) { headers['Depth'] = 'infinity'; } return this.#toastOnFailure( (): Promise<Response> => this.#http.COPY(from, { headers, }) ); } /** * @param fullPath The full path of the directory to create */ async createDirectory(fullPath: string): Promise<Response> { return this.#toastOnFailure( (): Promise<Response> => this.#http.MKCOL(fullPath) ); } /** * @param uri The URI of the item to delete */ async del(uri: string): Promise<Response> { return this.#toastOnFailure( (): Promise<Response> => this.#http.DELETE(uri, { headers: { Depth: 'infinity', }, }) ); } /** * @param uri The URI of the item to get */ async get(uri: string): Promise<string | null> { if (!this.#hasCache('GET', uri)) { const response = await this.#toastOnFailure( (): Promise<Response> => this.#http.GET(uri) ); if (!response.ok) { return; } this.#setCache('GET', uri, await response.text()); } return this.#getCache('GET', uri); } /** * @param path The directory path to invalidate cache for */ invalidateCache(path: string): void { if (this.#hasCache('PROPFIND', path)) { this.#dropCache('PROPFIND', path); } } /** * @param uri The directory to list * @param bypassCache Force skipping cache */ async list(uri: string, bypassCache: boolean = false): Promise<Collection> { uri = trailingSlash(uri); if (!bypassCache && this.#hasCache('PROPFIND', uri)) { return this.#getCache('PROPFIND', uri); } const check = await this.check(uri); if (!check || (!check.ok && check.status !== 405)) { return; } const data = await this.#toastOnFailure( (): Promise<Response> => this.#http.PROPFIND(uri) ), response = new DAVResponse(await data.text()), collection = response.collection({ sortDirectoriesFirst: this.#sortDirectoriesFirst, }); this.#setCache('PROPFIND', uri, collection); return collection; } /** * @param from The path to the file or directory to move * @param to The path of the directory to move to * @param entry The Entry object for the file or directory to move * @param overwrite */ async move( from: string, to: string, entry: Entry, overwrite: boolean = false ): Promise<Response> { const destination = this.#validDestination(to); return this.#toastOnFailure( (): Promise<Response> => this.#http.MOVE(from, { headers: { Destination: entry.directory ? trailingSlash(destination) : destination, Overwrite: overwrite ? 'T' : 'F', }, }) ); } /** * @param path The path to upload the file to * @param file The File object to upload */ async upload(path: string, file: File): Promise<Response> { const targetFile = joinPath(path, file.name); return this.#toastOnFailure( (): Promise<Response> => this.#http.PUT(targetFile, { headers: { 'Content-Type': file.type, }, body: file, }) ); } } export default DAV;