メインコンテンツまでスキップ

ブロックプログラミング形式でAmazon S3にアクセスするなにかを作ったよ

· 約10分
moritalous

Scratchに代表されるブロックプログラミング形式でAWSにアクセスするものを作ってみました。

image.png

ブロックプログラミングとは

Bing AIに聞いてみました。

ブロックプログラミングとは、プログラミングの基本的な概念を視覚的に表現したブロックを組み合わせて、コンピュータに命令する方法です。ブロックプログラミングは、テキストでコードを書くよりも簡単で直感的なので、初心者や子どもにもおすすめです。ブロックプログラミングを学ぶには、Microsoft MakeCodeやBlockly Gamesなどのオンラインツールが便利です。

ブロックプログラムを実現するOSSライブラリー "Blockly"

Google製のOSSライブラリー「Blockly」を使うと、独自のブロックプログラミング環境を作成することができます。

Blockly | Google Developers
https://developers.google.com/blockly

使い方を紹介します。

プロジェクトの作成

pnpmとviteを使ってReactプロジェクトを作成します。

  1. pnpmをインストール

    shell
    npm install -g pnpm
  2. Reactプロジェクトを作成

    shell
    pnpm create vite
    ✔ Project name: … blocky-s3-sample
    ✔ Select a framework: › React
    ✔ Select a variant: › TypeScript
  3. Reactに必要なライブラリーのインストール

    shell
    cd blocky-s3-sample
    pnpm install
  4. Blocklyをインストール

    shell
    pnpm install blockly
  5. Reactのstrictモードをオフにする

    Blocklyの動作に影響があるので、strictモードをオフにします。

    main.tsx
      import React from 'react'
    import ReactDOM from 'react-dom/client'
    import App from './App.tsx'
    import './index.css'

    ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    - <React.StrictMode>
    <App />
    - </React.StrictMode>,
    )

ここまでで必要なライブラリーの導入は完了です。

エディターの配置

  1. idにblocklyDivを指定したdivタグを追加します。

    App.tsx
    <div id="blocklyDiv" style={{ height: '50vh', width: '100vw' }}></div>
  2. toolboxを定義

    JSONやXMLでtoolboxを定義できるのですが、いちから作るのは大変なので、Blockly Developer Toolsを使って作成します。

    1. Blockly Developer Toolsにアクセス image.png

    2. Workspace Factoryタブを選択 保存してない旨を確認されますがそのままOKで進みます。

      image.png

    3. 画面真ん中あたりの+をクリックし、Standard Toolboxを選択

      image.png

    4. Exportをクリックし、Toolboxに名前をつけて保存

      image.png

    5. 取得したXMLを文字列として定義します。

      const toolboxXml = `<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox" style="display: none">
      ...
      ...
      ...
      </xml>
      `
      注記

      自動生成されるid` が入っている場合は - など別の文字に置換します。

  3. Blocklyをセット

    App.tsx
    useEffect(() => {
    const myWorkspace = Blockly.inject('blocklyDiv', { toolbox: toolboxXml })
    }, [])

一度起動して画面を確認します。

shell
pnpm run dev

ブロックエディタが表示されます。

image.png

独自のブロックを追加

まずは手始めにconsole.log()を行うブロックを追加してみましょう。

独自ブロックを追加する手順は大きく以下のとおりです。

  1. ブロックを定義
  2. ツールバーにブロックを配置
  3. ブロックの処理を記述

console.log()を行う処理はsrc/block/consoleLog.tsxに作成します。

1. ブロックを定義

先程使用したBlockly Developer Toolsでブロック定義が作成できます。今回はこのようにしました。

consoleLog.tsx
export const define = {
"type": blockType,
"message0": "ログ出力 %1",
"args0": [
{
"type": "input_value",
"name": "message",
"check": "String",
},
],
"previousStatement": null,
"nextStatement": null,
"colour": "#888888",
"tooltip": "",
"helpUrl": ""
}

App.tsxで参照します。

App.tsx
+ import * as consoleLogBlock from './block/consoleLog'

...

useEffect(() => {
+ Blockly.defineBlocksWithJsonArray([
+ consoleLogBlock.define,
+ ])

const myWorkspace = Blockly.inject('blocklyDiv', { toolbox: toolboxXml })
}, [])

2. ツールバーにブロックを配置

toolboxXmlの末尾にツールバーのカテゴリを追加します。

App.tsx
  <category name="Functions" colour="#995ba5" custom="PROCEDURE"></category>
+ <sep></sep>
+ <category name="Debug" custom="debug" colour="#888888"></category>
</xml>

追加したカテゴリにブロックを追加します。

App.tsx
const debugFlyoutCallback = function (workspace: Blockly.WorkspaceSvg) {
const blockList = [
{
"kind": "block",
"type": consoleLogBlock.blockType
},
];
return blockList;
};
myWorkspace.registerToolboxCategoryCallback('debug', debugFlyoutCallback);
注記

registerToolboxCategoryCallbackの第一引数が、XMLで定義したcategorycustom属性と対応します

3. ブロックの処理を記述

consoleLog.tsx
export const javascriptCode = function (block: Blockly.Block) {

const value_message = javascriptGenerator.valueToCode(block, 'message', javascriptGenerator.ORDER_ATOMIC);

const code = `console.log(${value_message})
`

return code
}

valueToCodeはブロックの引数を取得しています。codeが実行されるコードです。

App.tsxで参照します。

App.tsx
+ import { javascriptGenerator } from 'blockly/javascript';

useEffect(() => {
Blockly.defineBlocksWithJsonArray([
consoleLogBlock.define,
])

const myWorkspace = Blockly.inject('blocklyDiv', { toolbox: toolboxXml })

const debugFlyoutCallback = function (workspace: Blockly.WorkspaceSvg) {
const blockList = [
{
"kind": "block",
"type": consoleLogBlock.blockType
},
];
return blockList;
};
myWorkspace.registerToolboxCategoryCallback('debug', debugFlyoutCallback);

+ javascriptGenerator[consoleLogBlock.blockType] = consoleLogBlock.javascriptCode

}, [])

これでDebugカテゴリにログ出力ブロックが追加されます。

image.png

コード生成を行うようにボタンとテキストエリアを追加します。

App.tsx
  function App() {

+ const [workspace, setWorkspace] = useState<Blockly.WorkspaceSvg>()
+ const [generatedCode, setGeneratedCode] = useState('')

useEffect(() => {
...
}, [])

+ const generateCode = (() => {
+ const jsCode = javascriptGenerator.workspaceToCode(workspace)
+
+ setGeneratedCode(jsCode)
+ })

return (
<>
<div id="blocklyDiv"
style={{ height: '600px', width: '800px' }}
></div>
+ <div>
+ <button onClick={generateCode}>generate code</button>
+ <br />
+ <textarea
+ readOnly
+ style={{ height: '40vh', width: '100vw' }}
+ value={generatedCode}
+ ></textarea>
+ </div>
</>
)
}

これでコード生成ができました。

image.png

S3を操作するためのブロックを作成

いよいよS3にアクセスする部分です。

ブロックを3つ作成しました。

  1. Import文を生成するブロック
  2. S3Clientを作成するブロック
  3. listObjectsを実行するブロック

1. Import文を生成するブロック

image.png

  1. ブロックを定義

    import.tsx
    export const define = {
    "type": blockType,
    "message0": "Import宣言",
    "nextStatement": null,
    "colour": "#FF9900",
    "tooltip": "",
    "helpUrl": ""
    }
  2. ツールバーにブロックを配置

    toolboxXmlの末尾にツールバーのカテゴリを追加します。

    App.tsx
      <category name="Functions" colour="#995ba5" custom="PROCEDURE"></category>
    <sep></sep>
    <category name="Debug" custom="debug" colour="#888888"></category>
    + <sep></sep>
    + <category name="AWS" custom="AWS" colour="#FF9900"></category>
    </xml>
    注記

    色はAWSカラーです

    App.tsx
      Blockly.defineBlocksWithJsonArray([
    consoleLogBlock.define,
    + importBlock.define,
    ])
  3. ブロックの処理を記述

    import.tsx
    export const javascriptCode = function (block: Blockly.Block) {

    const code = `import {
    ListObjectsV2Command,
    S3Client,
    } from '@aws-sdk/client-s3'
    `

    return code
    }
    App.tsx
    javascriptGenerator[importBlock.blockType] = importBlock.javascriptCode

2. S3Clientを作成するブロック

image.png

  1. ブロックを定義

    createS3Client.tsx
    export const define = {
    "type": blockType,
    "message0": "S3Client生成: アクセスキー %1 シークレットアクセス %2 リージョン %3",
    "args0": [
    {
    "type": "input_value",
    "name": "accessKeyId",
    "check": "String",
    },
    {
    "type": "input_value",
    "name": "secretAccessKey",
    "check": "String",
    },
    {
    "type": "field_dropdown",
    "name": "region",
    "options": [
    ["アジアパシフィック (東京)", "ap-northeast-1"],
    ["米国東部 (バージニア北部)", "us-east-1"]
    ],
    "check": "String",
    }
    ],
    "output": null,
    "inputsInline": false,
    "colour": "#FF9900",
    "tooltip": "",
    "helpUrl": ""
    }
    注記

    リージョンはドロップダウンで選択できるようにしました

  2. ツールバーにブロックを配置

    App.tsx
      Blockly.defineBlocksWithJsonArray([
    consoleLogBlock.define,
    importBlock.define,
    + createS3ClientBlock.define,
    ])
  3. ブロックの処理を記述

    createS3Client.tsx
    export const javascriptCode = function (block: Blockly.Block) {

    const value_accesskeyid = javascriptGenerator.valueToCode(block, 'accessKeyId', javascriptGenerator.ORDER_ATOMIC);
    const value_secretaccesskey = javascriptGenerator.valueToCode(block, 'secretAccessKey', javascriptGenerator.ORDER_ATOMIC);
    const value_region = block.getFieldValue('region');

    const code = `new S3Client({
    credentials: {
    accessKeyId: ${value_accesskeyid},
    secretAccessKey: ${value_secretaccesskey},
    },
    region: '${value_region}',
    })
    `

    return [code, javascriptGenerator.ORDER_ATOMIC]

    }

3. listObjectsを実行するブロック

image.png

  1. ブロックを定義

    listObjects.tsx
    export const define = {
    "type": blockType,
    "message0": "オブジェクト一覧取得: S3Client %1 バケット名 %2",
    "args0": [
    {
    "type": "input_value",
    "name": "client"
    },
    {
    "type": "input_value",
    "name": "bucket",
    "check": "String"
    },
    ],
    "output": "Array",
    "colour": "#FF9900",
    "tooltip": "",
    "helpUrl": ""
    }
  2. ツールバーにブロックを配置

    App.tsx
      Blockly.defineBlocksWithJsonArray([
    consoleLogBlock.define,
    importBlock.define,
    createS3ClientBlock.define,
    + listObjectsBlock.define
    ])
  3. ブロックの処理を記述

    listObjects.tsx
    export const javascriptCode = function (block: Blockly.Block) {

    const value_client = javascriptGenerator.valueToCode(block, 'client', javascriptGenerator.ORDER_ATOMIC);
    const value_bucket = javascriptGenerator.valueToCode(block, 'bucket', javascriptGenerator.ORDER_ATOMIC);

    const code = `await (async (client, bucket) => {

    const command = new ListObjectsV2Command({
    Bucket: bucket,
    });

    var keys = []

    try {
    let isTruncated = true;

    while (isTruncated) {
    const { Contents, IsTruncated, NextContinuationToken } = await client.send(command);

    Contents?.forEach((content) => {
    if (content.Key) keys.push(content.Key)
    })

    isTruncated = IsTruncated !== undefined ? IsTruncated : false;

    command.input.ContinuationToken = NextContinuationToken;
    }

    } catch (err) {
    console.error(err);
    }

    return keys
    })(${value_client}, ${value_bucket})
    `

    return [code, javascriptGenerator.ORDER_ATOMIC]
    }
    注記

    無名関数を作ってawaitして返します

    App.tsx
    javascriptGenerator[listObjectsBlock.blockType] = listObjectsBlock.javascriptCode

完成

image.png

生成されるコードはこちら

var s3Client, objectList, j;


import {
ListObjectsV2Command,
S3Client,
} from '@aws-sdk/client-s3'
s3Client = new S3Client({
credentials: {
accessKeyId: 'key',
secretAccessKey: 'secret',
},
region: 'ap-northeast-1',
})
;
objectList = await (async (client, bucket) => {

const command = new ListObjectsV2Command({
Bucket: bucket,
});

var keys = []

try {
let isTruncated = true;

while (isTruncated) {
const { Contents, IsTruncated, NextContinuationToken } = await client.send(command);

Contents?.forEach((content) => {
if (content.Key) keys.push(content.Key)
})

isTruncated = IsTruncated !== undefined ? IsTruncated : false;

command.input.ContinuationToken = NextContinuationToken;
}

} catch (err) {
console.error(err);
}

return keys
})(s3Client, 'my-bucket')
;
for (var j_index in objectList) {
j = objectList[j_index];
console.log(j)
}

generate.jsとして保存し、AWS SDKをインストールし、実行します。

shell
pnpm i @aws-sdk/client-s3
node generate.js

オブジェクト一覧が取得できました。