remote.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { parseByteRanges, parseContentRange, parseContentType } from './httputils.js';
  2. import { BaseSource } from './basesource.js';
  3. import { BlockedSource } from './blockedsource.js';
  4. import { FetchClient } from './client/fetch.js';
  5. import { XHRClient } from './client/xhr.js';
  6. import { HttpClient } from './client/http.js';
  7. class RemoteSource extends BaseSource {
  8. /**
  9. *
  10. * @param {BaseClient} client
  11. * @param {object} headers
  12. * @param {numbers} maxRanges
  13. * @param {boolean} allowFullFile
  14. */
  15. constructor(client, headers, maxRanges, allowFullFile) {
  16. super();
  17. this.client = client;
  18. this.headers = headers;
  19. this.maxRanges = maxRanges;
  20. this.allowFullFile = allowFullFile;
  21. this._fileSize = null;
  22. }
  23. /**
  24. *
  25. * @param {Slice[]} slices
  26. */
  27. async fetch(slices, signal) {
  28. // if we allow multi-ranges, split the incoming request into that many sub-requests
  29. // and join them afterwards
  30. if (this.maxRanges >= slices.length) {
  31. return this.fetchSlices(slices, signal);
  32. } else if (this.maxRanges > 0 && slices.length > 1) {
  33. // TODO: split into multiple multi-range requests
  34. // const subSlicesRequests = [];
  35. // for (let i = 0; i < slices.length; i += this.maxRanges) {
  36. // subSlicesRequests.push(
  37. // this.fetchSlices(slices.slice(i, i + this.maxRanges), signal),
  38. // );
  39. // }
  40. // return (await Promise.all(subSlicesRequests)).flat();
  41. }
  42. // otherwise make a single request for each slice
  43. return Promise.all(
  44. slices.map((slice) => this.fetchSlice(slice, signal)),
  45. );
  46. }
  47. async fetchSlices(slices, signal) {
  48. const response = await this.client.request({
  49. headers: {
  50. ...this.headers,
  51. Range: `bytes=${slices
  52. .map(({ offset, length }) => `${offset}-${offset + length}`)
  53. .join(',')
  54. }`,
  55. },
  56. signal,
  57. });
  58. if (!response.ok) {
  59. throw new Error('Error fetching data.');
  60. } else if (response.status === 206) {
  61. const { type, params } = parseContentType(response.getHeader('content-type'));
  62. if (type === 'multipart/byteranges') {
  63. const byteRanges = parseByteRanges(await response.getData(), params.boundary);
  64. this._fileSize = byteRanges[0].fileSize || null;
  65. return byteRanges;
  66. }
  67. const data = await response.getData();
  68. const { start, end, total } = parseContentRange(response.getHeader('content-range'));
  69. this._fileSize = total || null;
  70. const first = [{
  71. data,
  72. offset: start,
  73. length: end - start,
  74. }];
  75. if (slices.length > 1) {
  76. // we requested more than one slice, but got only the first
  77. // unfortunately, some HTTP Servers don't support multi-ranges
  78. // and return only the first
  79. // get the rest of the slices and fetch them iteratively
  80. const others = await Promise.all(slices.slice(1).map((slice) => this.fetchSlice(slice, signal)));
  81. return first.concat(others);
  82. }
  83. return first;
  84. } else {
  85. if (!this.allowFullFile) {
  86. throw new Error('Server responded with full file');
  87. }
  88. const data = await response.getData();
  89. this._fileSize = data.byteLength;
  90. return [{
  91. data,
  92. offset: 0,
  93. length: data.byteLength,
  94. }];
  95. }
  96. }
  97. async fetchSlice(slice, signal) {
  98. const { offset, length } = slice;
  99. const response = await this.client.request({
  100. headers: {
  101. ...this.headers,
  102. Range: `bytes=${offset}-${offset + length}`,
  103. },
  104. signal,
  105. });
  106. // check the response was okay and if the server actually understands range requests
  107. if (!response.ok) {
  108. throw new Error('Error fetching data.');
  109. } else if (response.status === 206) {
  110. const data = await response.getData();
  111. const { total } = parseContentRange(response.getHeader('content-range'));
  112. this._fileSize = total || null;
  113. return {
  114. data,
  115. offset,
  116. length,
  117. };
  118. } else {
  119. if (!this.allowFullFile) {
  120. throw new Error('Server responded with full file');
  121. }
  122. const data = await response.getData();
  123. this._fileSize = data.byteLength;
  124. return {
  125. data,
  126. offset: 0,
  127. length: data.byteLength,
  128. };
  129. }
  130. }
  131. get fileSize() {
  132. return this._fileSize;
  133. }
  134. }
  135. function maybeWrapInBlockedSource(source, { blockSize, cacheSize }) {
  136. if (blockSize === null) {
  137. return source;
  138. }
  139. return new BlockedSource(source, { blockSize, cacheSize });
  140. }
  141. export function makeFetchSource(url, { headers = {}, credentials, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
  142. const client = new FetchClient(url, credentials);
  143. const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
  144. return maybeWrapInBlockedSource(source, blockOptions);
  145. }
  146. export function makeXHRSource(url, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
  147. const client = new XHRClient(url);
  148. const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
  149. return maybeWrapInBlockedSource(source, blockOptions);
  150. }
  151. export function makeHttpSource(url, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
  152. const client = new HttpClient(url);
  153. const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
  154. return maybeWrapInBlockedSource(source, blockOptions);
  155. }
  156. /**
  157. *
  158. * @param {string} url
  159. * @param {object} options
  160. */
  161. export function makeRemoteSource(url, { forceXHR = false, ...clientOptions } = {}) {
  162. if (typeof fetch === 'function' && !forceXHR) {
  163. return makeFetchSource(url, clientOptions);
  164. }
  165. if (typeof XMLHttpRequest !== 'undefined') {
  166. return makeXHRSource(url, clientOptions);
  167. }
  168. return makeHttpSource(url, clientOptions);
  169. }