0
# Attachments
1
2
PouchDB provides comprehensive binary attachment management for storing and retrieving files associated with documents. Attachments support various content types including images, documents, audio, and any binary data.
3
4
## Capabilities
5
6
### Creating Attachments
7
8
#### db.putAttachment()
9
10
Attaches binary data to a document.
11
12
```javascript { .api }
13
/**
14
* Attach binary data to a document
15
* @param docId - Document ID to attach to
16
* @param attachmentId - Unique identifier for the attachment
17
* @param rev - Current document revision
18
* @param blob - Binary data (Blob, Buffer, ArrayBuffer, or base64 string)
19
* @param type - MIME type of the attachment
20
* @param callback - Optional callback function (err, result) => void
21
* @returns Promise resolving to attachment operation result
22
*/
23
db.putAttachment(docId, attachmentId, rev, blob, type, callback);
24
```
25
26
**Usage Examples:**
27
28
```javascript
29
// Attach a text file
30
const doc = await db.get('user_001');
31
const textContent = 'This is a text file content';
32
const textBlob = new Blob([textContent], { type: 'text/plain' });
33
34
const result = await db.putAttachment(
35
'user_001',
36
'notes.txt',
37
doc._rev,
38
textBlob,
39
'text/plain'
40
);
41
42
// Attach an image (browser)
43
const fileInput = document.getElementById('fileInput');
44
const file = fileInput.files[0];
45
46
if (file) {
47
const doc = await db.get('user_001');
48
const result = await db.putAttachment(
49
'user_001',
50
'profile-photo.jpg',
51
doc._rev,
52
file,
53
file.type
54
);
55
}
56
57
// Attach binary data (Node.js)
58
const fs = require('fs');
59
const imageBuffer = fs.readFileSync('./image.png');
60
61
const doc = await db.get('document_001');
62
const result = await db.putAttachment(
63
'document_001',
64
'image.png',
65
doc._rev,
66
imageBuffer,
67
'image/png'
68
);
69
70
// Attach base64 encoded data
71
const base64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
72
const doc = await db.get('user_001');
73
74
const result = await db.putAttachment(
75
'user_001',
76
'pixel.png',
77
doc._rev,
78
base64Data,
79
'image/png'
80
);
81
```
82
83
### Retrieving Attachments
84
85
#### db.getAttachment()
86
87
Retrieves an attachment from a document.
88
89
```javascript { .api }
90
/**
91
* Retrieve an attachment
92
* @param docId - Document ID containing the attachment
93
* @param attachmentId - Attachment identifier
94
* @param options - Optional retrieval parameters
95
* @param callback - Optional callback function (err, blob) => void
96
* @returns Promise resolving to attachment data
97
*/
98
db.getAttachment(docId, attachmentId, options, callback);
99
```
100
101
**Usage Examples:**
102
103
```javascript
104
// Get attachment as Blob (browser)
105
const blob = await db.getAttachment('user_001', 'profile-photo.jpg');
106
const imageUrl = URL.createObjectURL(blob);
107
document.getElementById('profileImage').src = imageUrl;
108
109
// Get attachment as Buffer (Node.js)
110
const buffer = await db.getAttachment('document_001', 'image.png');
111
const fs = require('fs');
112
fs.writeFileSync('./downloaded-image.png', buffer);
113
114
// Get specific revision of attachment
115
const blob = await db.getAttachment('user_001', 'notes.txt', {
116
rev: '2-abc123def456'
117
});
118
119
// Get attachment as binary string
120
const binaryString = await db.getAttachment('user_001', 'data.bin', {
121
binary: true
122
});
123
```
124
125
### Removing Attachments
126
127
#### db.removeAttachment()
128
129
Removes an attachment from a document.
130
131
```javascript { .api }
132
/**
133
* Remove an attachment from a document
134
* @param docId - Document ID containing the attachment
135
* @param attachmentId - Attachment identifier to remove
136
* @param rev - Current document revision
137
* @param callback - Optional callback function (err, result) => void
138
* @returns Promise resolving to removal operation result
139
*/
140
db.removeAttachment(docId, attachmentId, rev, callback);
141
```
142
143
**Usage Examples:**
144
145
```javascript
146
// Remove an attachment
147
const doc = await db.get('user_001');
148
const result = await db.removeAttachment(
149
'user_001',
150
'old-photo.jpg',
151
doc._rev
152
);
153
154
console.log('Attachment removed:', result.ok);
155
156
// Remove with error handling
157
try {
158
const doc = await db.get('user_001');
159
await db.removeAttachment('user_001', 'notes.txt', doc._rev);
160
console.log('Attachment removed successfully');
161
} catch (err) {
162
console.error('Failed to remove attachment:', err);
163
}
164
```
165
166
## Attachment Metadata
167
168
### Retrieving Attachment Information
169
170
```javascript
171
// Get document with attachment metadata
172
const doc = await db.get('user_001', {
173
attachments: true,
174
binary: false
175
});
176
177
console.log(doc._attachments);
178
// Output example:
179
// {
180
// "profile-photo.jpg": {
181
// "content_type": "image/jpeg",
182
// "revpos": 2,
183
// "digest": "md5-abc123def456",
184
// "length": 45123,
185
// "stub": true
186
// }
187
// }
188
189
// Get document with attachment data included
190
const docWithAttachments = await db.get('user_001', {
191
attachments: true,
192
binary: true
193
});
194
195
// Attachment data will be included as base64 strings
196
console.log(docWithAttachments._attachments['profile-photo.jpg'].data);
197
```
198
199
### Attachment Metadata Structure
200
201
```javascript { .api }
202
interface AttachmentMetadata {
203
/** MIME type of the attachment */
204
content_type: string;
205
206
/** Revision position when attachment was added */
207
revpos: number;
208
209
/** MD5 digest of the attachment content */
210
digest: string;
211
212
/** Size of the attachment in bytes */
213
length: number;
214
215
/** Indicates if attachment data is included */
216
stub: boolean;
217
218
/** Base64 encoded attachment data (when stub is false) */
219
data?: string;
220
}
221
```
222
223
## Configuration Options
224
225
### GetAttachment Options
226
227
```javascript { .api }
228
interface GetAttachmentOptions {
229
/** Specific document revision to retrieve attachment from */
230
rev?: string;
231
232
/** Return attachment as binary data */
233
binary?: boolean;
234
235
/** Additional retrieval options */
236
[key: string]: any;
237
}
238
```
239
240
## Advanced Usage Examples
241
242
### Bulk Attachment Operations
243
244
```javascript
245
// Upload multiple attachments to a document
246
async function uploadMultipleAttachments(docId, attachments) {
247
let doc = await db.get(docId);
248
249
for (const attachment of attachments) {
250
try {
251
const result = await db.putAttachment(
252
docId,
253
attachment.id,
254
doc._rev,
255
attachment.data,
256
attachment.type
257
);
258
259
// Update revision for next attachment
260
doc._rev = result.rev;
261
console.log(`Uploaded attachment: ${attachment.id}`);
262
} catch (err) {
263
console.error(`Failed to upload ${attachment.id}:`, err);
264
}
265
}
266
267
return doc;
268
}
269
270
// Usage
271
const attachments = [
272
{ id: 'photo1.jpg', data: imageBlob1, type: 'image/jpeg' },
273
{ id: 'photo2.jpg', data: imageBlob2, type: 'image/jpeg' },
274
{ id: 'notes.txt', data: textBlob, type: 'text/plain' }
275
];
276
277
await uploadMultipleAttachments('user_001', attachments);
278
```
279
280
### Attachment Synchronization
281
282
```javascript
283
// Download all attachments from a document
284
async function downloadAllAttachments(docId, downloadPath) {
285
const doc = await db.get(docId, { attachments: false });
286
287
if (!doc._attachments) {
288
console.log('No attachments found');
289
return;
290
}
291
292
const attachmentIds = Object.keys(doc._attachments);
293
const downloads = [];
294
295
for (const attachmentId of attachmentIds) {
296
try {
297
const blob = await db.getAttachment(docId, attachmentId);
298
downloads.push({
299
id: attachmentId,
300
data: blob,
301
metadata: doc._attachments[attachmentId]
302
});
303
} catch (err) {
304
console.error(`Failed to download ${attachmentId}:`, err);
305
}
306
}
307
308
return downloads;
309
}
310
311
// Sync attachments between databases
312
async function syncAttachments(sourceDB, targetDB, docId) {
313
// Get source document with attachment metadata
314
const sourceDoc = await sourceDB.get(docId, { attachments: false });
315
316
if (!sourceDoc._attachments) {
317
return;
318
}
319
320
// Get or create target document
321
let targetDoc;
322
try {
323
targetDoc = await targetDB.get(docId);
324
} catch (err) {
325
if (err.status === 404) {
326
// Create document without attachments first
327
const { _attachments, ...docWithoutAttachments } = sourceDoc;
328
targetDoc = await targetDB.put(docWithoutAttachments);
329
} else {
330
throw err;
331
}
332
}
333
334
// Sync each attachment
335
for (const attachmentId of Object.keys(sourceDoc._attachments)) {
336
try {
337
const attachmentData = await sourceDB.getAttachment(docId, attachmentId);
338
const metadata = sourceDoc._attachments[attachmentId];
339
340
await targetDB.putAttachment(
341
docId,
342
attachmentId,
343
targetDoc._rev,
344
attachmentData,
345
metadata.content_type
346
);
347
348
// Update target document revision
349
targetDoc = await targetDB.get(docId);
350
} catch (err) {
351
console.error(`Failed to sync attachment ${attachmentId}:`, err);
352
}
353
}
354
}
355
```
356
357
### File Upload with Progress Tracking
358
359
```javascript
360
// Upload file with progress tracking (browser)
361
async function uploadFileWithProgress(docId, file, onProgress) {
362
const chunkSize = 64 * 1024; // 64KB chunks
363
const totalChunks = Math.ceil(file.size / chunkSize);
364
let uploadedChunks = 0;
365
366
// Read file in chunks and upload
367
const reader = new FileReader();
368
const chunks = [];
369
370
for (let i = 0; i < totalChunks; i++) {
371
const start = i * chunkSize;
372
const end = Math.min(start + chunkSize, file.size);
373
const chunk = file.slice(start, end);
374
375
const chunkData = await new Promise((resolve) => {
376
reader.onload = (e) => resolve(e.target.result);
377
reader.readAsArrayBuffer(chunk);
378
});
379
380
chunks.push(new Uint8Array(chunkData));
381
uploadedChunks++;
382
383
// Report progress
384
if (onProgress) {
385
onProgress({
386
loaded: uploadedChunks * chunkSize,
387
total: file.size,
388
percentage: (uploadedChunks / totalChunks) * 100
389
});
390
}
391
}
392
393
// Combine chunks and upload
394
const combinedData = new Uint8Array(file.size);
395
let offset = 0;
396
397
for (const chunk of chunks) {
398
combinedData.set(chunk, offset);
399
offset += chunk.length;
400
}
401
402
const blob = new Blob([combinedData], { type: file.type });
403
const doc = await db.get(docId);
404
405
return await db.putAttachment(
406
docId,
407
file.name,
408
doc._rev,
409
blob,
410
file.type
411
);
412
}
413
414
// Usage
415
const fileInput = document.getElementById('fileInput');
416
const file = fileInput.files[0];
417
418
await uploadFileWithProgress('user_001', file, (progress) => {
419
console.log(`Upload progress: ${progress.percentage.toFixed(2)}%`);
420
});
421
```
422
423
### Attachment Caching
424
425
```javascript
426
// Attachment caching system
427
class AttachmentCache {
428
constructor(maxSize = 50 * 1024 * 1024) { // 50MB default
429
this.cache = new Map();
430
this.maxSize = maxSize;
431
this.currentSize = 0;
432
}
433
434
async getAttachment(db, docId, attachmentId) {
435
const cacheKey = `${docId}/${attachmentId}`;
436
437
// Check cache first
438
if (this.cache.has(cacheKey)) {
439
const cached = this.cache.get(cacheKey);
440
// Move to end (LRU)
441
this.cache.delete(cacheKey);
442
this.cache.set(cacheKey, cached);
443
return cached.data;
444
}
445
446
// Fetch from database
447
const data = await db.getAttachment(docId, attachmentId);
448
const size = this._getDataSize(data);
449
450
// Add to cache if there's room
451
if (size <= this.maxSize) {
452
this._ensureSpace(size);
453
this.cache.set(cacheKey, { data, size });
454
this.currentSize += size;
455
}
456
457
return data;
458
}
459
460
_getDataSize(data) {
461
if (data instanceof Blob) {
462
return data.size;
463
} else if (data instanceof ArrayBuffer) {
464
return data.byteLength;
465
} else if (Buffer.isBuffer(data)) {
466
return data.length;
467
}
468
return 0;
469
}
470
471
_ensureSpace(neededSize) {
472
while (this.currentSize + neededSize > this.maxSize && this.cache.size > 0) {
473
const firstKey = this.cache.keys().next().value;
474
const removed = this.cache.get(firstKey);
475
this.cache.delete(firstKey);
476
this.currentSize -= removed.size;
477
}
478
}
479
480
clear() {
481
this.cache.clear();
482
this.currentSize = 0;
483
}
484
}
485
486
// Usage
487
const cache = new AttachmentCache();
488
489
// Get cached attachment
490
const imageData = await cache.getAttachment(db, 'user_001', 'profile-photo.jpg');
491
```
492
493
### Attachment Validation
494
495
```javascript
496
// Validate attachments before upload
497
class AttachmentValidator {
498
constructor(options = {}) {
499
this.maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB
500
this.allowedTypes = options.allowedTypes || [
501
'image/jpeg',
502
'image/png',
503
'image/gif',
504
'text/plain',
505
'application/pdf'
506
];
507
this.maxFilenameLength = options.maxFilenameLength || 255;
508
}
509
510
validate(attachmentId, data, contentType) {
511
const errors = [];
512
513
// Validate filename
514
if (!attachmentId || attachmentId.length === 0) {
515
errors.push('Attachment ID is required');
516
}
517
518
if (attachmentId.length > this.maxFilenameLength) {
519
errors.push(`Filename too long (max ${this.maxFilenameLength} characters)`);
520
}
521
522
// Validate content type
523
if (!this.allowedTypes.includes(contentType)) {
524
errors.push(`Content type ${contentType} not allowed`);
525
}
526
527
// Validate size
528
const size = this._getDataSize(data);
529
if (size > this.maxSize) {
530
errors.push(`File too large (max ${this.maxSize} bytes)`);
531
}
532
533
if (size === 0) {
534
errors.push('File is empty');
535
}
536
537
return {
538
valid: errors.length === 0,
539
errors
540
};
541
}
542
543
_getDataSize(data) {
544
if (data instanceof Blob) return data.size;
545
if (data instanceof ArrayBuffer) return data.byteLength;
546
if (Buffer.isBuffer(data)) return data.length;
547
if (typeof data === 'string') return data.length;
548
return 0;
549
}
550
}
551
552
// Usage
553
const validator = new AttachmentValidator({
554
maxSize: 5 * 1024 * 1024, // 5MB
555
allowedTypes: ['image/jpeg', 'image/png']
556
});
557
558
async function uploadWithValidation(docId, attachmentId, data, contentType) {
559
const validation = validator.validate(attachmentId, data, contentType);
560
561
if (!validation.valid) {
562
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
563
}
564
565
const doc = await db.get(docId);
566
return await db.putAttachment(docId, attachmentId, doc._rev, data, contentType);
567
}
568
```
569
570
## Performance Considerations
571
572
### Optimizing Attachment Performance
573
574
```javascript
575
// Efficient attachment handling for large files
576
async function handleLargeAttachment(docId, attachmentId, file) {
577
// Check if attachment already exists
578
try {
579
const doc = await db.get(docId, { attachments: false });
580
const existingAttachment = doc._attachments?.[attachmentId];
581
582
if (existingAttachment) {
583
// Calculate file hash to check if upload is necessary
584
const fileHash = await calculateFileHash(file);
585
if (existingAttachment.digest === `md5-${fileHash}`) {
586
console.log('Attachment unchanged, skipping upload');
587
return { ok: true, unchanged: true };
588
}
589
}
590
} catch (err) {
591
// Document doesn't exist, continue with upload
592
}
593
594
// Upload with compression for text files
595
let dataToUpload = file;
596
if (file.type.startsWith('text/')) {
597
dataToUpload = await compressData(file);
598
}
599
600
const doc = await db.get(docId).catch(() => ({ _id: docId }));
601
return await db.putAttachment(
602
docId,
603
attachmentId,
604
doc._rev,
605
dataToUpload,
606
file.type
607
);
608
}
609
610
// Memory-efficient attachment streaming (Node.js)
611
const stream = require('stream');
612
613
function createAttachmentStream(db, docId, attachmentId) {
614
return new stream.Readable({
615
async read() {
616
try {
617
const data = await db.getAttachment(docId, attachmentId);
618
this.push(data);
619
this.push(null); // End of stream
620
} catch (err) {
621
this.emit('error', err);
622
}
623
}
624
});
625
}
626
```