or run

npx @tessl/cli init
Log in

Version

Files

docs

attachments.mdbulk-queries.mdchanges-events.mddatabase-operations.mdindex.mdreplication-sync.md
tile.json

attachments.mddocs/

Attachments

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.

Capabilities

Creating Attachments

db.putAttachment()

Attaches binary data to a document.

/**
 * Attach binary data to a document
 * @param docId - Document ID to attach to
 * @param attachmentId - Unique identifier for the attachment
 * @param rev - Current document revision
 * @param blob - Binary data (Blob, Buffer, ArrayBuffer, or base64 string)
 * @param type - MIME type of the attachment
 * @param callback - Optional callback function (err, result) => void
 * @returns Promise resolving to attachment operation result
 */
db.putAttachment(docId, attachmentId, rev, blob, type, callback);

Usage Examples:

// Attach a text file
const doc = await db.get('user_001');
const textContent = 'This is a text file content';
const textBlob = new Blob([textContent], { type: 'text/plain' });

const result = await db.putAttachment(
  'user_001',
  'notes.txt',
  doc._rev,
  textBlob,
  'text/plain'
);

// Attach an image (browser)
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];

if (file) {
  const doc = await db.get('user_001');
  const result = await db.putAttachment(
    'user_001',
    'profile-photo.jpg',
    doc._rev,
    file,
    file.type
  );
}

// Attach binary data (Node.js)
const fs = require('fs');
const imageBuffer = fs.readFileSync('./image.png');

const doc = await db.get('document_001');
const result = await db.putAttachment(
  'document_001',
  'image.png',
  doc._rev,
  imageBuffer,
  'image/png'
);

// Attach base64 encoded data
const base64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
const doc = await db.get('user_001');

const result = await db.putAttachment(
  'user_001',
  'pixel.png',
  doc._rev,
  base64Data,
  'image/png'
);

Retrieving Attachments

db.getAttachment()

Retrieves an attachment from a document.

/**
 * Retrieve an attachment
 * @param docId - Document ID containing the attachment
 * @param attachmentId - Attachment identifier
 * @param options - Optional retrieval parameters
 * @param callback - Optional callback function (err, blob) => void
 * @returns Promise resolving to attachment data
 */
db.getAttachment(docId, attachmentId, options, callback);

Usage Examples:

// Get attachment as Blob (browser)
const blob = await db.getAttachment('user_001', 'profile-photo.jpg');
const imageUrl = URL.createObjectURL(blob);
document.getElementById('profileImage').src = imageUrl;

// Get attachment as Buffer (Node.js)
const buffer = await db.getAttachment('document_001', 'image.png');
const fs = require('fs');
fs.writeFileSync('./downloaded-image.png', buffer);

// Get specific revision of attachment
const blob = await db.getAttachment('user_001', 'notes.txt', {
  rev: '2-abc123def456'
});

// Get attachment as binary string
const binaryString = await db.getAttachment('user_001', 'data.bin', {
  binary: true
});

Removing Attachments

db.removeAttachment()

Removes an attachment from a document.

/**
 * Remove an attachment from a document
 * @param docId - Document ID containing the attachment
 * @param attachmentId - Attachment identifier to remove
 * @param rev - Current document revision
 * @param callback - Optional callback function (err, result) => void
 * @returns Promise resolving to removal operation result
 */
db.removeAttachment(docId, attachmentId, rev, callback);

Usage Examples:

// Remove an attachment
const doc = await db.get('user_001');
const result = await db.removeAttachment(
  'user_001',
  'old-photo.jpg',
  doc._rev
);

console.log('Attachment removed:', result.ok);

// Remove with error handling
try {
  const doc = await db.get('user_001');
  await db.removeAttachment('user_001', 'notes.txt', doc._rev);
  console.log('Attachment removed successfully');
} catch (err) {
  console.error('Failed to remove attachment:', err);
}

Attachment Metadata

Retrieving Attachment Information

// Get document with attachment metadata
const doc = await db.get('user_001', {
  attachments: true,
  binary: false
});

console.log(doc._attachments);
// Output example:
// {
//   "profile-photo.jpg": {
//     "content_type": "image/jpeg",
//     "revpos": 2,
//     "digest": "md5-abc123def456",
//     "length": 45123,
//     "stub": true
//   }
// }

// Get document with attachment data included
const docWithAttachments = await db.get('user_001', {
  attachments: true,
  binary: true
});

// Attachment data will be included as base64 strings
console.log(docWithAttachments._attachments['profile-photo.jpg'].data);

Attachment Metadata Structure

interface AttachmentMetadata {
  /** MIME type of the attachment */
  content_type: string;
  
  /** Revision position when attachment was added */
  revpos: number;
  
  /** MD5 digest of the attachment content */
  digest: string;
  
  /** Size of the attachment in bytes */
  length: number;
  
  /** Indicates if attachment data is included */
  stub: boolean;
  
  /** Base64 encoded attachment data (when stub is false) */
  data?: string;
}

Configuration Options

GetAttachment Options

interface GetAttachmentOptions {
  /** Specific document revision to retrieve attachment from */
  rev?: string;
  
  /** Return attachment as binary data */
  binary?: boolean;
  
  /** Additional retrieval options */
  [key: string]: any;
}

Advanced Usage Examples

Bulk Attachment Operations

// Upload multiple attachments to a document
async function uploadMultipleAttachments(docId, attachments) {
  let doc = await db.get(docId);
  
  for (const attachment of attachments) {
    try {
      const result = await db.putAttachment(
        docId,
        attachment.id,
        doc._rev,
        attachment.data,
        attachment.type
      );
      
      // Update revision for next attachment
      doc._rev = result.rev;
      console.log(`Uploaded attachment: ${attachment.id}`);
    } catch (err) {
      console.error(`Failed to upload ${attachment.id}:`, err);
    }
  }
  
  return doc;
}

// Usage
const attachments = [
  { id: 'photo1.jpg', data: imageBlob1, type: 'image/jpeg' },
  { id: 'photo2.jpg', data: imageBlob2, type: 'image/jpeg' },
  { id: 'notes.txt', data: textBlob, type: 'text/plain' }
];

await uploadMultipleAttachments('user_001', attachments);

Attachment Synchronization

// Download all attachments from a document
async function downloadAllAttachments(docId, downloadPath) {
  const doc = await db.get(docId, { attachments: false });
  
  if (!doc._attachments) {
    console.log('No attachments found');
    return;
  }
  
  const attachmentIds = Object.keys(doc._attachments);
  const downloads = [];
  
  for (const attachmentId of attachmentIds) {
    try {
      const blob = await db.getAttachment(docId, attachmentId);
      downloads.push({
        id: attachmentId,
        data: blob,
        metadata: doc._attachments[attachmentId]
      });
    } catch (err) {
      console.error(`Failed to download ${attachmentId}:`, err);
    }
  }
  
  return downloads;
}

// Sync attachments between databases
async function syncAttachments(sourceDB, targetDB, docId) {
  // Get source document with attachment metadata
  const sourceDoc = await sourceDB.get(docId, { attachments: false });
  
  if (!sourceDoc._attachments) {
    return;
  }
  
  // Get or create target document
  let targetDoc;
  try {
    targetDoc = await targetDB.get(docId);
  } catch (err) {
    if (err.status === 404) {
      // Create document without attachments first
      const { _attachments, ...docWithoutAttachments } = sourceDoc;
      targetDoc = await targetDB.put(docWithoutAttachments);
    } else {
      throw err;
    }
  }
  
  // Sync each attachment
  for (const attachmentId of Object.keys(sourceDoc._attachments)) {
    try {
      const attachmentData = await sourceDB.getAttachment(docId, attachmentId);
      const metadata = sourceDoc._attachments[attachmentId];
      
      await targetDB.putAttachment(
        docId,
        attachmentId,
        targetDoc._rev,
        attachmentData,
        metadata.content_type
      );
      
      // Update target document revision
      targetDoc = await targetDB.get(docId);
    } catch (err) {
      console.error(`Failed to sync attachment ${attachmentId}:`, err);
    }
  }
}

File Upload with Progress Tracking

// Upload file with progress tracking (browser)
async function uploadFileWithProgress(docId, file, onProgress) {
  const chunkSize = 64 * 1024; // 64KB chunks
  const totalChunks = Math.ceil(file.size / chunkSize);
  let uploadedChunks = 0;
  
  // Read file in chunks and upload
  const reader = new FileReader();
  const chunks = [];
  
  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    const chunkData = await new Promise((resolve) => {
      reader.onload = (e) => resolve(e.target.result);
      reader.readAsArrayBuffer(chunk);
    });
    
    chunks.push(new Uint8Array(chunkData));
    uploadedChunks++;
    
    // Report progress
    if (onProgress) {
      onProgress({
        loaded: uploadedChunks * chunkSize,
        total: file.size,
        percentage: (uploadedChunks / totalChunks) * 100
      });
    }
  }
  
  // Combine chunks and upload
  const combinedData = new Uint8Array(file.size);
  let offset = 0;
  
  for (const chunk of chunks) {
    combinedData.set(chunk, offset);
    offset += chunk.length;
  }
  
  const blob = new Blob([combinedData], { type: file.type });
  const doc = await db.get(docId);
  
  return await db.putAttachment(
    docId,
    file.name,
    doc._rev,
    blob,
    file.type
  );
}

// Usage
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];

await uploadFileWithProgress('user_001', file, (progress) => {
  console.log(`Upload progress: ${progress.percentage.toFixed(2)}%`);
});

Attachment Caching

// Attachment caching system
class AttachmentCache {
  constructor(maxSize = 50 * 1024 * 1024) { // 50MB default
    this.cache = new Map();
    this.maxSize = maxSize;
    this.currentSize = 0;
  }
  
  async getAttachment(db, docId, attachmentId) {
    const cacheKey = `${docId}/${attachmentId}`;
    
    // Check cache first
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);
      // Move to end (LRU)
      this.cache.delete(cacheKey);
      this.cache.set(cacheKey, cached);
      return cached.data;
    }
    
    // Fetch from database
    const data = await db.getAttachment(docId, attachmentId);
    const size = this._getDataSize(data);
    
    // Add to cache if there's room
    if (size <= this.maxSize) {
      this._ensureSpace(size);
      this.cache.set(cacheKey, { data, size });
      this.currentSize += size;
    }
    
    return data;
  }
  
  _getDataSize(data) {
    if (data instanceof Blob) {
      return data.size;
    } else if (data instanceof ArrayBuffer) {
      return data.byteLength;
    } else if (Buffer.isBuffer(data)) {
      return data.length;
    }
    return 0;
  }
  
  _ensureSpace(neededSize) {
    while (this.currentSize + neededSize > this.maxSize && this.cache.size > 0) {
      const firstKey = this.cache.keys().next().value;
      const removed = this.cache.get(firstKey);
      this.cache.delete(firstKey);
      this.currentSize -= removed.size;
    }
  }
  
  clear() {
    this.cache.clear();
    this.currentSize = 0;
  }
}

// Usage
const cache = new AttachmentCache();

// Get cached attachment
const imageData = await cache.getAttachment(db, 'user_001', 'profile-photo.jpg');

Attachment Validation

// Validate attachments before upload
class AttachmentValidator {
  constructor(options = {}) {
    this.maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB
    this.allowedTypes = options.allowedTypes || [
      'image/jpeg',
      'image/png',
      'image/gif',
      'text/plain',
      'application/pdf'
    ];
    this.maxFilenameLength = options.maxFilenameLength || 255;
  }
  
  validate(attachmentId, data, contentType) {
    const errors = [];
    
    // Validate filename
    if (!attachmentId || attachmentId.length === 0) {
      errors.push('Attachment ID is required');
    }
    
    if (attachmentId.length > this.maxFilenameLength) {
      errors.push(`Filename too long (max ${this.maxFilenameLength} characters)`);
    }
    
    // Validate content type
    if (!this.allowedTypes.includes(contentType)) {
      errors.push(`Content type ${contentType} not allowed`);
    }
    
    // Validate size
    const size = this._getDataSize(data);
    if (size > this.maxSize) {
      errors.push(`File too large (max ${this.maxSize} bytes)`);
    }
    
    if (size === 0) {
      errors.push('File is empty');
    }
    
    return {
      valid: errors.length === 0,
      errors
    };
  }
  
  _getDataSize(data) {
    if (data instanceof Blob) return data.size;
    if (data instanceof ArrayBuffer) return data.byteLength;
    if (Buffer.isBuffer(data)) return data.length;
    if (typeof data === 'string') return data.length;
    return 0;
  }
}

// Usage
const validator = new AttachmentValidator({
  maxSize: 5 * 1024 * 1024, // 5MB
  allowedTypes: ['image/jpeg', 'image/png']
});

async function uploadWithValidation(docId, attachmentId, data, contentType) {
  const validation = validator.validate(attachmentId, data, contentType);
  
  if (!validation.valid) {
    throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
  }
  
  const doc = await db.get(docId);
  return await db.putAttachment(docId, attachmentId, doc._rev, data, contentType);
}

Performance Considerations

Optimizing Attachment Performance

// Efficient attachment handling for large files
async function handleLargeAttachment(docId, attachmentId, file) {
  // Check if attachment already exists
  try {
    const doc = await db.get(docId, { attachments: false });
    const existingAttachment = doc._attachments?.[attachmentId];
    
    if (existingAttachment) {
      // Calculate file hash to check if upload is necessary
      const fileHash = await calculateFileHash(file);
      if (existingAttachment.digest === `md5-${fileHash}`) {
        console.log('Attachment unchanged, skipping upload');
        return { ok: true, unchanged: true };
      }
    }
  } catch (err) {
    // Document doesn't exist, continue with upload
  }
  
  // Upload with compression for text files
  let dataToUpload = file;
  if (file.type.startsWith('text/')) {
    dataToUpload = await compressData(file);
  }
  
  const doc = await db.get(docId).catch(() => ({ _id: docId }));
  return await db.putAttachment(
    docId,
    attachmentId,
    doc._rev,
    dataToUpload,
    file.type
  );
}

// Memory-efficient attachment streaming (Node.js)
const stream = require('stream');

function createAttachmentStream(db, docId, attachmentId) {
  return new stream.Readable({
    async read() {
      try {
        const data = await db.getAttachment(docId, attachmentId);
        this.push(data);
        this.push(null); // End of stream
      } catch (err) {
        this.emit('error', err);
      }
    }
  });
}