fileTools.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import { WebRequest } from './webRequest';
  2. import { DomManagement } from './domManagement';
  3. import { Nullable } from '../types';
  4. import { IOfflineProvider } from '../Offline/IOfflineProvider';
  5. import { IFileRequest } from './fileRequest';
  6. import { Observable } from './observable';
  7. import { FilesInputStore } from './filesInputStore';
  8. import { RetryStrategy } from './retryStrategy';
  9. import { BaseError } from './baseError';
  10. import { StringTools } from './stringTools';
  11. /** @ignore */
  12. export class LoadFileError extends BaseError {
  13. public request?: WebRequest;
  14. public file?: File;
  15. /**
  16. * Creates a new LoadFileError
  17. * @param message defines the message of the error
  18. * @param request defines the optional web request
  19. * @param file defines the optional file
  20. */
  21. constructor(message: string, object?: WebRequest | File) {
  22. super(message);
  23. this.name = "LoadFileError";
  24. BaseError._setPrototypeOf(this, LoadFileError.prototype);
  25. if (object instanceof WebRequest) {
  26. this.request = object;
  27. }
  28. else {
  29. this.file = object;
  30. }
  31. }
  32. }
  33. /** @ignore */
  34. export class RequestFileError extends BaseError {
  35. /**
  36. * Creates a new LoadFileError
  37. * @param message defines the message of the error
  38. * @param request defines the optional web request
  39. */
  40. constructor(message: string, public request: WebRequest) {
  41. super(message);
  42. this.name = "RequestFileError";
  43. BaseError._setPrototypeOf(this, RequestFileError.prototype);
  44. }
  45. }
  46. /** @ignore */
  47. export class ReadFileError extends BaseError {
  48. /**
  49. * Creates a new ReadFileError
  50. * @param message defines the message of the error
  51. * @param file defines the optional file
  52. */
  53. constructor(message: string, public file: File) {
  54. super(message);
  55. this.name = "ReadFileError";
  56. BaseError._setPrototypeOf(this, ReadFileError.prototype);
  57. }
  58. }
  59. /**
  60. * @hidden
  61. */
  62. export class FileTools {
  63. /**
  64. * Gets or sets the retry strategy to apply when an error happens while loading an asset
  65. */
  66. public static DefaultRetryStrategy = RetryStrategy.ExponentialBackoff();
  67. /**
  68. * Gets or sets the base URL to use to load assets
  69. */
  70. public static BaseUrl = "";
  71. /**
  72. * Default behaviour for cors in the application.
  73. * It can be a string if the expected behavior is identical in the entire app.
  74. * Or a callback to be able to set it per url or on a group of them (in case of Video source for instance)
  75. */
  76. public static CorsBehavior: string | ((url: string | string[]) => string) = "anonymous";
  77. /**
  78. * Gets or sets a function used to pre-process url before using them to load assets
  79. */
  80. public static PreprocessUrl = (url: string) => {
  81. return url;
  82. }
  83. /**
  84. * Removes unwanted characters from an url
  85. * @param url defines the url to clean
  86. * @returns the cleaned url
  87. */
  88. private static _CleanUrl(url: string): string {
  89. url = url.replace(/#/mg, "%23");
  90. return url;
  91. }
  92. /**
  93. * Sets the cors behavior on a dom element. This will add the required Tools.CorsBehavior to the element.
  94. * @param url define the url we are trying
  95. * @param element define the dom element where to configure the cors policy
  96. */
  97. public static SetCorsBehavior(url: string | string[], element: { crossOrigin: string | null }): void {
  98. if (url && url.indexOf("data:") === 0) {
  99. return;
  100. }
  101. if (this.CorsBehavior) {
  102. if (typeof (this.CorsBehavior) === 'string' || this.CorsBehavior instanceof String) {
  103. element.crossOrigin = <string>this.CorsBehavior;
  104. }
  105. else {
  106. var result = this.CorsBehavior(url);
  107. if (result) {
  108. element.crossOrigin = result;
  109. }
  110. }
  111. }
  112. }
  113. /**
  114. * Loads an image as an HTMLImageElement.
  115. * @param input url string, ArrayBuffer, or Blob to load
  116. * @param onLoad callback called when the image successfully loads
  117. * @param onError callback called when the image fails to load
  118. * @param offlineProvider offline provider for caching
  119. * @param mimeType optional mime type
  120. * @returns the HTMLImageElement of the loaded image
  121. */
  122. public static LoadImage(input: string | ArrayBuffer | ArrayBufferView | Blob, onLoad: (img: HTMLImageElement | ImageBitmap) => void, onError: (message?: string, exception?: any) => void, offlineProvider: Nullable<IOfflineProvider>, mimeType?: string): Nullable<HTMLImageElement> {
  123. let url: string;
  124. let usingObjectURL = false;
  125. if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
  126. if (typeof Blob !== 'undefined') {
  127. url = URL.createObjectURL(new Blob([input]));
  128. usingObjectURL = true;
  129. } else {
  130. url = `data:${mimeType || "image/jpg"};base64,` + StringTools.EncodeArrayBufferToBase64(input);
  131. }
  132. }
  133. else if (input instanceof Blob) {
  134. url = URL.createObjectURL(input);
  135. usingObjectURL = true;
  136. }
  137. else {
  138. url = this._CleanUrl(input);
  139. url = this.PreprocessUrl(input);
  140. }
  141. if (typeof Image === "undefined") {
  142. this.LoadFile(url, (data) => {
  143. createImageBitmap(new Blob([data])).then((imgBmp) => {
  144. onLoad(imgBmp);
  145. if (usingObjectURL) {
  146. URL.revokeObjectURL(url);
  147. }
  148. }).catch((reason) => {
  149. if (onError) {
  150. onError("Error while trying to load image: " + input, reason);
  151. }
  152. });
  153. }, undefined, offlineProvider || undefined, true, (request, exception) => {
  154. if (onError) {
  155. onError("Error while trying to load image: " + input, exception);
  156. }
  157. });
  158. return null;
  159. }
  160. var img = new Image();
  161. this.SetCorsBehavior(url, img);
  162. const loadHandler = () => {
  163. img.removeEventListener("load", loadHandler);
  164. img.removeEventListener("error", errorHandler);
  165. onLoad(img);
  166. // Must revoke the URL after calling onLoad to avoid security exceptions in
  167. // certain scenarios (e.g. when hosted in vscode).
  168. if (usingObjectURL && img.src) {
  169. URL.revokeObjectURL(img.src);
  170. }
  171. };
  172. const errorHandler = (err: any) => {
  173. img.removeEventListener("load", loadHandler);
  174. img.removeEventListener("error", errorHandler);
  175. if (onError) {
  176. onError("Error while trying to load image: " + input, err);
  177. }
  178. if (usingObjectURL && img.src) {
  179. URL.revokeObjectURL(img.src);
  180. }
  181. };
  182. img.addEventListener("load", loadHandler);
  183. img.addEventListener("error", errorHandler);
  184. var noOfflineSupport = () => {
  185. img.src = url;
  186. };
  187. var loadFromOfflineSupport = () => {
  188. if (offlineProvider) {
  189. offlineProvider.loadImage(url, img);
  190. }
  191. };
  192. if (url.substr(0, 5) !== "data:" && offlineProvider && offlineProvider.enableTexturesOffline) {
  193. offlineProvider.open(loadFromOfflineSupport, noOfflineSupport);
  194. }
  195. else {
  196. if (url.indexOf("file:") !== -1) {
  197. var textureName = decodeURIComponent(url.substring(5).toLowerCase());
  198. if (FilesInputStore.FilesToLoad[textureName]) {
  199. try {
  200. var blobURL;
  201. try {
  202. blobURL = URL.createObjectURL(FilesInputStore.FilesToLoad[textureName]);
  203. }
  204. catch (ex) {
  205. // Chrome doesn't support oneTimeOnly parameter
  206. blobURL = URL.createObjectURL(FilesInputStore.FilesToLoad[textureName]);
  207. }
  208. img.src = blobURL;
  209. usingObjectURL = true;
  210. }
  211. catch (e) {
  212. img.src = "";
  213. }
  214. return img;
  215. }
  216. }
  217. noOfflineSupport();
  218. }
  219. return img;
  220. }
  221. /**
  222. * Reads a file from a File object
  223. * @param file defines the file to load
  224. * @param onSuccess defines the callback to call when data is loaded
  225. * @param onProgress defines the callback to call during loading process
  226. * @param useArrayBuffer defines a boolean indicating that data must be returned as an ArrayBuffer
  227. * @param onError defines the callback to call when an error occurs
  228. * @returns a file request object
  229. */
  230. public static ReadFile(file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: ReadFileError) => void): IFileRequest {
  231. let reader = new FileReader();
  232. let request: IFileRequest = {
  233. onCompleteObservable: new Observable<IFileRequest>(),
  234. abort: () => reader.abort(),
  235. };
  236. reader.onloadend = (e) => request.onCompleteObservable.notifyObservers(request);
  237. if (onError) {
  238. reader.onerror = (e) => {
  239. onError(new ReadFileError(`Unable to read ${file.name}`, file));
  240. };
  241. }
  242. reader.onload = (e) => {
  243. //target doesn't have result from ts 1.3
  244. onSuccess((<any>e.target)['result']);
  245. };
  246. if (onProgress) {
  247. reader.onprogress = onProgress;
  248. }
  249. if (!useArrayBuffer) {
  250. // Asynchronous read
  251. reader.readAsText(file);
  252. }
  253. else {
  254. reader.readAsArrayBuffer(file);
  255. }
  256. return request;
  257. }
  258. /**
  259. * Loads a file from a url
  260. * @param url url to load
  261. * @param onSuccess callback called when the file successfully loads
  262. * @param onProgress callback called while file is loading (if the server supports this mode)
  263. * @param offlineProvider defines the offline provider for caching
  264. * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
  265. * @param onError callback called when the file fails to load
  266. * @returns a file request object
  267. */
  268. public static LoadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (ev: ProgressEvent) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (request?: WebRequest, exception?: LoadFileError) => void): IFileRequest {
  269. // If file and file input are set
  270. if (url.indexOf("file:") !== -1) {
  271. const fileName = decodeURIComponent(url.substring(5).toLowerCase());
  272. const file = FilesInputStore.FilesToLoad[fileName];
  273. if (file) {
  274. return this.ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError ? (error) => onError(undefined, new LoadFileError(error.message, error.file)) : undefined);
  275. }
  276. }
  277. return this.RequestFile(url, (data, request) => {
  278. onSuccess(data, request ? request.responseURL : undefined);
  279. }, onProgress, offlineProvider, useArrayBuffer, onError ? (error) => {
  280. onError(error.request, new LoadFileError(error.message, error.request));
  281. } : undefined);
  282. }
  283. /**
  284. * Loads a file
  285. * @param url url to load
  286. * @param onSuccess callback called when the file successfully loads
  287. * @param onProgress callback called while file is loading (if the server supports this mode)
  288. * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
  289. * @param onError callback called when the file fails to load
  290. * @param onOpened callback called when the web request is opened
  291. * @returns a file request object
  292. */
  293. public static RequestFile(url: string, onSuccess: (data: string | ArrayBuffer, request?: WebRequest) => void, onProgress?: (event: ProgressEvent) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (error: RequestFileError) => void, onOpened?: (request: WebRequest) => void): IFileRequest {
  294. url = this._CleanUrl(url);
  295. url = this.PreprocessUrl(url);
  296. const loadUrl = this.BaseUrl + url;
  297. let aborted = false;
  298. const fileRequest: IFileRequest = {
  299. onCompleteObservable: new Observable<IFileRequest>(),
  300. abort: () => aborted = true,
  301. };
  302. const requestFile = () => {
  303. let request = new WebRequest();
  304. let retryHandle: Nullable<number> = null;
  305. fileRequest.abort = () => {
  306. aborted = true;
  307. if (request.readyState !== (XMLHttpRequest.DONE || 4)) {
  308. request.abort();
  309. }
  310. if (retryHandle !== null) {
  311. clearTimeout(retryHandle);
  312. retryHandle = null;
  313. }
  314. };
  315. const retryLoop = (retryIndex: number) => {
  316. request.open('GET', loadUrl);
  317. if (onOpened) {
  318. onOpened(request);
  319. }
  320. if (useArrayBuffer) {
  321. request.responseType = "arraybuffer";
  322. }
  323. if (onProgress) {
  324. request.addEventListener("progress", onProgress);
  325. }
  326. const onLoadEnd = () => {
  327. request.removeEventListener("loadend", onLoadEnd);
  328. fileRequest.onCompleteObservable.notifyObservers(fileRequest);
  329. fileRequest.onCompleteObservable.clear();
  330. };
  331. request.addEventListener("loadend", onLoadEnd);
  332. const onReadyStateChange = () => {
  333. if (aborted) {
  334. return;
  335. }
  336. // In case of undefined state in some browsers.
  337. if (request.readyState === (XMLHttpRequest.DONE || 4)) {
  338. // Some browsers have issues where onreadystatechange can be called multiple times with the same value.
  339. request.removeEventListener("readystatechange", onReadyStateChange);
  340. if ((request.status >= 200 && request.status < 300) || (request.status === 0 && (!DomManagement.IsWindowObjectExist() || this.IsFileURL()))) {
  341. onSuccess(useArrayBuffer ? request.response : request.responseText, request);
  342. return;
  343. }
  344. let retryStrategy = this.DefaultRetryStrategy;
  345. if (retryStrategy) {
  346. let waitTime = retryStrategy(loadUrl, request, retryIndex);
  347. if (waitTime !== -1) {
  348. // Prevent the request from completing for retry.
  349. request.removeEventListener("loadend", onLoadEnd);
  350. request = new WebRequest();
  351. retryHandle = setTimeout(() => retryLoop(retryIndex + 1), waitTime);
  352. return;
  353. }
  354. }
  355. const error = new RequestFileError("Error status: " + request.status + " " + request.statusText + " - Unable to load " + loadUrl, request);
  356. if (onError) {
  357. onError(error);
  358. }
  359. }
  360. };
  361. request.addEventListener("readystatechange", onReadyStateChange);
  362. request.send();
  363. };
  364. retryLoop(0);
  365. };
  366. // Caching all files
  367. if (offlineProvider && offlineProvider.enableSceneOffline) {
  368. const noOfflineSupport = (request?: any) => {
  369. if (request && request.status > 400) {
  370. if (onError) {
  371. onError(request);
  372. }
  373. } else {
  374. requestFile();
  375. }
  376. };
  377. const loadFromOfflineSupport = () => {
  378. // TODO: database needs to support aborting and should return a IFileRequest
  379. if (offlineProvider) {
  380. offlineProvider.loadFile(this.BaseUrl + url, (data) => {
  381. if (!aborted) {
  382. onSuccess(data);
  383. }
  384. fileRequest.onCompleteObservable.notifyObservers(fileRequest);
  385. }, onProgress ? (event) => {
  386. if (!aborted) {
  387. onProgress(event);
  388. }
  389. } : undefined, noOfflineSupport, useArrayBuffer);
  390. }
  391. };
  392. offlineProvider.open(loadFromOfflineSupport, noOfflineSupport);
  393. }
  394. else {
  395. requestFile();
  396. }
  397. return fileRequest;
  398. }
  399. /**
  400. * Checks if the loaded document was accessed via `file:`-Protocol.
  401. * @returns boolean
  402. */
  403. public static IsFileURL(): boolean {
  404. return location.protocol === "file:";
  405. }
  406. }