1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Node.jsでNotion API待望のFileUploadを試す

Last updated at Posted at 2025-06-05

20025年5月のアップデートでNotionのファイルアップロードAPIが解禁されたので、こちらの写真をページにアップロードしてみたいと思います。

まじで待望でし

ローカルのこんなファイルをNotion上にアップロードします。

CleanShot 2025-06-05 at 18.16.41.png

こういう状態へ。

CleanShot 2025-06-05 at 18.07.59.png

@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経由でアップロードできるとやれることが広がるので色々試したいですね。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?