fileTools.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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. const inputText = input.toString();
  179. onError("Error while trying to load image: " + (inputText.length < 32 ? inputText : inputText.slice(0, 32) + "..."), err);
  180. }
  181. if (usingObjectURL && img.src) {
  182. URL.revokeObjectURL(img.src);
  183. }
  184. };
  185. img.addEventListener("load", loadHandler);
  186. img.addEventListener("error", errorHandler);
  187. var noOfflineSupport = () => {
  188. img.src = url;
  189. };
  190. var loadFromOfflineSupport = () => {
  191. if (offlineProvider) {
  192. offlineProvider.loadImage(url, img);
  193. }
  194. };
  195. if (url.substr(0, 5) !== "data:" && offlineProvider && offlineProvider.enableTexturesOffline) {
  196. offlineProvider.open(loadFromOfflineSupport, noOfflineSupport);
  197. }
  198. else {
  199. if (url.indexOf("file:") !== -1) {
  200. var textureName = decodeURIComponent(url.substring(5).toLowerCase());
  201. if (FilesInputStore.FilesToLoad[textureName]) {
  202. try {
  203. var blobURL;
  204. try {
  205. blobURL = URL.createObjectURL(FilesInputStore.FilesToLoad[textureName]);
  206. }
  207. catch (ex) {
  208. // Chrome doesn't support oneTimeOnly parameter
  209. blobURL = URL.createObjectURL(FilesInputStore.FilesToLoad[textureName]);
  210. }
  211. img.src = blobURL;
  212. usingObjectURL = true;
  213. }
  214. catch (e) {
  215. img.src = "";
  216. }
  217. return img;
  218. }
  219. }
  220. noOfflineSupport();
  221. }
  222. return img;
  223. }
  224. /**
  225. * Reads a file from a File object
  226. * @param file defines the file to load
  227. * @param onSuccess defines the callback to call when data is loaded
  228. * @param onProgress defines the callback to call during loading process
  229. * @param useArrayBuffer defines a boolean indicating that data must be returned as an ArrayBuffer
  230. * @param onError defines the callback to call when an error occurs
  231. * @returns a file request object
  232. */
  233. public static ReadFile(file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: ReadFileError) => void): IFileRequest {
  234. let reader = new FileReader();
  235. let request: IFileRequest = {
  236. onCompleteObservable: new Observable<IFileRequest>(),
  237. abort: () => reader.abort(),
  238. };
  239. reader.onloadend = (e) => request.onCompleteObservable.notifyObservers(request);
  240. if (onError) {
  241. reader.onerror = (e) => {
  242. onError(new ReadFileError(`Unable to read ${file.name}`, file));
  243. };
  244. }
  245. reader.onload = (e) => {
  246. //target doesn't have result from ts 1.3
  247. onSuccess((<any>e.target)['result']);
  248. };
  249. if (onProgress) {
  250. reader.onprogress = onProgress;
  251. }
  252. if (!useArrayBuffer) {
  253. // Asynchronous read
  254. reader.readAsText(file);
  255. }
  256. else {
  257. reader.readAsArrayBuffer(file);
  258. }
  259. return request;
  260. }
  261. /**
  262. * Loads a file from a url
  263. * @param url url to load
  264. * @param onSuccess callback called when the file successfully loads
  265. * @param onProgress callback called while file is loading (if the server supports this mode)
  266. * @param offlineProvider defines the offline provider for caching
  267. * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
  268. * @param onError callback called when the file fails to load
  269. * @returns a file request object
  270. */
  271. 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 {
  272. // If file and file input are set
  273. if (url.indexOf("file:") !== -1) {
  274. let fileName = decodeURIComponent(url.substring(5).toLowerCase());
  275. if (fileName.indexOf('./') === 0) {
  276. fileName = fileName.substring(2);
  277. }
  278. const file = FilesInputStore.FilesToLoad[fileName];
  279. if (file) {
  280. return FileTools.ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError ? (error) => onError(undefined, new LoadFileError(error.message, error.file)) : undefined);
  281. }
  282. }
  283. return FileTools.RequestFile(url, (data, request) => {
  284. onSuccess(data, request ? request.responseURL : undefined);
  285. }, onProgress, offlineProvider, useArrayBuffer, onError ? (error) => {
  286. onError(error.request, new LoadFileError(error.message, error.request));
  287. } : undefined);
  288. }
  289. /**
  290. * Loads a file
  291. * @param url url to load
  292. * @param onSuccess callback called when the file successfully loads
  293. * @param onProgress callback called while file is loading (if the server supports this mode)
  294. * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
  295. * @param onError callback called when the file fails to load
  296. * @param onOpened callback called when the web request is opened
  297. * @returns a file request object
  298. */
  299. 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 {
  300. url = FileTools._CleanUrl(url);
  301. url = FileTools.PreprocessUrl(url);
  302. const loadUrl = FileTools.BaseUrl + url;
  303. let aborted = false;
  304. const fileRequest: IFileRequest = {
  305. onCompleteObservable: new Observable<IFileRequest>(),
  306. abort: () => aborted = true,
  307. };
  308. const requestFile = () => {
  309. let request = new WebRequest();
  310. let retryHandle: Nullable<number> = null;
  311. fileRequest.abort = () => {
  312. aborted = true;
  313. if (request.readyState !== (XMLHttpRequest.DONE || 4)) {
  314. request.abort();
  315. }
  316. if (retryHandle !== null) {
  317. clearTimeout(retryHandle);
  318. retryHandle = null;
  319. }
  320. };
  321. const retryLoop = (retryIndex: number) => {
  322. request.open('GET', loadUrl);
  323. if (onOpened) {
  324. onOpened(request);
  325. }
  326. if (useArrayBuffer) {
  327. request.responseType = "arraybuffer";
  328. }
  329. if (onProgress) {
  330. request.addEventListener("progress", onProgress);
  331. }
  332. const onLoadEnd = () => {
  333. request.removeEventListener("loadend", onLoadEnd);
  334. fileRequest.onCompleteObservable.notifyObservers(fileRequest);
  335. fileRequest.onCompleteObservable.clear();
  336. };
  337. request.addEventListener("loadend", onLoadEnd);
  338. const onReadyStateChange = () => {
  339. if (aborted) {
  340. return;
  341. }
  342. // In case of undefined state in some browsers.
  343. if (request.readyState === (XMLHttpRequest.DONE || 4)) {
  344. // Some browsers have issues where onreadystatechange can be called multiple times with the same value.
  345. request.removeEventListener("readystatechange", onReadyStateChange);
  346. if ((request.status >= 200 && request.status < 300) || (request.status === 0 && (!DomManagement.IsWindowObjectExist() || FileTools.IsFileURL()))) {
  347. onSuccess(useArrayBuffer ? request.response : request.responseText, request);
  348. return;
  349. }
  350. let retryStrategy = FileTools.DefaultRetryStrategy;
  351. if (retryStrategy) {
  352. let waitTime = retryStrategy(loadUrl, request, retryIndex);
  353. if (waitTime !== -1) {
  354. // Prevent the request from completing for retry.
  355. request.removeEventListener("loadend", onLoadEnd);
  356. request = new WebRequest();
  357. retryHandle = setTimeout(() => retryLoop(retryIndex + 1), waitTime);
  358. return;
  359. }
  360. }
  361. const error = new RequestFileError("Error status: " + request.status + " " + request.statusText + " - Unable to load " + loadUrl, request);
  362. if (onError) {
  363. onError(error);
  364. }
  365. }
  366. };
  367. request.addEventListener("readystatechange", onReadyStateChange);
  368. request.send();
  369. };
  370. retryLoop(0);
  371. };
  372. // Caching all files
  373. if (offlineProvider && offlineProvider.enableSceneOffline) {
  374. const noOfflineSupport = (request?: any) => {
  375. if (request && request.status > 400) {
  376. if (onError) {
  377. onError(request);
  378. }
  379. } else {
  380. requestFile();
  381. }
  382. };
  383. const loadFromOfflineSupport = () => {
  384. // TODO: database needs to support aborting and should return a IFileRequest
  385. if (offlineProvider) {
  386. offlineProvider.loadFile(FileTools.BaseUrl + url, (data) => {
  387. if (!aborted) {
  388. onSuccess(data);
  389. }
  390. fileRequest.onCompleteObservable.notifyObservers(fileRequest);
  391. }, onProgress ? (event) => {
  392. if (!aborted) {
  393. onProgress(event);
  394. }
  395. } : undefined, noOfflineSupport, useArrayBuffer);
  396. }
  397. };
  398. offlineProvider.open(loadFromOfflineSupport, noOfflineSupport);
  399. }
  400. else {
  401. requestFile();
  402. }
  403. return fileRequest;
  404. }
  405. /**
  406. * Checks if the loaded document was accessed via `file:`-Protocol.
  407. * @returns boolean
  408. */
  409. public static IsFileURL(): boolean {
  410. return location.protocol === "file:";
  411. }
  412. }
  413. ThinEngine._FileToolsLoadImage = FileTools.LoadImage.bind(FileTools);
  414. ThinEngine._FileToolsLoadFile = FileTools.LoadFile.bind(FileTools);
  415. ShaderProcessor._FileToolsLoadFile = FileTools.LoadFile.bind(FileTools);