【Notion API】ファイル直接アップロード機能でAWS環境が不要に
はじめに
以前、「GitHub と Notion の同期システム実装ガイド」という記事で、Notion API では画像を直接アップロードできないため AWS S3 を経由する方法を紹介しました。
しかし、2025年9月にリリースされた Notion API v5 により、Notion API で直接ファイルアップロードが可能になりました。これにより AWS 環境の構築が不要となり、Notion 連携がより簡単に実装できるようになりました。
本記事では、Notion API v5.0.0 を使った新しいファイルアップロード機能の実装方法を紹介します。
重要: 5MB制限は「1ファイルあたり」の制限です。無料プランでも、各ファイルが5MB以下であれば複数の画像をアップロード可能です。ただし、大きな画像(5MB超)や20MB超のマルチパートアップロードには有料プラン(5GBまで対応)が必要です。
何が変わったのか?
旧バージョン(〜v4.x)の課題
- Notion APIでは画像やPDFなどのファイルを直接アップロードできない
- GitHubマークダウンから画像付きページを同期する場合、別途AWS S3などの外部ストレージが必要
- AWS S3、CloudFront、Lambda@Edgeなどのインフラ構築が必要
- インフラ構築コストと運用コストが発生(月額数千円〜)
新バージョン(v5.0.0〜)の改善点
- Notion API で直接ファイルアップロードが可能に
- AWS 環境が不要(月額数千円のインフラコスト削減)
- 小さなファイル(20MB以下)の直接アップロード対応
- 大きなファイル(最大5GB)のマルチパートアップロード対応(有料プランのみ)
- シンプルな実装で管理が容易
制限の詳細:
- 無料プラン: 1ファイルあたり5MBまで(複数ファイルOK)
- 有料プラン: 1ファイルあたり5GBまで、20MB超のマルチパート対応
- 一般的なスクリーンショット(1〜3MB)であれば、無料プランでも十分実用的
Notion API v5.0.0のファイルアップロードの仕組み
Notion API v5.0.0では、ファイルアップロードに専用のエンドポイントが追加されました。
公式ドキュメント:
基本的なフロー
1. 小さなファイル(20MB以下)の場合
1. File Upload Object作成
POST /v1/file_uploads
2. ファイルコンテンツのアップロード
POST /v1/file_uploads/{upload_id}/send
3. アップロード完了通知(pendingの場合のみ)
POST /v1/file_uploads/{upload_id}/complete
2. 大きなファイル(20MB〜5GB)の場合(有料プランのみ)
1. Multipart Upload開始
POST /v1/file_uploads
body: { mode: 'multi_part', number_of_parts: N, ... }
2. パートのアップロード(複数回、FormData形式)
POST /v1/file_uploads/{upload_id}/send
body: FormData with part_number and file
3. アップロード完了
POST /v1/file_uploads/{upload_id}/complete
body: {} # 空のボディ
実装例
環境設定
const { Client } = require('@notionhq/client');
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const FormData = require('form-data');
const NOTION_API_VERSION = '2025-09-03'; // v5.0.0対応バージョン
const notion = new Client({
auth: process.env.NOTION_API_KEY,
notionVersion: NOTION_API_VERSION,
});
// 設定定数
const MAX_FILE_SIZE_FREE = 5 * 1024 * 1024; // 5MB(無料プラン)
const MAX_FILE_SIZE_PAID = 5 * 1024 * 1024 * 1024; // 5GB(有料プラン)
const DIRECT_UPLOAD_LIMIT = 20 * 1024 * 1024; // 20MB(直接アップロードの上限)
小さなファイルの直接アップロード
/**
* 小さなファイル(20MB以下)を直接アップロード
*/
async function uploadSmallFileToNotion(localFilePath, fileName, contentType) {
// Step 1: File Upload Objectを作成
const createResponse = await fetch('https://api.notion.com/v1/file_uploads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': NOTION_API_VERSION,
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename: fileName,
content_type: contentType
})
});
const uploadInfo = await createResponse.json();
const fileUploadId = uploadInfo.id;
const uploadUrl = uploadInfo.upload_url;
// Step 2: ファイル内容をアップロード
// ⚠️ 注意: multipart/form-dataを使用。他のNotion APIと異なりJSONではない
const fileBuffer = fs.readFileSync(localFilePath);
const formData = new FormData();
formData.append('file', fileBuffer, {
filename: fileName,
contentType: contentType
});
const uploadResponse = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': NOTION_API_VERSION,
// Content-Typeヘッダーは自動設定される(boundaryを含む)
...formData.getHeaders()
},
body: formData
});
const uploadResult = await uploadResponse.json();
// Step 3: アップロード完了通知(statusがpendingの場合のみ)
// 注: single_partモードでは通常、このステップは不要の場合が多い
if (uploadResult.status === 'pending') {
const completeResponse = await fetch(
`https://api.notion.com/v1/file_uploads/${fileUploadId}/complete`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': NOTION_API_VERSION,
'Content-Type': 'application/json'
},
body: JSON.stringify({})
}
);
return await completeResponse.json();
}
return uploadResult;
}
Content-Typeの判定
/**
* ファイル拡張子からContent-Typeを取得
*/
function getContentTypeFromExtension(extension) {
const contentTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.csv': 'text/csv',
};
return contentTypes[extension.toLowerCase()] || 'text/plain';
}
ファイルサイズによるアップロード方法の切り替え
/**
* ファイルサイズに応じて適切なアップロード方法を選択
*/
async function uploadFileToNotion(localFilePath, fileName, isPaidWorkspace = false) {
const stats = fs.statSync(localFilePath);
const fileSize = stats.size;
const maxSize = isPaidWorkspace ? MAX_FILE_SIZE_PAID : MAX_FILE_SIZE_FREE;
// サイズチェック
if (fileSize > maxSize) {
throw new Error(`ファイルサイズが制限を超えています: ${fileSize} bytes (制限: ${maxSize} bytes)`);
}
const extension = path.extname(localFilePath);
const contentType = getContentTypeFromExtension(extension);
// 20MB以下なら直接アップロード、それ以上ならマルチパートアップロード
if (fileSize <= DIRECT_UPLOAD_LIMIT) {
return await uploadSmallFileToNotion(localFilePath, fileName, contentType);
} else {
return await uploadLargeFileToNotion(localFilePath, fileName, contentType);
}
}
大きなファイルのマルチパートアップロード
⚠️ 有料プランのみ対応(無料プランは5MB制限のため使用不可)
/**
* 大きなファイル(20MB〜5GB)をマルチパートでアップロード
* 注意: 有料プランのみ利用可能
*/
async function uploadLargeFileToNotion(localFilePath, fileName, contentType) {
const fileSize = fs.statSync(localFilePath).size;
const chunkSize = 10 * 1024 * 1024; // 10MBずつ
const numberOfParts = Math.ceil(fileSize / chunkSize);
// Step 1: マルチパートアップロード開始(通常のエンドポイントにmodeパラメータを追加)
const initResponse = await fetch('https://api.notion.com/v1/file_uploads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': NOTION_API_VERSION,
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename: fileName,
content_type: contentType,
file_size: fileSize,
mode: 'multi_part', // マルチパートモードを指定
number_of_parts: numberOfParts
})
});
const uploadInfo = await initResponse.json();
const uploadId = uploadInfo.id;
// Step 2: ファイルを分割してアップロード
for (let partNumber = 1; partNumber <= numberOfParts; partNumber++) {
const start = (partNumber - 1) * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
// ファイルの一部を読み込み
const buffer = Buffer.alloc(end - start);
const fd = fs.openSync(localFilePath, 'r');
fs.readSync(fd, buffer, 0, end - start, start);
fs.closeSync(fd);
// FormDataを使ってパートをアップロード
// ⚠️ 重要: mode=multi_partの場合、part_numberフィールドが必須
const formData = new FormData();
formData.append('part_number', partNumber.toString());
formData.append('file', buffer, {
filename: `${fileName}-part-${partNumber}`,
contentType: 'application/octet-stream'
});
const partResponse = await fetch(
`https://api.notion.com/v1/file_uploads/${uploadId}/send`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': NOTION_API_VERSION,
...formData.getHeaders()
},
body: formData
}
);
const partResult = await partResponse.json();
console.log(`パート ${partNumber}/${numberOfParts} アップロード完了`);
}
// Step 3: アップロード完了(空のボディで通知)
// 注: このステップはmode=multi_partの場合のみ必要
const completeResponse = await fetch(
`https://api.notion.com/v1/file_uploads/${uploadId}/complete`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': NOTION_API_VERSION,
'Content-Type': 'application/json'
},
body: JSON.stringify({}) // 空のボディを送信
}
);
return await completeResponse.json();
}
GitHubマークダウンをNotionページに同期する実装例
マークダウンから画像参照を抽出
/**
* Markdownから画像参照を抽出
*/
function extractImageReferences(markdownContent) {
const imageRegex = /!\[.*?\]\((\/images\/.*?\.(png|jpg|jpeg|gif|svg|webp))\)/gi;
const matches = [...markdownContent.matchAll(imageRegex)];
return matches.map(match => match[1]);
}
/**
* Markdownから画像をアップロードしてURLを置換
*/
async function uploadImagesFromMarkdown(markdownContent, repoRoot) {
const imageRefs = extractImageReferences(markdownContent);
let updatedContent = markdownContent;
for (const imageRef of imageRefs) {
const localPath = path.join(repoRoot, imageRef);
if (fs.existsSync(localPath)) {
const fileName = path.basename(imageRef);
// Notionにアップロード
const uploadResult = await uploadFileToNotion(localPath, fileName);
// マークダウン内のパスを置換
updatedContent = updatedContent.replace(imageRef, uploadResult.id);
console.log(`画像アップロード完了: ${fileName} -> ${uploadResult.id}`);
}
}
return updatedContent;
}
パッケージ依存関係
{
"name": "notion-sync",
"version": "0.1.0",
"dependencies": {
"@notionhq/client": "^5.0.0",
"@tryfabric/martian": "^1.2.4",
"form-data": "^4.0.4",
"front-matter": "^4.0.2",
"node-fetch": "^2.7.0"
}
}
GitHub Actionsでの自動同期
name: Sync to Notion
on:
push:
branches:
- main
paths:
- 'docs/**'
- 'images/**'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: |
cd notion-sync
npm install
- name: Sync to Notion
env:
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
NOTION_DATASOURCE_ID: ${{ secrets.NOTION_DATASOURCE_ID }}
FILE_UPLOAD_MODE: notion
NOTION_PAID_WORKSPACE: true
run: |
cd notion-sync
node sync-to-notion.js
ファイルサイズ制限とプラン
| プラン | 1ファイルあたりの最大サイズ | 直接アップロード | マルチパートアップロード | 実用性 |
|---|---|---|---|---|
| 無料プラン | 5MB | 可能(5MB以下) | 不可 | ⚠️ 条件付き |
| 有料プラン | 5GB | 可能(20MB以下) | 可能(20MB〜5GB) | ✅ 推奨 |
実用性の判断基準
無料プランで十分なケース
- スクリーンショット中心(通常1〜3MB/枚)
- 最適化された画像ファイル(各5MB以下)
- ドキュメント数が少ない
有料プランが必要なケース
- 高解像度の画像(5MB超/枚)
- 動画ファイルやPDF(20MB超)
- 大量のファイルを頻繁にアップロード(レート制限回避)
結論: 一般的なドキュメント管理(スクリーンショット中心)であれば、無料プランでも実用可能です。ただし、画像サイズが大きい場合や、大容量ファイルを扱う場合は有料プラン推奨です。
実装時の注意点
1. multipart/form-dataの扱い
NotionのファイルアップロードAPI(Send a file upload)は、他のNotion APIと異なりmultipart/form-dataを使用します。
// NG: JSON で送信(他の Notion API はこれで OK)
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json' // ファイルアップロードではエラー
},
body: JSON.stringify({ file: base64Data })
});
// OK: multipart/form-data で送信
const formData = new FormData();
formData.append('file', fileBuffer, { filename: 'image.png' });
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
...formData.getHeaders() // boundary が自動設定される
},
body: formData
});
重要: Content-Typeヘッダーにboundaryパラメータが必要ですが、ほとんどのライブラリ(fetch、node-fetch、FormData)は自動的に設定します。明示的にContent-Typeを上書きしないことが重要です。
2. リトライ処理の実装
Notion APIは時々タイムアウトやレート制限エラーを返すため、リトライ処理を実装することをお勧めします。
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`リトライ ${i + 1}/${maxRetries}...`);
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}
2. API呼び出しのレート制限
Notion APIには以下のレート制限があります:
- 平均: 3リクエスト/秒
- バースト: 短時間で多数のリクエストを送ると制限される
- マルチパートアップロードの各パートも標準のレート制限に従う
複数ファイルをアップロードする場合は、適切な待機時間を設けましょう。
公式ドキュメント: Request limits
// ファイルアップロード間に待機時間を設ける
await new Promise(resolve => setTimeout(resolve, 400)); // 0.4秒待機
サポートされているContent-Type:
- 画像:
image/jpeg,image/png,image/gif,image/svg+xml,image/webp - ドキュメント:
application/pdf,text/plain,text/csv - Microsoft Office:
application/msword,application/vnd.openxmlformats-officedocument.*
Content-Typeが不明な場合やファイル名に拡張子が含まれている場合、NotionのAPIが自動的に推論します。
3. Content-Typeの正確性
ファイルの拡張子から正しいContent-Typeを設定することが重要です。不正確なContent-Typeを設定すると、アップロードが失敗する場合があります。
4. エラーハンドリング
アップロード失敗時の処理を適切に実装しましょう。
try {
const result = await uploadFileToNotion(filePath, fileName);
console.log('アップロード成功:', result.id);
} catch (error) {
console.error('アップロード失敗:', error.message);
// フォールバック処理やログ記録など
}
まとめ
Notion API v5.0.0により、ついに念願のファイル直接アップロード機能が実装されました!
メリット
- インフラコスト削減: AWS環境(S3、CloudFront、Lambda@Edge)が不要になり、月々数千円のコスト削減
- シンプルな実装: 外部ストレージの構築・管理が不要
- 高速化: 外部ストレージを経由しないため、アップロード処理が高速
- セキュリティ: ファイルがNotion内で完結し、アクセス制御が容易
- 運用負荷軽減: AWSのインフラ監視やメンテナンスが不要
デメリット
- 1ファイルあたりのサイズ制限: 無料プランは5MB/ファイル、有料プランでも5GB/ファイル
- 大容量ファイルは有料プラン必須: 5MB超のファイルや20MB超のマルチパートアップロードには有料プラン(月額$8〜$10/ユーザー)が必要
- レート制限: 平均3リクエスト/秒のため、大量ファイルの一括アップロードには時間がかかる
- ベンダーロックイン: Notion依存度が高まるため、将来的な移行が困難になる可能性
コスト比較
| 項目 | AWS S3連携 | Notion直接アップロード |
|---|---|---|
| 初期構築 | 必要(S3/CloudFront/Lambda@Edge) | 不要 |
| 月額コスト | 数千円〜(通信量次第) | Notion有料プランに含まれる |
| 実装難易度 | 高(AWSインフラ構築必要) | 低(API呼び出しのみ) |
| 運用負荷 | 高(インフラ監視必要) | 低(Notionに委任) |
結論:
- 無料プランでも実用可能(通常のスクリーンショット中心のドキュメント管理)
- 既にNotionの有料プランを利用している場合、追加コストなしで実装できるため最適な選択
- 画像サイズが大きい場合(5MB超)や大容量ファイルを扱う場合は有料プラン推奨
参考リンク
- Notion API v5.0.0 Documentation
- Notion API File Upload Reference
- 前回の記事: GitHub と Notion の同期システム実装ガイド
以上、お疲れ様でした!