B-Teck!

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

【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(); // ここでリンク生成  
}

【Android】syntheticsのサポートが終了するのでViewBindingを使ってみた

なぜViewBindingの導入をしたか

Kotlin Android ExtentionのSyntheticsなどが非推奨になったため。

Kotlin Android Extensions の未来

Syntheticsがなぜ非推奨になったのか

  • どこからでも参照できてしまうのでネームスペースがごちゃごちゃになる
  • Nullの可能性の情報を公開しないので、常にNullableとして扱う必要がある
  • Kotlinでしか動作しない

ViewBindingの利点

  • DataBindingとbindingの扱い方がほぼ同じ
  • Bindingクラスが自動生成されるのでコードが必ずレイアウトに対応する
  • Null安全
  • Javaでも動く

導入

buildFeaturesに設定値を追加すると、Viewに対応するBindingクラスが自動生成される。

buildFeatures {
    viewBinding true
}

自動生成されたクラスをViewのOnCreateで読み出し利用する。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.androidfundamentals.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.textView1.text = "test"
    }
}

参考にしたサイト

matsudamper.hatenablog.com qiita.com

緩い完了条件と開始時間を決めると習慣化に成功する(かも)

習慣を続けるのは難しい

英語を勉強する、毎日運動をする、机の周りを片付ける…
この記事を読んでいる方にもいろいろな習慣があると思います。
こういう習慣、きちんと続いていますか?
今日はいいや…って後回しになりがちじゃありませんか?
私も、色々なことを習慣づけようとしてはうまく行かず、投げ出してきました。

なんとなく続かないものには原因があるのかもしれないと思い、
理由を自分なりに考えてみました。

とことん低い目標を立てる

一番最初に思いついた理由は、掲げる目標が曖昧だったり、高すぎるということでした。

曖昧な目標は例えば、「毎日読書をする」みたいなふわっとした目標を立ててみたものの、
実際どのくらい読めば納得できるのかわからない…とみたいになってしまったり。

高すぎる目標であれば、「毎日100回スクワット」みたいな目標を立てて、 普段運動してないから辛くなってしまい、数日休んでそのままやらなくなってしまったり。

こうならないために、「とことん低い目標を立てる」ということをしてみました。
本を10ページだけ読む、英語のアプリを1周だけやる、みたいな感じで、
取り掛かるハードルが低くて、まず達成できそうな目標を設定します。

もし気分が乗ってもっとやりたくなれば続ける、だめだったら切り上げる。 そのくらいの緩さのほうが続けやすそうです。

やり始める時間を決める

達成するハードルが低くても、やらなきゃならないことをやり始めるのって億劫じゃないですか。

なんとなく別のことがやりたくなって、ズルズル先延ばししてるうちに寝る時間になって、
今日はもういいかな…みたいなことありますよね。

こうならないようにするには、「やり始める時間を決める」というのが良さそうでした。

時間になったら、嫌でも、やりたくなくても、とりあえず動き始める。
そう決めておくことで、自分の気持ちで取り掛からない、ということをなくします。

もちろん、仕事や他の用事のせいで同じ時間に出来ない日なんかはあります。
でも、時間を過ぎてるからササッとやってしまおう、となったり、
今日はこの時間忙しそうだから先に済ませておこう、なんて配分も出来るようになります。

続けるために

これを意識するようになってから、私は比較的うまく習慣化が続いています。
もちろんそんなにうまく行かないよ、とかそれが出来れば苦労しない!と思う人もいるかも。

でも、目標を立てているということは何かを成したいと思っているはず。
どんなに小さい目標でも、行動でも、続けていればいずれスケールしていきます。
その意志力で、まずは一つ、私と一緒に動き始めてみませんか?