Skip to content

Commit a7c5b03

Browse files
andris9claude
andcommitted
docs(storage-handler): add JSDoc comments to StorageHandler class
Document the constructor, all three methods (add, get, delete), and define typedefs for options/return shapes. Add inline comments where logic is non-obvious (filename inference, base64 decoding, ownership scoping). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 86d2627 commit a7c5b03

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

lib/storage-handler.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,72 @@
11
'use strict';
22

3+
/**
4+
* @module storage-handler
5+
* @description Manages per-user file storage backed by MongoDB GridFS. Files stored here are
6+
* typically draft attachments uploaded via the REST API (`POST /users/:user/storage`) and later
7+
* attached to outgoing messages. The download route (`GET /users/:user/storage/:file`) bypasses
8+
* the `get()` method and streams directly from the public `gridstore` property via
9+
* `gridstore.openDownloadStream()`.
10+
*/
11+
312
const GridFSBucket = require('mongodb').GridFSBucket;
413
const libbase64 = require('libbase64');
514
const libmime = require('libmime');
615

16+
/**
17+
* @typedef {Object} StorageHandlerOptions
18+
* @property {import('mongodb').Db} database - Primary MongoDB database connection
19+
* @property {import('mongodb').Db} [gridfs] - GridFS database connection (defaults to `database`)
20+
* @property {import('mongodb').Db} [users] - Users database connection (defaults to `database`)
21+
* @property {Function} [loggelf] - Graylog logging function (accepted but unused by this class)
22+
*/
23+
24+
/**
25+
* @typedef {Object} FileAddOptions
26+
* @property {string} [filename] - Original filename. If omitted, one is generated from the date and contentType.
27+
* @property {string} [contentType] - MIME type. If omitted, inferred from filename or defaults to `application/octet-stream`.
28+
* @property {string} [encoding] - Content encoding identifier. When falsy or omitted, `content` is
29+
* written directly to GridFS as raw bytes — the caller is expected to provide a Buffer (or a string
30+
* that Node.js writable streams accept). When set to `'base64'`, the `content` string is piped
31+
* through a `libbase64.Decoder` that strips base64 encoding before writing the decoded bytes to
32+
* GridFS. `'base64'` is the only value accepted by the storage API route's Joi validation
33+
* (`.valid('base64')`); the message submission route defaults it to `'base64'` as well. Internally
34+
* any truthy value triggers the same base64 decode path — there is no branching for other encodings.
35+
* @property {Buffer|string} content - File content as a Buffer (raw) or string (when base64-encoded).
36+
* @property {string} [cid] - Optional Content-ID for inline attachments (e.g. embedded images in HTML drafts).
37+
*/
38+
39+
/**
40+
* @typedef {Object} StoredFileData
41+
* @property {string} id - Hex string of the GridFS file ObjectId
42+
* @property {string} filename - Stored filename
43+
* @property {string} contentType - MIME type
44+
* @property {number} size - File size in bytes
45+
* @property {Buffer} content - Entire file content buffered in memory
46+
* @property {string} [cid] - Content-ID if one was set during upload
47+
*/
48+
49+
/**
50+
* Handles user-scoped file storage operations using MongoDB GridFS. Files are stored in the
51+
* `storage.files` / `storage.chunks` collections with a 255 KB chunk size. Each file's metadata
52+
* includes the owning user's ObjectId, ensuring all read and delete operations are user-scoped.
53+
*
54+
* Used by the storage REST API routes (`lib/api/storage.js`) and the message submission route
55+
* (`lib/api/messages.js`) to attach uploaded files to outgoing draft messages.
56+
*/
757
class StorageHandler {
58+
/**
59+
* Creates a new StorageHandler instance.
60+
*
61+
* @param {StorageHandlerOptions} options - Configuration options
62+
* @example
63+
* const storageHandler = new StorageHandler({
64+
* database: db.database,
65+
* users: db.users,
66+
* gridfs: db.gridfs,
67+
* loggelf: message => loggelf(message)
68+
* });
69+
*/
870
constructor(options) {
971
this.database = options.database;
1072
this.gridfs = options.gridfs || options.database;
@@ -17,10 +79,42 @@ class StorageHandler {
1779
});
1880
}
1981

82+
/**
83+
* Stores a file in GridFS for the given user. If neither `filename` nor `contentType` is
84+
* provided, a date-based filename with `.bin` extension and `application/octet-stream` type
85+
* is generated. If only one is provided, the other is inferred using libmime. When
86+
* `options.encoding` is set (e.g. `'base64'`), the content is piped through a base64 decoder
87+
* before being written to GridFS.
88+
*
89+
* @param {import('mongodb').ObjectId} user - Owner user's ObjectId, stored in file metadata
90+
* @param {FileAddOptions} options - File data and metadata
91+
* @returns {Promise<import('mongodb').ObjectId>} The ObjectId of the newly stored GridFS file
92+
* @throws {Error} If the GridFS upload stream emits an error or content writing fails
93+
* @example
94+
* // Raw binary upload
95+
* let fileId = await storageHandler.add(userId, {
96+
* filename: 'report.pdf',
97+
* contentType: 'application/pdf',
98+
* content: pdfBuffer
99+
* });
100+
*
101+
* @example
102+
* // Base64-encoded upload (from REST API)
103+
* let fileId = await storageHandler.add(userId, {
104+
* filename: 'image.png',
105+
* contentType: 'image/png',
106+
* encoding: 'base64',
107+
* content: base64String,
108+
* cid: 'unique-cid@domain'
109+
* });
110+
*/
20111
async add(user, options) {
21112
let { filename, contentType, cid } = options;
22113

114+
// Generate a date-based default filename stem (e.g. 'upload-2024-01-15')
23115
let filebase = 'upload-' + new Date().toISOString().substr(0, 10);
116+
117+
// Infer missing filename or contentType from whichever one is provided
24118
if (!contentType && !filename) {
25119
filename = filebase + '.bin';
26120
contentType = 'application/octet-stream';
@@ -30,6 +124,7 @@ class StorageHandler {
30124
filename = filebase + '.' + libmime.detectExtension(contentType);
31125
}
32126

127+
// Build GridFS metadata; user scopes the file for ownership checks in get/delete
33128
let metadata = {
34129
user
35130
};
@@ -62,6 +157,7 @@ class StorageHandler {
62157
return;
63158
}
64159

160+
// Pipe content through a base64 decoder before writing to GridFS
65161
let decoder = new libbase64.Decoder();
66162
decoder.pipe(store);
67163

@@ -78,7 +174,27 @@ class StorageHandler {
78174
});
79175
}
80176

177+
/**
178+
* Retrieves a file from GridFS, buffering the entire content into memory. The file must
179+
* belong to the specified user (verified via `metadata.user`). Returns file metadata along
180+
* with the full content as a Buffer.
181+
*
182+
* Note: The entire file is buffered in memory (suitable for files up to the system's 64 MB
183+
* message size limit). For streaming large files to HTTP responses, use
184+
* `storageHandler.gridstore.openDownloadStream(fileId)` directly as the download route does.
185+
*
186+
* @param {import('mongodb').ObjectId} user - Owner user's ObjectId for ownership verification
187+
* @param {import('mongodb').ObjectId} file - GridFS file ObjectId to retrieve
188+
* @returns {Promise<StoredFileData>} File metadata and buffered content
189+
* @throws {Error} Throws with `responseCode: 404` and `code: 'FileNotFound'` if the file
190+
* does not exist or does not belong to the user
191+
* @throws {Error} If the GridFS download stream emits an error
192+
* @example
193+
* let fileData = await storageHandler.get(userId, new ObjectId(fileId));
194+
* // fileData: { id, filename, contentType, size, content: Buffer, cid }
195+
*/
81196
async get(user, file) {
197+
// Query verifies both file existence and user ownership
82198
let fileData = await this.gridfs.collection('storage.files').findOne({
83199
_id: file,
84200
'metadata.user': user
@@ -91,6 +207,7 @@ class StorageHandler {
91207
throw err;
92208
}
93209

210+
// Stream the file from GridFS and buffer all chunks into a single Buffer
94211
return new Promise((resolve, reject) => {
95212
let stream = this.gridstore.openDownloadStream(file);
96213
let chunks = [];
@@ -121,7 +238,22 @@ class StorageHandler {
121238
});
122239
}
123240

241+
/**
242+
* Deletes a file from GridFS. The file must belong to the specified user (verified via
243+
* `metadata.user`). Removes both the `storage.files` document and all associated
244+
* `storage.chunks` documents. Typically called to clean up uploaded attachments after a
245+
* draft message is submitted for delivery.
246+
*
247+
* @param {import('mongodb').ObjectId} user - Owner user's ObjectId for ownership verification
248+
* @param {import('mongodb').ObjectId} file - GridFS file ObjectId to delete
249+
* @returns {Promise<void>}
250+
* @throws {Error} Throws with `responseCode: 404` and `code: 'FileNotFound'` if the file
251+
* does not exist or does not belong to the user
252+
* @example
253+
* await storageHandler.delete(userId, new ObjectId(fileId));
254+
*/
124255
async delete(user, file) {
256+
// Query verifies both file existence and user ownership before deletion
125257
let fileData = await this.gridfs.collection('storage.files').findOne({
126258
_id: file,
127259
'metadata.user': user
@@ -134,6 +266,7 @@ class StorageHandler {
134266
throw err;
135267
}
136268

269+
// Deletes the files document and all associated chunks from GridFS
137270
return this.gridstore.delete(file);
138271
}
139272
}

0 commit comments

Comments
 (0)