CoolFace
Apppublic

opusdev/vector-similarity-api

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
upload.ts560 linesDownload Raw Back to gridfs
1import { Writable } from 'stream';2 3import { type Document, ObjectId } from '../bson';4import type { Collection } from '../collection';5import { CursorTimeoutMode } from '../cursor/abstract_cursor';6import {7  MongoAPIError,8  MONGODB_ERROR_CODES,9  MongoError,10  MongoOperationTimeoutError11} from '../error';12import { CSOTTimeoutContext } from '../timeout';13import { type Callback, resolveTimeoutOptions, squashError } from '../utils';14import type { WriteConcernOptions } from '../write_concern';15import { WriteConcern } from './../write_concern';16import type { GridFSFile } from './download';17import type { GridFSBucket } from './index';18 19/** @public */20export interface GridFSChunk {21  _id: ObjectId;22  files_id: ObjectId;23  n: number;24  data: Buffer | Uint8Array;25}26 27/** @public */28export interface GridFSBucketWriteStreamOptions extends WriteConcernOptions {29  /** Overwrite this bucket's chunkSizeBytes for this file */30  chunkSizeBytes?: number;31  /** Custom file id for the GridFS file. */32  id?: ObjectId;33  /** Object to store in the file document's `metadata` field */34  metadata?: Document;35  /**36   * String to store in the file document's `contentType` field.37   * @deprecated Will be removed in the next major version. Add a contentType field to the metadata document instead.38   */39  contentType?: string;40  /**41   * Array of strings to store in the file document's `aliases` field.42   * @deprecated Will be removed in the next major version. Add an aliases field to the metadata document instead.43   */44  aliases?: string[];45  /**46   * @experimental47   * Specifies the time an operation will run until it throws a timeout error48   */49  timeoutMS?: number;50}51 52/**53 * A writable stream that enables you to write buffers to GridFS.54 *55 * Do not instantiate this class directly. Use `openUploadStream()` instead.56 * @public57 */58export class GridFSBucketWriteStream extends Writable {59  bucket: GridFSBucket;60  /** A Collection instance where the file's chunks are stored */61  chunks: Collection<GridFSChunk>;62  /** A Collection instance where the file's GridFSFile document is stored */63  files: Collection<GridFSFile>;64  /** The name of the file */65  filename: string;66  /** Options controlling the metadata inserted along with the file */67  options: GridFSBucketWriteStreamOptions;68  /** Indicates the stream is finished uploading */69  done: boolean;70  /** The ObjectId used for the `_id` field on the GridFSFile document */71  id: ObjectId;72  /** The number of bytes that each chunk will be limited to */73  chunkSizeBytes: number;74  /** Space used to store a chunk currently being inserted */75  bufToStore: Buffer;76  /** Accumulates the number of bytes inserted as the stream uploads chunks */77  length: number;78  /** Accumulates the number of chunks inserted as the stream uploads file contents */79  n: number;80  /** Tracks the current offset into the buffered bytes being uploaded */81  pos: number;82  /** Contains a number of properties indicating the current state of the stream */83  state: {84    /** If set the stream has ended */85    streamEnd: boolean;86    /** Indicates the number of chunks that still need to be inserted to exhaust the current buffered data */87    outstandingRequests: number;88    /** If set an error occurred during insertion */89    errored: boolean;90    /** If set the stream was intentionally aborted */91    aborted: boolean;92  };93  /** The write concern setting to be used with every insert operation */94  writeConcern?: WriteConcern;95  /**96   * The document containing information about the inserted file.97   * This property is defined _after_ the finish event has been emitted.98   * It will remain `null` if an error occurs.99   *100   * @example101   * ```ts102   * fs.createReadStream('file.txt')103   *   .pipe(bucket.openUploadStream('file.txt'))104   *   .on('finish', function () {105   *     console.log(this.gridFSFile)106   *   })107   * ```108   */109  gridFSFile: GridFSFile | null = null;110  /** @internal */111  timeoutContext?: CSOTTimeoutContext;112 113  /**114   * @param bucket - Handle for this stream's corresponding bucket115   * @param filename - The value of the 'filename' key in the files doc116   * @param options - Optional settings.117   * @internal118   */119  constructor(bucket: GridFSBucket, filename: string, options?: GridFSBucketWriteStreamOptions) {120    super();121 122    options = options ?? {};123    this.bucket = bucket;124    this.chunks = bucket.s._chunksCollection;125    this.filename = filename;126    this.files = bucket.s._filesCollection;127    this.options = options;128    this.writeConcern = WriteConcern.fromOptions(options) || bucket.s.options.writeConcern;129    // Signals the write is all done130    this.done = false;131 132    this.id = options.id ? options.id : new ObjectId();133    // properly inherit the default chunksize from parent134    this.chunkSizeBytes = options.chunkSizeBytes || this.bucket.s.options.chunkSizeBytes;135    this.bufToStore = Buffer.alloc(this.chunkSizeBytes);136    this.length = 0;137    this.n = 0;138    this.pos = 0;139    this.state = {140      streamEnd: false,141      outstandingRequests: 0,142      errored: false,143      aborted: false144    };145 146    if (options.timeoutMS != null)147      this.timeoutContext = new CSOTTimeoutContext({148        timeoutMS: options.timeoutMS,149        serverSelectionTimeoutMS: resolveTimeoutOptions(this.bucket.s.db.client, {})150          .serverSelectionTimeoutMS151      });152  }153 154  /**155   * @internal156   *157   * The stream is considered constructed when the indexes are done being created158   */159  override _construct(callback: (error?: Error | null) => void): void {160    if (!this.bucket.s.calledOpenUploadStream) {161      this.bucket.s.calledOpenUploadStream = true;162 163      checkIndexes(this).then(164        () => {165          this.bucket.s.checkedIndexes = true;166          this.bucket.emit('index');167          callback();168        },169        error => {170          if (error instanceof MongoOperationTimeoutError) {171            return handleError(this, error, callback);172          }173          squashError(error);174          callback();175        }176      );177    } else {178      return process.nextTick(callback);179    }180  }181 182  /**183   * @internal184   * Write a buffer to the stream.185   *186   * @param chunk - Buffer to write187   * @param encoding - Optional encoding for the buffer188   * @param callback - Function to call when the chunk was added to the buffer, or if the entire chunk was persisted to MongoDB if this chunk caused a flush.189   */190  override _write(191    chunk: Buffer | string,192    encoding: BufferEncoding,193    callback: Callback<void>194  ): void {195    doWrite(this, chunk, encoding, callback);196  }197 198  /** @internal */199  override _final(callback: (error?: Error | null) => void): void {200    if (this.state.streamEnd) {201      return process.nextTick(callback);202    }203    this.state.streamEnd = true;204    writeRemnant(this, callback);205  }206 207  /**208   * Places this write stream into an aborted state (all future writes fail)209   * and deletes all chunks that have already been written.210   */211  async abort(): Promise<void> {212    if (this.state.streamEnd) {213      // TODO(NODE-3485): Replace with MongoGridFSStreamClosed214      throw new MongoAPIError('Cannot abort a stream that has already completed');215    }216 217    if (this.state.aborted) {218      // TODO(NODE-3485): Replace with MongoGridFSStreamClosed219      throw new MongoAPIError('Cannot call abort() on a stream twice');220    }221 222    this.state.aborted = true;223    const remainingTimeMS = this.timeoutContext?.getRemainingTimeMSOrThrow(224      `Upload timed out after ${this.timeoutContext?.timeoutMS}ms`225    );226 227    await this.chunks.deleteMany({ files_id: this.id }, { timeoutMS: remainingTimeMS });228  }229}230 231function handleError(stream: GridFSBucketWriteStream, error: Error, callback: Callback): void {232  if (stream.state.errored) {233    process.nextTick(callback);234    return;235  }236  stream.state.errored = true;237  process.nextTick(callback, error);238}239 240function createChunkDoc(filesId: ObjectId, n: number, data: Buffer): GridFSChunk {241  return {242    _id: new ObjectId(),243    files_id: filesId,244    n,245    data246  };247}248 249async function checkChunksIndex(stream: GridFSBucketWriteStream): Promise<void> {250  const index = { files_id: 1, n: 1 };251 252  let remainingTimeMS;253  remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow(254    `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`255  );256 257  let indexes;258  try {259    indexes = await stream.chunks260      .listIndexes({261        timeoutMode: remainingTimeMS != null ? CursorTimeoutMode.LIFETIME : undefined,262        timeoutMS: remainingTimeMS263      })264      .toArray();265  } catch (error) {266    if (error instanceof MongoError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound) {267      indexes = [];268    } else {269      throw error;270    }271  }272 273  const hasChunksIndex = !!indexes.find(index => {274    const keys = Object.keys(index.key);275    if (keys.length === 2 && index.key.files_id === 1 && index.key.n === 1) {276      return true;277    }278    return false;279  });280 281  if (!hasChunksIndex) {282    remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow(283      `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`284    );285    await stream.chunks.createIndex(index, {286      ...stream.writeConcern,287      background: true,288      unique: true,289      timeoutMS: remainingTimeMS290    });291  }292}293 294function checkDone(stream: GridFSBucketWriteStream, callback: Callback): void {295  if (stream.done) {296    return process.nextTick(callback);297  }298 299  if (stream.state.streamEnd && stream.state.outstandingRequests === 0 && !stream.state.errored) {300    // Set done so we do not trigger duplicate createFilesDoc301    stream.done = true;302    // Create a new files doc303    const gridFSFile = createFilesDoc(304      stream.id,305      stream.length,306      stream.chunkSizeBytes,307      stream.filename,308      stream.options.contentType,309      stream.options.aliases,310      stream.options.metadata311    );312 313    if (isAborted(stream, callback)) {314      return;315    }316 317    const remainingTimeMS = stream.timeoutContext?.remainingTimeMS;318    if (remainingTimeMS != null && remainingTimeMS <= 0) {319      return handleError(320        stream,321        new MongoOperationTimeoutError(322          `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`323        ),324        callback325      );326    }327 328    stream.files329      .insertOne(gridFSFile, { writeConcern: stream.writeConcern, timeoutMS: remainingTimeMS })330      .then(331        () => {332          stream.gridFSFile = gridFSFile;333          callback();334        },335        error => {336          return handleError(stream, error, callback);337        }338      );339    return;340  }341 342  process.nextTick(callback);343}344 345async function checkIndexes(stream: GridFSBucketWriteStream): Promise<void> {346  let remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow(347    `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`348  );349  const doc = await stream.files.findOne(350    {},351    {352      projection: { _id: 1 },353      timeoutMS: remainingTimeMS354    }355  );356  if (doc != null) {357    // If at least one document exists assume the collection has the required index358    return;359  }360 361  const index = { filename: 1, uploadDate: 1 };362 363  let indexes;364  remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow(365    `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`366  );367  const listIndexesOptions = {368    timeoutMode: remainingTimeMS != null ? CursorTimeoutMode.LIFETIME : undefined,369    timeoutMS: remainingTimeMS370  };371  try {372    indexes = await stream.files.listIndexes(listIndexesOptions).toArray();373  } catch (error) {374    if (error instanceof MongoError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound) {375      indexes = [];376    } else {377      throw error;378    }379  }380 381  const hasFileIndex = !!indexes.find(index => {382    const keys = Object.keys(index.key);383    if (keys.length === 2 && index.key.filename === 1 && index.key.uploadDate === 1) {384      return true;385    }386    return false;387  });388 389  if (!hasFileIndex) {390    remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow(391      `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`392    );393 394    await stream.files.createIndex(index, { background: false, timeoutMS: remainingTimeMS });395  }396 397  await checkChunksIndex(stream);398}399 400function createFilesDoc(401  _id: ObjectId,402  length: number,403  chunkSize: number,404  filename: string,405  contentType?: string,406  aliases?: string[],407  metadata?: Document408): GridFSFile {409  const ret: GridFSFile = {410    _id,411    length,412    chunkSize,413    uploadDate: new Date(),414    filename415  };416 417  if (contentType) {418    ret.contentType = contentType;419  }420 421  if (aliases) {422    ret.aliases = aliases;423  }424 425  if (metadata) {426    ret.metadata = metadata;427  }428 429  return ret;430}431 432function doWrite(433  stream: GridFSBucketWriteStream,434  chunk: Buffer | string,435  encoding: BufferEncoding,436  callback: Callback<void>437): void {438  if (isAborted(stream, callback)) {439    return;440  }441 442  const inputBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);443 444  stream.length += inputBuf.length;445 446  // Input is small enough to fit in our buffer447  if (stream.pos + inputBuf.length < stream.chunkSizeBytes) {448    inputBuf.copy(stream.bufToStore, stream.pos);449    stream.pos += inputBuf.length;450    process.nextTick(callback);451    return;452  }453 454  // Otherwise, buffer is too big for current chunk, so we need to flush455  // to MongoDB.456  let inputBufRemaining = inputBuf.length;457  let spaceRemaining: number = stream.chunkSizeBytes - stream.pos;458  let numToCopy = Math.min(spaceRemaining, inputBuf.length);459  let outstandingRequests = 0;460  while (inputBufRemaining > 0) {461    const inputBufPos = inputBuf.length - inputBufRemaining;462    inputBuf.copy(stream.bufToStore, stream.pos, inputBufPos, inputBufPos + numToCopy);463    stream.pos += numToCopy;464    spaceRemaining -= numToCopy;465    let doc: GridFSChunk;466    if (spaceRemaining === 0) {467      doc = createChunkDoc(stream.id, stream.n, Buffer.from(stream.bufToStore));468 469      const remainingTimeMS = stream.timeoutContext?.remainingTimeMS;470      if (remainingTimeMS != null && remainingTimeMS <= 0) {471        return handleError(472          stream,473          new MongoOperationTimeoutError(474            `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`475          ),476          callback477        );478      }479 480      ++stream.state.outstandingRequests;481      ++outstandingRequests;482 483      if (isAborted(stream, callback)) {484        return;485      }486 487      stream.chunks488        .insertOne(doc, { writeConcern: stream.writeConcern, timeoutMS: remainingTimeMS })489        .then(490          () => {491            --stream.state.outstandingRequests;492            --outstandingRequests;493 494            if (!outstandingRequests) {495              checkDone(stream, callback);496            }497          },498          error => {499            return handleError(stream, error, callback);500          }501        );502 503      spaceRemaining = stream.chunkSizeBytes;504      stream.pos = 0;505      ++stream.n;506    }507    inputBufRemaining -= numToCopy;508    numToCopy = Math.min(spaceRemaining, inputBufRemaining);509  }510}511 512function writeRemnant(stream: GridFSBucketWriteStream, callback: Callback): void {513  // Buffer is empty, so don't bother to insert514  if (stream.pos === 0) {515    return checkDone(stream, callback);516  }517 518  // Create a new buffer to make sure the buffer isn't bigger than it needs519  // to be.520  const remnant = Buffer.alloc(stream.pos);521  stream.bufToStore.copy(remnant, 0, 0, stream.pos);522  const doc = createChunkDoc(stream.id, stream.n, remnant);523 524  // If the stream was aborted, do not write remnant525  if (isAborted(stream, callback)) {526    return;527  }528 529  const remainingTimeMS = stream.timeoutContext?.remainingTimeMS;530  if (remainingTimeMS != null && remainingTimeMS <= 0) {531    return handleError(532      stream,533      new MongoOperationTimeoutError(534        `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms`535      ),536      callback537    );538  }539  ++stream.state.outstandingRequests;540  stream.chunks541    .insertOne(doc, { writeConcern: stream.writeConcern, timeoutMS: remainingTimeMS })542    .then(543      () => {544        --stream.state.outstandingRequests;545        checkDone(stream, callback);546      },547      error => {548        return handleError(stream, error, callback);549      }550    );551}552 553function isAborted(stream: GridFSBucketWriteStream, callback: Callback<void>): boolean {554  if (stream.state.aborted) {555    process.nextTick(callback, new MongoAPIError('Stream has been aborted'));556    return true;557  }558  return false;559}560