B-Teck!

お仕事からゲームまで幅広く

【Lambda】SAMのテンプレートに複数のHTTPメソッドを書く

前置き

1つのLambdaでGET、PUT、POST、DELETEを扱いたい要件があったけど何もわからなかったので。

実装

この辺りを参考に、Events配下に対応するMethodのEventを追加してやればよかった。
AWS SAMテンプレートでREST APIの複数メソッドをひとつのLambdaに統合するには? - 銀の弾丸
AWS::Serverless::Function - AWS Serverless Application Model
EventSource - AWS Serverless Application Model

Resources:
  SampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        Get:
          Type: Api
          Properties:
            Path: /sample
            Method: GET
        Put:
          Type: Api
          Properties:
            Path: /sample
            Method: PUT
        Post:
          Type: Api
          Properties:
            Path: /sample
            Method: POST
        Delete:
          Type: Api
          Properties:
            Path: /sample
            Method: DELETE

多分Goだとこんな感じで分岐できる。

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    resp := events.APIGatewayProxyResponse{Body: "Hello", StatusCode: http.StatusOK}

    // 任意のHTTPメソッドの分岐
    switch request.HTTPMethod {
    case http.MethodGet:
        return resp, nil
    case http.MethodPost:
        return resp, nil
    case http.MethodDelete:
        return resp, nil
    case http.MethodPut:
        return resp, nil
    default:
        return nil, errors.New("想定外のHTTPメソッドがリクエストされました")
    }
}

【雑記】2020年振り返り

大晦日なので2020年の振り返りです。

初めての挫折

今年の1月から5月頃まで、普段携わっているプロダクトではない部署へお手伝いに行っていました。
このチームは非常に良いチームではあったのですが、

  • 基本的に個々人が独立して作業している
  • 私に任された領域が実力よりも大きかった
  • ジョインまもなくから新型コロナウイルスの影響で在宅勤務になってしまった

など、様々な理由からバリューを出しづらい状態となってしまいました。
結果的にモノを完成させられずに期間満了で終了となってしまい、今でも悔いの残る結果となっています。
実は、社会人になってからしんどい場面はあったものの挫折らしい挫折は初めてです。
何をやってもうまく行かない、どう線を引いても終わる気がしない、弱音を吐く心理的安全性が無い、など、非常に苦しい状態でした。
在宅勤務で同僚の目が無いのを良いことに深夜に目覚めて作業したり、朝方から夜まで作業したりと、だいぶ追い詰められていた自覚があります。
生活習慣もボロボロになり、関係者の顔を見ると必要以上に緊張してしまう、というのが秋口まで続き、今年一年だいぶ苦しみました。
結局の所追い詰められた原因は

  • 失敗の経験がない故に甘く見積もってしまった
  • 無理そうなときに強く無理だと言えなかった
  • 自分を客観的に見ることができず、オーバーワークを許容してしまった
  • 当初の想定よりも手助けがなかった

という部分にあると思います。
次慣れないことをやるときはしっかりと弱音を吐ける、自分ですべてを抱え込もうとしない関係を構築していければな、と思っています。

外部記憶サービスの変遷

メモアプリの変更

2018年から利用していたDynalistの利用をやめ、メモの集約をNotionに移しています。
文章の構築を行う上では便利だったアウトライナーをやめた理由は

  • メモとして使っていると気がつくと階層が深くなりすぎたりする
  • 記事の形式にするときに項目のnote機能を使う必要がある

など、ブログへのアウトプットのベースとしては利用がしづらいことが理由でした。
Notionは

  • 各ドキュメントがページであること
  • ある程度markdownで出力することも考えながら書けること
  • D&Dで文章を入れ替えることが出来る

など、ブログの下書きとして利用する上で個人的にメリットが多かったため移行しました。
また、Notionではページに対して複数のビューを用意することができ、任意のタグだけフィルタリングする、カレンダーのように表示するなどといったカスタマイズが出来ます。
この機能を使ってみたいという興味も後押ししました。

現在は日記、技術的なメモのブログ下書き、読んだ記事一覧をNotionにストックしています。

読んだ記事はこれまでクラウドブックマークに記録していたのですが、後で見返したい記事と調べたり読んだりした記事を同じところに格納するのは煩雑だという結論に至ったため、別で管理することにしました。
この記事を書いているタイミングではこのような一覧になっています。
f:id:beatdjam:20201231113726p:plain
自分がどういう事柄に興味を持ったのか、線で見えるのはなかなかおもしろい体験なので、インプットを記録してみるのは結構ありだと思います。

クラウドブックマークの変更

従来ははてなブックマークを利用していたブックマークを、Raindrop.io — All-in-one bookmark manager に変更しました。

  • ブックマークを階層化出来る(有料機能)
  • はてなブックマークからimportが可能

だったのが主な理由です。

はてなブックマークは、ソーシャルブックマークとして他者のコメントを読んだり、コメントしたりという使い方は面白いものの、純粋なブックマークとしての使いづらさがあります。
Raindropは、ブックマーク整理のときにサイトをつかんで別のカテゴリに移したり、削除したりといった直感的な操作もできて、個人的に使いやすかったです。
大量のブックマークを表示させると若干重くなる部分があるので、整理にコツが必要な点はちょっとむずかしいところですが…

生活リズムの変化

blog.beatdjam.com blog.beatdjam.com blog.beatdjam.com 以前の記事でも書いたように、最近はタスク駆動で日常生活を送るようにしています。

この生活スタイルになったことによって、家事や日常の雑事をとりあえず片付けるようになり、だいぶ生活の質が向上しました。
反面、管理されない時間(娯楽や休息の時間)を取りにくいのが目下の悩みです。

とはいえ、できなかったことが出来ること、うまく行っていないことが可視化していることは以前よりも大きく前に進んでいる感覚があるので、来年はこれをよりブラッシュアップできればなと思っています。
あとは、業務などの変化によって揺らがない、自分なりの生活スタイルの軸を作れると良いかなと思っています。
これがないとあらゆるものが後回しになり、仕事にすべてを向けてしまうので…。

身体機能の衰え

今年30歳になったせいもあるのか、急激に体力や代謝が落ちているのを感じています。
これまでは身体的な若さでカバーできていた部分もだんだんそうはいかなくなっているようで、筋肉もかなり落ちてきてしまいました。
流石に不安になってきたので、休日は1~2時間程度の散歩を行いつつHIITをこなしたり、毎日Fitboxingをしたりしています。

おじさんになるとみんなジョギングしたり運動しだすのは突然趣味に目覚めるやつなのかなと思っていたんですが、みんな必要に駆られてやってたんですね…。

まだ目に見えて効果が出るほどは続けられていないので、来年の振り返りでうまく行っていることが書けるように願っています。

将来への不安

これも30になったからなのかもしれませんが、将来に漠然とした不安が生まれた1年でした。
社会人になりたての頃は家庭的な事情もあり、とりあえず生きているのが精一杯といった感じだったので、今の会社に入る頃までは将来のことなんて考える暇もありませんでした。

現職に入社してそろそろ3年。いろいろな経験を積んで、いろいろな物を見て、自分自身がどうなっていきたいのか、どういうポジションについていたいのか、そういった事を考える余裕がようやく出てきたのかもしれません。
今の仕事はとてもおもしろく、新しい技術にも日々触れる事ができ、責任を持って育てられるプロダクトもあり、充実して過ごしていますが、一方で賃金を上げたいなあという野望もあります。
知人が転職活動をするのを見るにつけ、自分の頭の中にも転職の二文字が浮かぶことはあったりもします。

今後どう進んでいくかはまだ何もわからないのですが、後になって振り返ってもあの方向に納得して進んだなと思える選択を模索していけたらなと思います。

設計への興味

これまでは小手先の実装テクニックばかりに目が行きがちだったのですが、今年は設計について考え続けた一年でした。
booth.pm
booth.pm

これらの本を読みながら、オブジェクトの責務とは何なのか、誰が何を知っているべきなのかといったことをひたすら考え続けています。
夢に見るくらい悩んでいてもなかなか腹落ちせず苦しんだりもしていますが、プロダクトに手探りで適用しながら適切に分割していくのは楽しいです。
来年末にはチョットデキルになっていたら嬉しいですね。

ブログのPV

2013年の開設以来、毎年前年比純増のPVを記録し続けていたんですが、昨年秋のコアアップデート以来著しく検索ヒット率が下がり、大打撃を受けてしまいました。
2020年は初めての純減どころか大幅な下がり幅を見せ、2016年頃の水準になってしまっています。
なかなか厳しいところでは有るのですが、今年の12月のコアアップデートで若干盛り返した気配があり、このまま復活していってくれることを願っています…
f:id:beatdjam:20201231130616p:plain

おわりに

2020年は、新型コロナウイルスの影響もあり、個人的にも、業務的にも非常に苦しんだ一年でした。
未だ収束は見えない状況ですが、一日も早く以前のように気軽に他者と触れ合うことができ、大手を振って遊びに行けるような状態になってくれないと、閉塞感でだいぶ厳しい気持ちになっています。
収束したら、気軽に飲みに誘ったり、遊びに行ったりしてやってください。
来年も変わらず仲良くしていただけると嬉しいです。どうぞよろしくおねがいします。

【GAS】Drive上のファイルの共有リンクを取得し、ダイアログからダウンロードさせる

前回の続きです blog.beatdjam.com

今回はDrive上のファイルの共有リンクを取得し、HTMLで作ったDL用のダイアログを表示させます。
また、スプレッドシートのメニューに任意のメニューを追加する方法も合わせて書きます。

ファイルの共有リンクを取得する

ファイルオブジェクトを取得する

共有リンクを取得するのはFileオブジェクトのIDを知る必要があります。
いくつか方法がありますが、今回はシンプルにDriveAppを用います。
フォルダ名(1階層)・ファイル名を指定して取得する場合はこのように書きます。

/**
 * フォルダ名、ファイル名からFileオブジェクトを取得
 * @param folderName 
 * @param fileName 
 */
function getFileId(folderName, fileName) {
    // フォルダ・ファイルを取得
    const folder = DriveApp.getFoldersByName(folderName).next();
    return folder.getFilesByName(fileName).next();  
}

もしDrive直下のファイルであれば、直接DriveAppから取得できます。

DriveApp.getFilesByName().next();

共有リンクを取得する

Fileには getDownloadUrl() が生えているので、これで共有リンクが取得できます。

function getDownloadUrl(folderName, fileName) {
  // フォルダ・ファイルを取得
  const folder = DriveApp.getFoldersByName(folderName).next();
  return folder.getFilesByName(fileName)
        .next()
        .getDownloadUrl(); // ここでリンク生成  
}

アクセストークンをつける(任意)

共有リンクにはアクセストークンを付与することができます。
組織内で認証が必要な権限のファイルなどをGASで取得するような場合に、アクセストークンがついていないと認証エラーで上手く取得できません。
アクセストークンは ScriptApp.getOAuthToken() で取得することができます。
これを前述したFile.getDownloadUrl()に下記のようにくっつけてやることで、認証可能なURLを生成できます。

file.getDownloadUrl() + "&access_token=" + ScriptApp.getOAuthToken()

HTMLテンプレートを利用してDL用のダイアログを作る

メニューに処理起動メニューを追加する

ここからHTMLをダイアログで表示する機能を作成しますが、その前にスプレッドシートからGASを起動するメニューを作成する必要があります。
というのも、UIに関わる操作はGASのエディタ上からは起動できず、スプレッドシートから起動しないと試せないからです。
手順は簡単で、onOpenハンドラ にメソッドを呼び出す挙動を記述してやるだけです。
今回はダイアログ作成用の関数を呼び出しています。

/**
 * ファイルを開いたときのイベントハンドラで自作メニューを追加
 */
function onOpen() {      
  const menu = SpreadsheetApp.getUi().createMenu('File Download');
  menu.addItem('Exec', 'showDownloadModal');
  menu.addToUi();
}

function showDownloadModal() {
  const url = doCreateZip(); // Zip作成→URL返却
}

この処理を記述した状態でスプレッドシートをリロードすると登録したメニューが現れます。

f:id:beatdjam:20201229173415p:plain

この記事では扱いませんが、より柔軟なメニューの追加はこちらの記事が参考になります。 Google Apps Scriptを使った独自メニューの作り方 - Qiita

これで、ダイアログを開発するための準備が整いました。

Templated htmlについて

GASでは、HTMLファイルのテンプレートを読み込んで、動的なページを生成することができます。
HTML Service: Templated HTML  |  Apps Script  |  Google Developers
ざっくりいうと、下記のような構文が利用できます。

<?= ?> : テキストとして出力される。HTMLタグなどが含まれていた場合はエスケープされる。  
<?!= ?> : テキストとして出力される。HTMLタグが含まれていてもそのまま埋め込まれる。  
<?  ?> : タグ内の処理がスクリプトとして実行される。実行結果は出力されない。  

これを利用するとこんな表示の制御ができます。

  • 要素の表示非表示切り替え
function displaySample() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.isVisible = true;
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Dialog");
}
<html>
  <body>
  <? if (isVisible) {?>
    isVisibleがTrueのときのみこの要素が出力されます。
  <? }?>
  </body>
</html>
  • リンク生成
function displaySample() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.url = "https://example.com/";
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Dialog");
}
<html>
  <body>
        <a href="<?=url?>">Link</a>
  </body>
</html>

ダイアログの表示

先述したリンク生成の内容ほぼそのままですが、これでテンプレートから生成したHTMLをモーダルダイアログとして表示できます。

/**
 * DLを取得してテンプレートからモーダルダイアログを生成して表示する
 */
function showDownloadModal() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.url = doCreateZip(); // Zip作成→URL返却
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Download");
}
<html>
  <body>
    <a href="<?=url?>">Link</a>
  </body>
</html>

f:id:beatdjam:20201229173456p:plain
これで完成です!

おわりに(コード全文)

前回の記事からだいぶ間があいてしまいましたし、GASもリファクタの余地がある状態ですが、ひとまずまとめることができました。
あまりGASを触ったことがないけれど、似たような事がしたい方などに本記事が役立てば良いなと思います。

前回分も含めて記事内で登場したコードの全文を掲載して終わりたいと思います。ありがとうございました!

/**
 * ファイルを開いたときのイベントハンドラで自作メニューを追加
 * 
 */
function onOpen() {      
  const menu = SpreadsheetApp.getUi().createMenu('File Download');
  menu.addItem('Exec', 'showDownloadModal');
  menu.addToUi();
}

/**
 * DLを取得してテンプレートからモーダルダイアログを生成して表示する
 * 
 */
function showDownloadModal() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.url = doCreateZip(); // Zip作成→URL返却
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Download");
}

/**
 * シートからZipファイルを作成してDLリンクを取得
 * 
 */
function doCreateZip() {
    const sheetName = 'sample';
    const jsonKey = 'sample_json';
    const json = JSON.stringify({ jsonKey: getData(sheetName) });

    // 圧縮用のblob作成
    const blobs = new Array();
    blobs.push(Utilities.newBlob(json, 'application/json', 'hoge/fuga.json'));
  
    const fileName = 'archive.zip';
    const zip = Utilities.zip(blobs, fileName);
  
  
    const folderName = 'temporary';
    const folder = getOrCreateTempFolder(folderName);
    const file = createFile(folder, zip);

    return getDownloadUrl("temporary", "archive.zip");
}

/**
 * 与えられたシート名からシートを取得し、表からjsonに変換可能なオブジェクトを生成して返却する
 * 
 * @param sheetName 
 */
function getData(sheetName) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    const rows = sheet.getDataRange().getValues();
    const keys = rows.splice(0, 1)[0];

    return rows.map(row => {
        const obj = {}
        row.forEach((item, index) => { obj[keys[index]] = item; });
        return obj;
    });
}

/**
 * 指定したフォルダ名がすでに存在していればそのフォルダを、
 * 存在しなければ作成したフォルダのオブジェクトを返す
 * 
 * @param folderName 
 */
function getOrCreateTempFolder(folderName) {
    // 未作成なら作成してフォルダを取得する
    const folders = DriveApp.getFoldersByName(folderName);
    if (folders.hasNext()) return folders.next()
    else return DriveApp.createFolder(folderName);
}

/**
 * 与えられたオブジェクトをファイル化して指定したフォルダ内に配置する
 * 
 * @param folder 
 * @param file 
 */
function createFile(folder, file) {
    const files = folder.getFilesByName(file.getName());
    if (files.hasNext()) folder.removeFile(files.next());
    return folder.createFile(file);
}

/**
 * フォルダ名とファイル名からDLリンクを生成します
 * 
 * @param folderName
 * @param fileName
 */
function getDownloadUrl(folderName, fileName) {
  // フォルダ・ファイルを取得
  const folder = DriveApp.getFoldersByName(folderName).next();
  return folder
            .getFilesByName(fileName)
            .next()
            .getDownloadUrl(); // ここでリンク生成  
}