import { Observable, Lock } from "@hibas123/utils"; //this references global on node and window in browser const fetch = typeof window !== "undefined" && typeof window.fetch !== undefined ? window.fetch : require("cross-fetch").default; export interface IFileVersion { version: string; time: Date; preview: string; deleted: boolean; } export interface IFile { _id: string; type: string; name: string; folder: string; deleted: boolean; active: IFileVersion; versions: IFileVersion[]; user: string; application: string; } export interface IHistory { file: IFile; history: IFileVersion[]; } export class NoConnection extends Error { type: string; constructor() { super("No connection"); this.type = "noconnection" } } export class Unauthorized extends Error { type: string; constructor() { super("Not authorized"); this.type = "unauthorized" } } export class NoPermission extends Error { type: string; constructor() { super("No permission"); this.type = "nopermission" } } export class NotFound extends Error { type: string; constructor() { super("Not found"); this.type = "notfound" } } export class BadRequest extends Error { type: string; constructor() { super("Bad request"); this.type = "badrequest" } } function statusParser(res: Response) { if (res.status !== 200) { switch (res.status) { case 400: throw new BadRequest(); case 404: throw new NotFound(); case 403: throw new NoPermission(); case 401: throw new Unauthorized(); default: throw new Error(res.statusText); } } } export type JWTCallback = (err: Error | null | string, jwt: string) => void; export default class SecureFileWrapper { private _jwtObservableServer: Observable = new Observable(); jwtObservable = this._jwtObservableServer.getPublicApi(); private jwt: string; private auth_lock = new Lock(); constructor(private server: string) { if (this.server.endsWith("/")) { this.server += "api/v1"; } else { this.server += "/api/v1"; } } public async getJWT() { if (!this.auth_lock.locked) { let lock = await this.auth_lock.getLock(); await new Promise((yes, no) => { this._jwtObservableServer.send((err: Error | null | string, jwt: string) => { if (err) { this.jwt = undefined; no(err); } else { this.jwt = jwt; yes(); } }); }).finally(() => lock.release()) } await this.auth_lock.getLock().then(lock => lock.release()) } public async makeRequest(endpoint: string, method: "POST" | "GET" | "PUT" | "DELETE", query: any, body?: ArrayBuffer | ArrayBufferView, second = false) { if (!this.jwt || this.jwt === undefined) { await this.getJWT(); } query.jwt = this.jwt; let query_str = "?"; let first = true; for (let key in query) { if (!first) query_str += "&"; query_str += encodeURIComponent(key) + "=" + encodeURIComponent(query[key]); first = false; } var headers = { "pragme": "no-cache", "cache-control": "no-cache" }; if (body) { headers["Content-Type"] = "application/octet-stream" } try { let res = await fetch(this.server + endpoint + query_str, { method, body, headers }); if (res.status === 401 && !second) { await this.getJWT(); return this.makeRequest(endpoint, method, query, body, true); } else { statusParser(res); return res; } } catch (err) { if (err instanceof TypeError || err.errno === "ECONNREFUSED") throw new NoConnection(); throw err; } } private fixIFileVersion(version: IFileVersion): IFileVersion { version.time = new Date(version.time) return version; } private fixIFile(file: IFile): IFile { file.active.time = new Date(file.active.time) if (file.versions) { file.versions = file.versions.map(e => this.fixIFileVersion(e)) } return file; } // async test(jwt): Promise<{ user: string, test: true }> { // let res = await this.makeRequest("/test", "GET", {}, undefined, this.jwt_enabled); // statusParser(res); // return await res.json(); // } async list(folder?: string): Promise { let query: any = {} if (folder) query.folder = folder; let res = await this.makeRequest("/files", "GET", query); let d: { files: IFile[] } = await res.json(); return d.files.map(e => this.fixIFile(e)); } async create(name: string, data: ArrayBuffer | ArrayBufferView, type: "text" | "binary", folder?: string, preview?: string, id?: string, date?: Date): Promise { let params: any = { type: type, name: name }; if (preview) params.preview = preview; if (folder) params.folder = folder; if (id) params.id = id if (date) params.date = date.toJSON() let res = await this.makeRequest("/files", "POST", params, data); return this.fixIFile((await res.json()).file); } async get(id: string, version?: string): Promise { let res: Response; if (typeof version === "string") { res = await this.makeRequest(`/files/${id}/history/${version}`, "GET", {}); } else { res = await this.makeRequest("/files/" + id, "GET", {}); } return res.arrayBuffer() } async update(id: string, data: ArrayBuffer | ArrayBufferView, preview?: string, date?: Date, old = false): Promise { let params: any = { old }; if (preview) params.preview = preview; if (date) params.date = date.toJSON() let res = await this.makeRequest("/files/" + id, "PUT", params, data); let json = await res.json() return this.fixIFile(json.file); } async delete(id: string): Promise { let res = await this.makeRequest("/files/" + id, "DELETE", {}); } async history(id: string): Promise { let res = await this.makeRequest(`/files/${id}/history`, "GET", {}); let data: IHistory = await res.json(); data.file = this.fixIFile(data.file) data.history = data.history.map(v => this.fixIFileVersion(v)); return data; } async restore(id: string, version: string) { await this.makeRequest(`/files/${id}/history/${version}/restore`, "PUT", {}); } async clean(id: string, val: number | Date): Promise { let query = typeof val === "number" ? { count: val } : { date: val.toISOString() }; return this.makeRequest(`/files/${id}/history/clean`, "PUT", query); } }