code icon Code

Upload File to Notion

Upload local files to Notion using Notion's three-step file upload API

Source Code

import fs from 'fs';
import path from 'path';
import mime from 'mime-types';

// Constants
const MULTIPART_THRESHOLD = 20 * 1024 * 1024; // 20 MB
const PART_SIZE = 10 * 1024 * 1024; // 10 MB per part

/**
 * Upload a local file to Notion using Notion's three-step upload process
 * @param {string} filePath - Local path to the file to upload
 * @param {string} notionToken - Notion API token
 * @returns {Promise<Object>} Upload result with file URL and metadata
 */
async function uploadFileToNotion(filePath, notionToken) {

  // Read the file
  const fileBuffer = fs.readFileSync(filePath);
  const fileName = path.basename(filePath);
  const fileSize = fileBuffer.length;

  // Detect content type
  const contentType = mime.lookup(filePath) || 'application/octet-stream';

  // Automatically determine upload mode based on file size
  const useMultipart = fileSize > MULTIPART_THRESHOLD;

  // Step 1: Create a file upload
  const createUploadBody = {
    name: fileName,
    content_type: contentType
  };

  if (useMultipart) {
    // Calculate number of parts for multipart upload
    const numberOfParts = Math.ceil(fileSize / PART_SIZE);

    createUploadBody.mode = 'multi_part';
    createUploadBody.number_of_parts = numberOfParts;

    console.log(`[DEBUG] File size ${fileSize} bytes exceeds ${MULTIPART_THRESHOLD} bytes, using multipart upload with ${numberOfParts} parts`);
  } else {
    // Use single part upload for smaller files
    createUploadBody.mode = 'single_part';

    console.log(`[DEBUG] File size ${fileSize} bytes, using single-part upload`);
  }

  const createUploadResponse = await fetch('https://api.notion.com/v1/file_uploads', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${notionToken}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2022-06-28'
    },
    body: JSON.stringify(createUploadBody)
  });

  const createUploadData = await createUploadResponse.json();

  if (!createUploadResponse.ok) {
    throw new Error(`Failed to create file upload: ${createUploadData.message || createUploadData.error}`);
  }

  const { id: fileUploadId } = createUploadData;

  // Step 2: Send the file contents
  if (useMultipart) {
    // Send file in multiple parts
    const numberOfParts = Math.ceil(fileSize / PART_SIZE);

    for (let partNumber = 1; partNumber <= numberOfParts; partNumber++) {
      const start = (partNumber - 1) * PART_SIZE;
      const end = Math.min(start + PART_SIZE, fileSize);
      const partBuffer = fileBuffer.subarray(start, end);

      const formData = new FormData();
      formData.append('file', new Blob([partBuffer], { type: contentType }), fileName);
      formData.append('part_number', partNumber.toString());

      console.log(`[DEBUG] Uploading part ${partNumber}/${numberOfParts} (${partBuffer.length} bytes)`);

      const sendUploadResponse = await fetch(
        `https://api.notion.com/v1/file_uploads/${fileUploadId}/send`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${notionToken}`,
            'Notion-Version': '2022-06-28'
          },
          body: formData
        }
      );

      if (!sendUploadResponse.ok) {
        const sendUploadError = await sendUploadResponse.text();
        throw new Error(`Failed to send file part ${partNumber}: ${sendUploadError}`);
      }
    }
  } else {
    // Send file in single part
    const formData = new FormData();
    formData.append('file', new Blob([fileBuffer], { type: contentType }), fileName);

    const sendUploadResponse = await fetch(
      `https://api.notion.com/v1/file_uploads/${fileUploadId}/send`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${notionToken}`,
          'Notion-Version': '2022-06-28'
        },
        body: formData
      }
    );

    if (!sendUploadResponse.ok) {
      const sendUploadError = await sendUploadResponse.text();
      throw new Error(`Failed to send file contents: ${sendUploadError}`);
    }
  }

  // Step 3: Complete the file upload
  const completeUploadResponse = await fetch(
    `https://api.notion.com/v1/file_uploads/${fileUploadId}/complete`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${notionToken}`,
        'Content-Type': 'application/json',
        'Notion-Version': '2022-06-28'
      }
    }
  );

  const completeUploadData = await completeUploadResponse.json();

  if (!completeUploadResponse.ok) {
    throw new Error(`Failed to complete file upload: ${completeUploadData.message || completeUploadData.error}`);
  }

  const fileUrl = completeUploadData.file.url;
  const expiryTime = completeUploadData.file.expiry_time;

  console.log(`[DEBUG] Successfully uploaded file to Notion: ${fileName}`);
  console.log(`[DEBUG] File Upload ID: ${fileUploadId}`);
  console.log(`[DEBUG] File URL: ${fileUrl}`);
  console.log(`[DEBUG] Expires at: ${expiryTime}`);

  return {
    success: true,
    fileUploadId: fileUploadId,
    fileName: fileName,
    url: fileUrl,
    expiryTime: expiryTime,
    contentType: contentType,
    fileSize: fileSize
  };
}


export { uploadFileToNotion };

/**
 * Run this script directly from the command line:
 * bun upload_file.js <filePath> <notionToken>
 *
 * The script automatically determines whether to use single-part or multi-part upload
 * based on file size (files >20MB use multi-part upload automatically).
 *
 * Returns the Notion-hosted file URL which you can then use in API calls to add to pages/databases.
 *
 * Examples:
 * # Upload local file
 * bun upload_file.js ./document.pdf token123
 *
 * # Upload image
 * bun upload_file.js ./photo.jpg token123
 */
if (import.meta.main) {
  const args = process.argv.slice(2);
  if (args.length < 2) {
    console.error('Usage: bun upload_file.js <filePath> <notionToken>');
    console.error('');
    console.error('The script automatically uses multi-part upload for files >20MB.');
    console.error('Returns a Notion-hosted URL that you can use in API calls.');
    console.error('');
    console.error('Examples:');
    console.error('  # Upload local file');
    console.error('  bun upload_file.js ./doc.pdf token123');
    console.error('');
    console.error('  # Upload image');
    console.error('  bun upload_file.js ./photo.jpg token123');
    process.exit(1);
  }

  const filePath = args[0];
  const notionToken = args[1];

  uploadFileToNotion(filePath, notionToken)
    .then(result => {
      console.log('\nāœ“ Upload successful!');
      console.log(`File Upload ID: ${result.fileUploadId}`);
      console.log(`File Name: ${result.fileName}`);
      console.log(`File Size: ${result.fileSize} bytes`);
      console.log(`Content Type: ${result.contentType}`);
      console.log(`URL: ${result.url}`);
      console.log(`Expires: ${result.expiryTime}`);
      console.log('\nUse the File Upload ID in Notion API calls to add the file to pages or databases.');
    })
    .catch(error => {
      console.error('Upload failed:', error.message);
      process.exit(1);
    });
}