RMarkdownをknitしたときに画像をgistにアップロードする

knitrには実は、upload.funというオプションがあって、knitしたときに画像ファイルを自動でどこかにアップロードすることができます。

Upload images - Yihui Xie | 谢益辉

というのを今日知ったので、試しにgistにアップロードする関数を作ってみます。

指定する関数の例として、imgurに画像をアップロードするimgur_upload()という関数が用意されています。これを見てみましょう。

imgur_upload = function(file, key = '...') {
  if (!is.character(key)) stop('The Imgur API Key must be a character string!')
  res = RCurl::postForm(
    ...
  )
  res = XML::xmlToList(res)
  if (is.null(res$link)) stop('failed to upload ', file)
  structure(res$link, XML = res)
}

(knitr/utils-upload.R at f02600d24a9628c8a376df41078756dc9e63bfc2 · yihui/knitr · GitHub)

これは、ファイルパスを引数に取って、アップロード後のURLを返す関数です。URLにはXMLというメタデータが付いていますが、これはknitする際には使われないみたいなのでとりあえず文字列を返せば大丈夫です。

つくってみる

仕組みがなんとなくわかったところで、gistrパッケージを使ってgistに画像をアップロードする関数を実装してみます。gistrパッケージはちょっと癖があるんですが、その説明はここでは省きます。

ナイーブに実装するとこんな感じでしょう。

# Gistをつくる。空でないファイルを置かないとだめなので、dummyというファイルをいったん置いてあとで消す
g <- gistr::gist_create(code = "dummy", filename = "dummy", browse = FALSE)
g$delete_files <- list("dummy")
g <- gistr::update(g)

raw_url_base <- sprintf("https://gist.githubusercontent.com/%s/%s/raw/", g$owner$login, g$id)

gist_upload <- function(file) {
  # ファイルをコピーしてコミットするだけ
  g <- gistr::add_files(g, file)
  g <- gistr::update(g)
  paste0(raw_url_base, basename(file))
}

しかし、これだとなぜかうまく画像をアップロードできません。

f:id:yutannihilation:20170214234420p:plain:w450

なぜ…?

GitHubのHTTP APIではバイナリファイルをアップロードできない

とかって悩んでソースコードを読んでいると、こんなコメントが残っていました。

#' ## if not, GitHub doesn't allow upload of binary files via the HTTP API (which gistr uses)
#' ## but check back later as I'm working on an option to get binary files uploaded,
#' ## but will involve having to use git

(gistr/gist_create.R at 2cc6ff0371038e259b96bafeb18f1de6c425b4f7 · ropensci/gistr · GitHub)

実は、GitHubのHTTP APIではバイナリファイルをアップロードできない、ということらしいです。まあ考えてみれば当たり前で、投げるペイロードJSON(↓)なので、BASE64エンコードして渡す方法でもなければバイナリのデータを含める方法はありません。

{
  "description": "the description for this gist",
  "public": true,
  "files": {
    "file1.txt": {
      "content": "String file contents"
    }
  }
}

(https://developer.github.com/v3/gists/#create-a-gist)

ただ、これはHTTP APIの制限事項であって、Gist自体はバイナリファイルを扱えます。となるとやることはひとつ、HTTP APIを使わずにGistを使うことです。Gitプロトコルを使います。

Gitプロトコルを使う

gistrは、Gitプロトコルもサポートしていますが、それはGistを作成する部分だけみたいです。具体的には以下のコードで、gist_create()の代わりにgist_create_git()を使うことができます。ただし、add_files()とかのあたりはHTTP APIになってしまうみたいなので、結局git2rパッケージを使う必要があります。

(クレデンシャルの部分はとりあえずこうしていますが、正しい指定の仕方がわからなかったので詳しい方は添削お願いします…)

# Gistをつくる
g <- gistr::gist_create(code = "dummy", filename = "dummy", browse = FALSE)
g$delete_files <- list("dummy")
g <- gistr::update(g)

# HTTP APIではバイナリファイルをアップロードできないのでgitプロトコルを使う
repo <- git2r::clone(g$git_pull_url, local_path = tempfile("gist"))

gist_upload <- function(file) {
  # ファイルをコピーしてコミットするだけ
  file.copy(file, to = repo@path)
  git2r::add(repo, basename(file))
  git2r::commit(repo, basename(file))
  git2r::push(repo, credentials = git2r::cred_env("GITHUB_USERNAME", "GITHUB_PAT"))
  
  basename(file)
}

これをknitrのknit optionに指定します。base.urlは、上のgist_upload()で絶対URLを返すようにしていれば必要ありません。

knitr::opts_knit$set(upload.fun = gist_upload,
                     base.url = sprintf("https://gist.githubusercontent.com/%s/%s/raw/", g$owner$login, g$id))

結果

こんな感じです。

gist.github.com

感想

Gitを使うとそれぞれの手元の環境で設定が違いすぎるので、GistはあきらめてAPIキー一発でアップロードできるウェブサービスを使うのがいい気がしました。オススメを募集中です。