Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/gridfs/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export interface GridFSBucketReadStreamPrivate {
expected: number;

/**
* The filter used to search in the _files_ collection (i.e., `{ _id: <> }`)
* The filter used to search in the _files_ collection (i.e., `{ _id: { $eq: <> } }`)
* This is not the same filter used when reading chunks from the chunks collection.
*/
filter: Document;
Expand Down Expand Up @@ -318,9 +318,8 @@ function init(stream: GridFSBucketReadStream): void {
if (stream.destroyed) return;

if (!doc) {
const identifier = stream.s.filter._id
? stream.s.filter._id.toString()
: stream.s.filter.filename;
const identifier =
stream.s.filter._id != null ? stream.s.filter._id.$eq : stream.s.filter.filename;
const errmsg = `FileNotFound: file ${identifier} was not found`;
// TODO(NODE-3483)
const err = new MongoRuntimeError(errmsg);
Expand Down Expand Up @@ -349,7 +348,7 @@ function init(stream: GridFSBucketReadStream): void {
return stream.destroy(error);
}

const filter: Document = { files_id: doc._id };
const filter: Document = { files_id: { $eq: doc._id } };

// Currently the skip function does not support the index,
// it needs to retrieve all the documents first and then skip them. (CS-25811)
Expand Down
11 changes: 7 additions & 4 deletions src/gridfs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class GridFSBucket extends TypedEventEmitter<GridFSBucketEvents> {
this.s._chunksCollection,
this.s._filesCollection,
this.s.options.readPreference,
{ _id: id },
{ _id: { $eq: id } },
{ timeoutMS: this.s.options.timeoutMS, ...options }
);
}
Expand All @@ -171,15 +171,18 @@ export class GridFSBucket extends TypedEventEmitter<GridFSBucketEvents> {
}

const { deletedCount } = await this.s._filesCollection.deleteOne(
{ _id: id },
{ _id: { $eq: id } },
{ timeoutMS: timeoutContext?.remainingTimeMS }
);

const remainingTimeMS = timeoutContext?.remainingTimeMS;
if (remainingTimeMS != null && remainingTimeMS <= 0)
throw new MongoOperationTimeoutError(`Timed out after ${timeoutMS}ms`);
// Delete orphaned chunks before returning FileNotFound
await this.s._chunksCollection.deleteMany({ files_id: id }, { timeoutMS: remainingTimeMS });
await this.s._chunksCollection.deleteMany(
{ files_id: { $eq: id } },
{ timeoutMS: remainingTimeMS }
);

if (deletedCount === 0) {
// TODO(NODE-3483): Replace with more appropriate error
Expand Down Expand Up @@ -230,7 +233,7 @@ export class GridFSBucket extends TypedEventEmitter<GridFSBucketEvents> {
* @param filename - new name for the file
*/
async rename(id: ObjectId, filename: string, options?: { timeoutMS: number }): Promise<void> {
const filter = { _id: id };
const filter = { _id: { $eq: id } };
const update = { $set: { filename } };
const { matchedCount } = await this.s._filesCollection.updateOne(filter, update, options);
if (matchedCount === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/gridfs/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class GridFSBucketWriteStream extends Writable {
`Upload timed out after ${this.timeoutContext?.timeoutMS}ms`
);

await this.chunks.deleteMany({ files_id: this.id }, { timeoutMS: remainingTimeMS });
await this.chunks.deleteMany({ files_id: { $eq: this.id } }, { timeoutMS: remainingTimeMS });
}
}

Expand Down
80 changes: 80 additions & 0 deletions test/integration/gridfs/gridfs.prose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { promisify } from 'node:util';

import { expect } from 'chai';

import {
type Db,
GridFSBucket,
MinKey,
type MongoClient,
MongoRuntimeError,
type ObjectId
} from '../../mongodb';

describe('GridFS Prose Tests', function () {
let client: MongoClient;
let db: Db;

beforeEach(async function () {
client = this.configuration.newClient();
db = client.db('gridfs_prose_tests');
});

afterEach(async function () {
await db.dropDatabase().catch(() => null);
await client.close();
});

describe("1. Aborting an upload with an injected file ID does not delete other files' chunks", function () {
// This test asserts that the delete command executed when a GridFS upload stream is aborted does not delete chunks
// associated with other files.
//
// This test MUST be skipped on server versions older than 5.0. (These versions do not support document values with
// "$"-prefixed keys.)

it(
'does not delete chunks of other files',
{ requires: { mongodb: '>=5.0' } },
async function () {
// 1. Create a GridFS bucket (referred to as `bucket`).
const bucket = new GridFSBucket(db);
// Drop `bucket` to clear its contents.
await bucket.drop();

// 2. Construct a small, non-empty vector of bytes to upload to a GridFS file (referred to as `file1Bytes`).
const file1Bytes = Buffer.from([0x11, 0x22, 0x33, 0x44]);
// Upload `file1Bytes` to `bucket` with the filename of "file1".
await pipeline(Readable.from([file1Bytes]), bucket.openUploadStream('file1'));

// 3. Open an upload stream from `bucket` with a filename of "file2", a file ID of `{ "$gt": MinKey }`, and
// `chunkSizeBytes` set to 2 (referred to as `uploadStream`).
const injectedId = { $gt: new MinKey() } as unknown as ObjectId;
const uploadStream = bucket.openUploadStreamWithId(injectedId, 'file2', {
chunkSizeBytes: 2
});

// 4. Write a vector containing 4 bytes to `uploadStream`.
const write = promisify(uploadStream.write.bind(uploadStream));
await write(Buffer.from([0x55, 0x66, 0x77, 0x88]));
// Then, abort `uploadStream`.
await uploadStream.abort();

// 5. Download the contents of "file1" from `bucket`.
const file1Chunks = await bucket.openDownloadStreamByName('file1').toArray();
// Assert that the downloaded contents match `file1Bytes`.
expect(Buffer.concat(file1Chunks)).to.deep.equal(file1Bytes);

// 6. Attempt to download the contents of "file2" from `bucket`.
const error = await bucket
.openDownloadStreamByName('file2')
.toArray()
.catch(error => error);
// Assert that the download fails with a "FileNotFound" error.
expect(error).to.be.instanceOf(MongoRuntimeError);
expect(error.message).to.include('FileNotFound');
}
);
});
});
34 changes: 32 additions & 2 deletions test/spec/gridfs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ GridFS. These tests utilize the [Unified Test Format](../../unified-test-format/
The unified test format allows binary stream data to be expressed and matched with `$$hexBytes` (for uploads) and
`$$matchesHexBytes` (for downloads), respectively; however, those operators are not supported in all contexts, such as
`insertData` and `outcome`. When binary data must be expressed as a base64-encoded string
([Extended JSON](../../extended-json.md) for a BSON binary type), the test SHOULD include a comment noting the
equivalent value in hexadecimal for human-readability. For example:
([Extended JSON](../../extended-json/extended-json.md) for a BSON binary type), the test SHOULD include a comment noting
the equivalent value in hexadecimal for human-readability. For example:

```yaml
data: { $binary: { base64: "ESIzRA==", subType: "00" } } # hex 11223344
Expand All @@ -26,3 +26,33 @@ the following PHP one-liner:
$ php -r 'echo base64_encode(hex2bin('11223344')), "\n";'
ESIzRA==
```

## Prose Tests

### 1. Aborting an upload with an injected file ID does not delete other files' chunks

This test asserts that the delete command executed when a GridFS upload stream is aborted does not delete chunks
associated with other files.

This test MUST be skipped if a driver does not support opening an upload stream with a custom ID, accepting a document
as a file ID, or aborting an upload stream.

This test MUST be skipped on server versions older than 5.0. (These versions do not support document values with
"$"-prefixed keys.)

#### Test steps

1. Create a GridFS bucket (referred to as `bucket`). Drop `bucket` to clear its contents.

2. Construct a small, non-empty vector of bytes to upload to a GridFS file (referred to as `file1Bytes`). Upload
`file1Bytes` to `bucket` with the filename of "file1".

3. Open an upload stream from `bucket` with a filename of "file2", a file ID of `{ "$gt": MinKey }`, and
`chunkSizeBytes` set to 2 (referred to as `uploadStream`).

4. Write a vector containing 4 bytes to `uploadStream`. Then, abort `uploadStream`.

5. Download the contents of "file1" from `bucket`. Assert that the downloaded contents match `file1Bytes`.

6. Attempt to download the contents of "file2" from `bucket`. Assert that the download fails with a "FileNotFound"
error.
Loading
Loading