20025年5月のアップデートでNotionのファイルアップロードAPIが解禁されたので、こちらの写真をページにアップロードしてみたいと思います。
まじで待望でし
ローカルのこんなファイルをNotion上にアップロードします。
こういう状態へ。
@notionhq/client v3.1系〜を使ってアップロード
$ npm i @notionhq/client
@notionqh/clinetはv3.1系を利用してますが、このバージョンからファイルアップロードに対応した模様です。
{
"name": "tldv-upload-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@notionhq/client": "^3.1.3"
}
}
公式
アップロードするコードサンプル
Gensparkに書いてもらいました。
Node.js v24系で試してるので.tsファイルにしてみてます。
アップロードはnotion.fileUploads.create()
とnotion.fileUploads.send()
を使う模様です。
createでアップロードオプジェクトを作り、そこにsendでアップロードするといった雰囲気。
// app.ts
import { Client } from '@notionhq/client';
import { openAsBlob } from 'node:fs';
import { basename } from 'node:path';
import { stat, access } from 'node:fs/promises';
const NOTION_TOKEN = `Notionのトークン`;
if (!NOTION_TOKEN) {
console.error('❌ NOTION_API_TOKEN環境変数が設定されていません');
process.exit(1);
}
class NotionFileUploader {
private notion: Client;
constructor(token: string) {
this.notion = new Client({ auth: token });
}
/**
* ページIDを正規化
*/
private normalizePageId(pageIdOrUrl: string): string {
// 既にUUID形式の場合
if (pageIdOrUrl.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/)) {
return pageIdOrUrl;
}
// 32文字のIDをUUIDフォーマットに変換
if (pageIdOrUrl.match(/^[a-f0-9]{32}$/)) {
return `${pageIdOrUrl.slice(0, 8)}-${pageIdOrUrl.slice(8, 12)}-${pageIdOrUrl.slice(12, 16)}-${pageIdOrUrl.slice(16, 20)}-${pageIdOrUrl.slice(20, 32)}`;
}
// URLから抽出
const match = pageIdOrUrl.match(/([a-f0-9]{32})/);
if (!match) {
throw new Error(`無効なページID/URL: ${pageIdOrUrl}`);
}
const rawId = match[1];
return `${rawId.slice(0, 8)}-${rawId.slice(8, 12)}-${rawId.slice(12, 16)}-${rawId.slice(16, 20)}-${rawId.slice(20, 32)}`;
}
/**
* ファイルのMIMEタイプを推測
*/
private getMimeType(filename: string): string {
const ext = filename.toLowerCase().split('.').pop();
const types: Record<string, string> = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml',
'pdf': 'application/pdf',
'txt': 'text/plain',
'mp4': 'video/mp4',
'mov': 'video/quicktime',
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
};
return types[ext || ''] || 'application/octet-stream';
}
/**
* ファイルアップロードオブジェクトを作成
*/
private async createFileUpload() {
return await this.notion.fileUploads.create({
mode: "single_part",
});
}
/**
* ファイルをアップロード
*/
private async sendFileUpload(fileUploadId: string, filePath: string) {
const filename = basename(filePath);
const mimeType = this.getMimeType(filename);
return await this.notion.fileUploads.send({
file_upload_id: fileUploadId,
file: {
filename: filename,
data: new Blob([await openAsBlob(filePath)], {
type: mimeType,
}),
},
});
}
/**
* ファイルをアップロードしてページに添付
*/
async uploadAndAttach(
filePath: string,
pageIdOrUrl: string,
attachType: 'cover' | 'icon' | 'image' | 'file' = 'image'
) {
try {
// ファイル存在チェック
try {
await access(filePath);
} catch {
throw new Error(`ファイルが見つかりません: ${filePath}`);
}
// ファイル情報取得
const filename = basename(filePath);
const fileSize = (await stat(filePath)).size;
console.log(`📤 ファイルアップロード開始: ${filename}`);
console.log(`📊 サイズ: ${(fileSize / 1024 / 1024).toFixed(2)}MB`);
// ページID正規化
const pageId = this.normalizePageId(pageIdOrUrl);
console.log(`🔍 ページID: ${pageId}`);
// 1. ファイルアップロードオブジェクトを作成
console.log('🔄 ファイルアップロードオブジェクト作成中...');
let fileUpload = await this.createFileUpload();
console.log(`✅ ファイルアップロードID: ${fileUpload.id}`);
// 2. ファイルをアップロード
console.log('🔄 ファイルデータアップロード中...');
fileUpload = await this.sendFileUpload(fileUpload.id, filePath);
console.log(`✅ ファイルアップロード完了! ステータス: ${fileUpload.status}`);
// 3. ページに添付
console.log(`🔄 ページに${attachType}として添付中...`);
switch (attachType) {
case 'cover':
await this.notion.pages.update({
page_id: pageId,
cover: {
type: 'file_upload',
file_upload: { id: fileUpload.id }
}
});
console.log('✅ ページカバーに設定完了');
break;
case 'icon':
await this.notion.pages.update({
page_id: pageId,
icon: {
type: 'file_upload',
file_upload: { id: fileUpload.id }
}
});
console.log('✅ ページアイコンに設定完了');
break;
case 'image':
await this.notion.blocks.children.append({
block_id: pageId,
children: [{
type: 'image',
image: {
type: 'file_upload',
file_upload: { id: fileUpload.id }
}
}]
});
console.log('✅ 画像ブロック追加完了');
break;
case 'file':
await this.notion.blocks.children.append({
block_id: pageId,
children: [{
type: 'file',
file: {
type: 'file_upload',
file_upload: { id: fileUpload.id }
}
}]
});
console.log('✅ ファイルブロック追加完了');
break;
}
console.log('\n🎉 処理完了!');
return fileUpload;
} catch (error) {
console.error('❌ エラー:', error);
throw error;
}
}
/**
* 複数ファイルを一度にアップロード
*/
async uploadMultipleFiles(
filePaths: string[],
pageIdOrUrl: string,
attachType: 'image' | 'file' = 'image'
) {
console.log(`📁 ${filePaths.length}個のファイルをアップロード開始`);
const results = [];
for (const [index, filePath] of filePaths.entries()) {
console.log(`\n📄 ファイル ${index + 1}/${filePaths.length}`);
try {
const result = await this.uploadAndAttach(filePath, pageIdOrUrl, attachType);
results.push({ success: true, filePath, result });
} catch (error) {
console.error(`❌ ${filePath} のアップロードに失敗:`, error);
results.push({ success: false, filePath, error });
}
}
const successful = results.filter(r => r.success).length;
console.log(`\n🎉 ${successful}/${filePaths.length} ファイルのアップロード完了`);
return results;
}
}
/**
* メイン実行関数
*/
async function main() {
try {
const [,, filePath, pageIdOrUrl, attachType] = process.argv;
if (!filePath || !pageIdOrUrl) {
console.log('使用方法:');
console.log('node --experimental-strip-types app.ts <ファイルパス> <ページIDまたはURL> [cover|icon|image|file]');
console.log('');
console.log('例:');
console.log('node app.ts ./image.png xxxxxxxxxxxx image');
process.exit(1);
}
const uploader = new NotionFileUploader(NOTION_TOKEN);
// ワイルドカード対応(複数ファイル)
if (filePath.includes('*')) {
const { glob } = await import('glob');
const files = await glob(filePath);
if (files.length === 0) {
console.error('❌ 該当するファイルが見つかりません');
process.exit(1);
}
await uploader.uploadMultipleFiles(
files,
pageIdOrUrl,
attachType as 'image' | 'file' || 'image'
);
} else {
// 単一ファイル
await uploader.uploadAndAttach(
filePath,
pageIdOrUrl,
attachType as 'cover' | 'icon' | 'image' | 'file' || 'image'
);
}
} catch (error) {
console.error('\n💥 実行エラー:', error);
process.exit(1);
}
}
await main();
使い方
アップロードしたいページのURLからページIDを取得します。
https://www.notion.so/hogehoge/<この辺の文字列がページID>?source=copy_link
こんな雰囲気で使います。
$ node app.ts <アップロードするファイルパス> <アップロード先のページID>
使うとこんな感じ
$ node app.ts ./image2.png HOGEHOGE-xxxxxxxxxxxxxxxxxxxxxxxxx
📊 サイズ: 1.84MB
🔍 ページID: HOGEHOGE-xxxxxxxxxxxxxxxxxxxxxxxxx
🔄 ファイルアップロードオブジェクト作成中...
✅ ファイルアップロードID: HOGEHOGE-xxxxxxxxxxxxxxxxxxxxxxxxx
🔄 ファイルデータアップロード中...
✅ ファイルアップロード完了! ステータス: uploaded
🔄 ページにimageとして添付中...
✅ 画像ブロック追加完了
所感
Notionoファイルアップロード機能はまだ実装例が少ないので何回かコケました。
NotionにAPI経由でアップロードできるとやれることが広がるので色々試したいですね。