download.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.GridFSBucketReadStream = void 0;
  4. const stream_1 = require("stream");
  5. const error_1 = require("../error");
  6. /**
  7. * A readable stream that enables you to read buffers from GridFS.
  8. *
  9. * Do not instantiate this class directly. Use `openDownloadStream()` instead.
  10. * @public
  11. */
  12. class GridFSBucketReadStream extends stream_1.Readable {
  13. /** @internal
  14. * @param chunks - Handle for chunks collection
  15. * @param files - Handle for files collection
  16. * @param readPreference - The read preference to use
  17. * @param filter - The filter to use to find the file document
  18. */
  19. constructor(chunks, files, readPreference, filter, options) {
  20. super();
  21. this.s = {
  22. bytesToTrim: 0,
  23. bytesToSkip: 0,
  24. bytesRead: 0,
  25. chunks,
  26. expected: 0,
  27. files,
  28. filter,
  29. init: false,
  30. expectedEnd: 0,
  31. options: {
  32. start: 0,
  33. end: 0,
  34. ...options
  35. },
  36. readPreference
  37. };
  38. }
  39. /**
  40. * Reads from the cursor and pushes to the stream.
  41. * Private Impl, do not call directly
  42. * @internal
  43. */
  44. _read() {
  45. if (this.destroyed)
  46. return;
  47. waitForFile(this, () => doRead(this));
  48. }
  49. /**
  50. * Sets the 0-based offset in bytes to start streaming from. Throws
  51. * an error if this stream has entered flowing mode
  52. * (e.g. if you've already called `on('data')`)
  53. *
  54. * @param start - 0-based offset in bytes to start streaming from
  55. */
  56. start(start = 0) {
  57. throwIfInitialized(this);
  58. this.s.options.start = start;
  59. return this;
  60. }
  61. /**
  62. * Sets the 0-based offset in bytes to start streaming from. Throws
  63. * an error if this stream has entered flowing mode
  64. * (e.g. if you've already called `on('data')`)
  65. *
  66. * @param end - Offset in bytes to stop reading at
  67. */
  68. end(end = 0) {
  69. throwIfInitialized(this);
  70. this.s.options.end = end;
  71. return this;
  72. }
  73. /**
  74. * Marks this stream as aborted (will never push another `data` event)
  75. * and kills the underlying cursor. Will emit the 'end' event, and then
  76. * the 'close' event once the cursor is successfully killed.
  77. *
  78. * @param callback - called when the cursor is successfully closed or an error occurred.
  79. */
  80. abort(callback) {
  81. this.push(null);
  82. this.destroyed = true;
  83. if (this.s.cursor) {
  84. this.s.cursor.close(error => {
  85. this.emit(GridFSBucketReadStream.CLOSE);
  86. callback && callback(error);
  87. });
  88. }
  89. else {
  90. if (!this.s.init) {
  91. // If not initialized, fire close event because we will never
  92. // get a cursor
  93. this.emit(GridFSBucketReadStream.CLOSE);
  94. }
  95. callback && callback();
  96. }
  97. }
  98. }
  99. exports.GridFSBucketReadStream = GridFSBucketReadStream;
  100. /**
  101. * An error occurred
  102. * @event
  103. */
  104. GridFSBucketReadStream.ERROR = 'error';
  105. /**
  106. * Fires when the stream loaded the file document corresponding to the provided id.
  107. * @event
  108. */
  109. GridFSBucketReadStream.FILE = 'file';
  110. /**
  111. * Emitted when a chunk of data is available to be consumed.
  112. * @event
  113. */
  114. GridFSBucketReadStream.DATA = 'data';
  115. /**
  116. * Fired when the stream is exhausted (no more data events).
  117. * @event
  118. */
  119. GridFSBucketReadStream.END = 'end';
  120. /**
  121. * Fired when the stream is exhausted and the underlying cursor is killed
  122. * @event
  123. */
  124. GridFSBucketReadStream.CLOSE = 'close';
  125. function throwIfInitialized(stream) {
  126. if (stream.s.init) {
  127. throw new error_1.MongoGridFSStreamError('Options cannot be changed after the stream is initialized');
  128. }
  129. }
  130. function doRead(stream) {
  131. if (stream.destroyed)
  132. return;
  133. if (!stream.s.cursor)
  134. return;
  135. if (!stream.s.file)
  136. return;
  137. stream.s.cursor.next((error, doc) => {
  138. if (stream.destroyed) {
  139. return;
  140. }
  141. if (error) {
  142. stream.emit(GridFSBucketReadStream.ERROR, error);
  143. return;
  144. }
  145. if (!doc) {
  146. stream.push(null);
  147. process.nextTick(() => {
  148. if (!stream.s.cursor)
  149. return;
  150. stream.s.cursor.close(error => {
  151. if (error) {
  152. stream.emit(GridFSBucketReadStream.ERROR, error);
  153. return;
  154. }
  155. stream.emit(GridFSBucketReadStream.CLOSE);
  156. });
  157. });
  158. return;
  159. }
  160. if (!stream.s.file)
  161. return;
  162. const bytesRemaining = stream.s.file.length - stream.s.bytesRead;
  163. const expectedN = stream.s.expected++;
  164. const expectedLength = Math.min(stream.s.file.chunkSize, bytesRemaining);
  165. if (doc.n > expectedN) {
  166. return stream.emit(GridFSBucketReadStream.ERROR, new error_1.MongoGridFSChunkError(`ChunkIsMissing: Got unexpected n: ${doc.n}, expected: ${expectedN}`));
  167. }
  168. if (doc.n < expectedN) {
  169. return stream.emit(GridFSBucketReadStream.ERROR, new error_1.MongoGridFSChunkError(`ExtraChunk: Got unexpected n: ${doc.n}, expected: ${expectedN}`));
  170. }
  171. let buf = Buffer.isBuffer(doc.data) ? doc.data : doc.data.buffer;
  172. if (buf.byteLength !== expectedLength) {
  173. if (bytesRemaining <= 0) {
  174. return stream.emit(GridFSBucketReadStream.ERROR, new error_1.MongoGridFSChunkError(`ExtraChunk: Got unexpected n: ${doc.n}, expected file length ${stream.s.file.length} bytes but already read ${stream.s.bytesRead} bytes`));
  175. }
  176. return stream.emit(GridFSBucketReadStream.ERROR, new error_1.MongoGridFSChunkError(`ChunkIsWrongSize: Got unexpected length: ${buf.byteLength}, expected: ${expectedLength}`));
  177. }
  178. stream.s.bytesRead += buf.byteLength;
  179. if (buf.byteLength === 0) {
  180. return stream.push(null);
  181. }
  182. let sliceStart = null;
  183. let sliceEnd = null;
  184. if (stream.s.bytesToSkip != null) {
  185. sliceStart = stream.s.bytesToSkip;
  186. stream.s.bytesToSkip = 0;
  187. }
  188. const atEndOfStream = expectedN === stream.s.expectedEnd - 1;
  189. const bytesLeftToRead = stream.s.options.end - stream.s.bytesToSkip;
  190. if (atEndOfStream && stream.s.bytesToTrim != null) {
  191. sliceEnd = stream.s.file.chunkSize - stream.s.bytesToTrim;
  192. }
  193. else if (stream.s.options.end && bytesLeftToRead < doc.data.byteLength) {
  194. sliceEnd = bytesLeftToRead;
  195. }
  196. if (sliceStart != null || sliceEnd != null) {
  197. buf = buf.slice(sliceStart || 0, sliceEnd || buf.byteLength);
  198. }
  199. stream.push(buf);
  200. });
  201. }
  202. function init(stream) {
  203. const findOneOptions = {};
  204. if (stream.s.readPreference) {
  205. findOneOptions.readPreference = stream.s.readPreference;
  206. }
  207. if (stream.s.options && stream.s.options.sort) {
  208. findOneOptions.sort = stream.s.options.sort;
  209. }
  210. if (stream.s.options && stream.s.options.skip) {
  211. findOneOptions.skip = stream.s.options.skip;
  212. }
  213. stream.s.files.findOne(stream.s.filter, findOneOptions, (error, doc) => {
  214. if (error) {
  215. return stream.emit(GridFSBucketReadStream.ERROR, error);
  216. }
  217. if (!doc) {
  218. const identifier = stream.s.filter._id
  219. ? stream.s.filter._id.toString()
  220. : stream.s.filter.filename;
  221. const errmsg = `FileNotFound: file ${identifier} was not found`;
  222. // TODO(NODE-3483)
  223. const err = new error_1.MongoRuntimeError(errmsg);
  224. err.code = 'ENOENT'; // TODO: NODE-3338 set property as part of constructor
  225. return stream.emit(GridFSBucketReadStream.ERROR, err);
  226. }
  227. // If document is empty, kill the stream immediately and don't
  228. // execute any reads
  229. if (doc.length <= 0) {
  230. stream.push(null);
  231. return;
  232. }
  233. if (stream.destroyed) {
  234. // If user destroys the stream before we have a cursor, wait
  235. // until the query is done to say we're 'closed' because we can't
  236. // cancel a query.
  237. stream.emit(GridFSBucketReadStream.CLOSE);
  238. return;
  239. }
  240. try {
  241. stream.s.bytesToSkip = handleStartOption(stream, doc, stream.s.options);
  242. }
  243. catch (error) {
  244. return stream.emit(GridFSBucketReadStream.ERROR, error);
  245. }
  246. const filter = { files_id: doc._id };
  247. // Currently (MongoDB 3.4.4) skip function does not support the index,
  248. // it needs to retrieve all the documents first and then skip them. (CS-25811)
  249. // As work around we use $gte on the "n" field.
  250. if (stream.s.options && stream.s.options.start != null) {
  251. const skip = Math.floor(stream.s.options.start / doc.chunkSize);
  252. if (skip > 0) {
  253. filter['n'] = { $gte: skip };
  254. }
  255. }
  256. stream.s.cursor = stream.s.chunks.find(filter).sort({ n: 1 });
  257. if (stream.s.readPreference) {
  258. stream.s.cursor.withReadPreference(stream.s.readPreference);
  259. }
  260. stream.s.expectedEnd = Math.ceil(doc.length / doc.chunkSize);
  261. stream.s.file = doc;
  262. try {
  263. stream.s.bytesToTrim = handleEndOption(stream, doc, stream.s.cursor, stream.s.options);
  264. }
  265. catch (error) {
  266. return stream.emit(GridFSBucketReadStream.ERROR, error);
  267. }
  268. stream.emit(GridFSBucketReadStream.FILE, doc);
  269. });
  270. }
  271. function waitForFile(stream, callback) {
  272. if (stream.s.file) {
  273. return callback();
  274. }
  275. if (!stream.s.init) {
  276. init(stream);
  277. stream.s.init = true;
  278. }
  279. stream.once('file', () => {
  280. callback();
  281. });
  282. }
  283. function handleStartOption(stream, doc, options) {
  284. if (options && options.start != null) {
  285. if (options.start > doc.length) {
  286. throw new error_1.MongoInvalidArgumentError(`Stream start (${options.start}) must not be more than the length of the file (${doc.length})`);
  287. }
  288. if (options.start < 0) {
  289. throw new error_1.MongoInvalidArgumentError(`Stream start (${options.start}) must not be negative`);
  290. }
  291. if (options.end != null && options.end < options.start) {
  292. throw new error_1.MongoInvalidArgumentError(`Stream start (${options.start}) must not be greater than stream end (${options.end})`);
  293. }
  294. stream.s.bytesRead = Math.floor(options.start / doc.chunkSize) * doc.chunkSize;
  295. stream.s.expected = Math.floor(options.start / doc.chunkSize);
  296. return options.start - stream.s.bytesRead;
  297. }
  298. throw new error_1.MongoInvalidArgumentError('Start option must be defined');
  299. }
  300. function handleEndOption(stream, doc, cursor, options) {
  301. if (options && options.end != null) {
  302. if (options.end > doc.length) {
  303. throw new error_1.MongoInvalidArgumentError(`Stream end (${options.end}) must not be more than the length of the file (${doc.length})`);
  304. }
  305. if (options.start == null || options.start < 0) {
  306. throw new error_1.MongoInvalidArgumentError(`Stream end (${options.end}) must not be negative`);
  307. }
  308. const start = options.start != null ? Math.floor(options.start / doc.chunkSize) : 0;
  309. cursor.limit(Math.ceil(options.end / doc.chunkSize) - start);
  310. stream.s.expectedEnd = Math.ceil(options.end / doc.chunkSize);
  311. return Math.ceil(options.end / doc.chunkSize) * doc.chunkSize - options.end;
  312. }
  313. throw new error_1.MongoInvalidArgumentError('End option must be defined');
  314. }
  315. //# sourceMappingURL=download.js.map