fileTools.ts 18 KB

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