はじめに
以前から気になってたiPhone用の Editorial がバージョンアップ記念?で嬉しい値下げ。同じ作者さんの Pythonista を買おうか Editorial の新しいバージョンがリリースされたら買おうか迷ってたので、さっそく購入して遊んでみた。
いやー、これはいいですわ。 Automator っぽくGUIでさらっとWorkflowを作ることも、Pythonでゴリゴリ実装することも出来る。おまけにGUIのフォームも利用出来るし。ってことで、それまで使っていた 1Writer から速攻で乗り換えちゃいました。いや、 1Writer も良かったんですけどね。
ということで、あまりにも感動したので思わず記事をPublishしてみました。書くだけではなく公開まで。それもiPhoneだけで。そうiPhoneならね。
仕組み
このBlogは GitLab の Page機能 を使い、 Hugo で作られた静的ファイルを同じく GitLab の CI機能 を使って Netlify に転送し、HTTPS配信してる。
- Markdownで記事作成
- GitLabへコミット
- GitLabのCIによりHugoを使って静的ファイルが作成
- 同じくGitLabのCIでNetlifyへ転送
なので GitLab へコミットさえ出来ればiPhoneから更新することも可能となっている。iPhoneにGitクライアントあればそれで手っ取り早く構築できるのだが、ここでは Editorial のPythonを利用して環境を構築してみようと思う。
と言ってもPython経由でGitの操作をするわけではなく、 GitLab の RestAPI 経由でコミットすれば他は自動的に流れてくれる。なので,Markdownを扱えてPythonでHTTPの操作が出来る Editorial とは非常に都合が良いと思う。
仕様
サーバー周りの仕様については今まで通りなので今回は省略し、クライアント側(iPhone)での操作だけに絞る。
- EditorialのWorkflowを起動
Pull
かPush
を指定Pull
を選んだ場合- API経由でファイルリストを取得して一覧表示
- 新規作成も可能なように一番上には
New content...
を追加
- 新規作成も可能なように一番上には
- 一覧表示から編集したいファイル名を選択
- 日付順に並べさせたいところだが、API経由のJSONに更新日の情報がないので適当な並びになってしまう
- ファイル名じゃなくタイトルにしたいところだけど、こちらも(ry
New content...
の場合には別途ファイル名を指定させる
- 指定したファイルのデータをこれまたAPI経由で取得して
local
のファイルとして書き出し- 新しいファイルだったりエラーが発生した場合はMETAデータのみの空ファイルとする
- API経由でファイルリストを取得して一覧表示
Push
を選んだ場合- 現在開いているファイルの情報を元にGitLabへAPI経由でコミット
- ファイル名は開いているファイルの名前を利用する
- ナイルの内容は開いているファイルの本文を利用する
- コミットが成功したら
local
に作成したファイルを削除
- 現在開いているファイルの情報を元にGitLabへAPI経由でコミット
Push
と Pull
を同じWorkflowにしてるのは単に共通する処理があるためなんだけど、こんな感じで増やしていくとWorkflowリストもスクロールしないと探せなくなるので出来るだけまとめたほうが良さそう。
実装
実装についてはこんな感じ。GitLabのAPIについては こちら を参照してください。
#coding: utf-8
import requests
import json
import base64
import workflow
import editor
import os
import console
import datetime
API = 'https://gitlab.com/api/v3'
BRANCH = 'master'
ROOT = 'content/post'
TOKEN = 'xxxxxxxxxxxxxxxxxxxx'
PROJECT = '0123456789'
def buildURL(path, args):
args.append("private_token=" + TOKEN)
return API + path + '?' + '&'.join(args)
def getFileList():
args = []
args.append("path=" + ROOT)
args.append("recursive=true")
url = buildURL('/projects/' + PROJECT + '/repository/tree', args)
res = requests.request('GET', url)
objects = res.json()
files = ["New contents..."]
for object in objects:
if object["type"] == "blob":
files.append(object["path"])
return files
def getFileContent(path):
args = []
args.append("file_path=" + path)
args.append("ref=" + BRANCH)
url = buildURL("/projects/" + PROJECT + "/repository/files", args)
req = requests.request('GET', url)
if req.status_code == 200:
res = req.json()
return base64.b64decode(res["content"])
else:
return ""
def putFileContent(file, content):
args = []
url = buildURL("/projects/" + PROJECT + "/repository/files", args)
payloads = json.dumps(
{
"file_path": ROOT + "/" + file,
"branch_name": "master",
"commit_message": file,
"content": content
},
)
res = None
if getFileContent(ROOT + "/" + file) == "":
res = requests.post(url, data=payloads, headers={'Content-Type': "application/json"})
else:
res = requests.put(url, data=payloads, headers={'Content-Type': "application/json"})
if res.status_code == 200:
return True
else:
return False
def pull():
path = dialogs.list_dialog("File select", getFileList(), False)
if path == None:
return
if path == "New contents...":
fields = [{"type": "text"}]
path = dialogs.form_dialog("Enter filename", fields)["0"] + ".md"
content = """
+++
tags = []
date = "{date}"
title = ""
draft = true
+++
"""[1:-1].format(date=datetime.date.today().isoformat())
else:
content = getFileContent(path)
if content != "":
file = path.replace(ROOT + "/", "")
workflow.set_output(file)
editor.set_file_contents(file, content)
workflow.set_output(file)
else:
console.hud_alert("Could not get content")
def push():
file = editor.get_path().split("/")[-1]
if '.md' in file:
content = editor.get_file_contents(file)
result = putFileContent(file, content)
if result:
path = os.path.expanduser('~/Documents/' + file)
os.remove(path)
console.hud_alert("Success")
else:
console.hud_alert("Put error")
else:
console.hud_alert("Not markdown")
result = console.alert("Action?", "Select action", "Pull", "Push", "Cancel", hide_cancel_button=True)
if result == 1:
pull()
elif result == 2:
push()
workflow.set_output("")
TOKENとかPROJECTなんかは KeyChain を利用するのが良いんだろうけど、個人用ということで省略。
そして、最後に変数 Input
が空じゃなかったら変数 Input
のファイルを開くというWorkflowを追加している。
最後に
と、ここまで書いて気がついたのだが、画像を送る手段は別途考えなきゃいけないことに気づいた...
とは言え、テキストのみであれば問題なく更新することができるようになった。電車の中でざざーっと下書きを作ったり、事前にデスクトップで作成していたものをリリースしたりといろいろ活用できそう。
なかなかいい環境が出来たし、他にもたくさん活用できそうで夢が広がってくる。
「 そこに山がある限り 」 的なノリで、新しい環境が用意されるとそれを利用してなにか新しいことを試したくなるこの性格。なかなか面倒だけど自分ではちょっと気に入ってる。