ECHOPFのJavaScript SDKを使ってMarkdown記法で書いたドキュメントをブログにアップロードできるソフトウェアを作ります。JavaScript SDKはNode.jsで利用します。
前回まででECHOPFのサーバからデータを取得する流れができあがりました。今回は新しい記事を書くための下準備と、クラウドにアップロードするまでの流れを作ります。
新規記事のベースを作成する
新しい記事はMarkdown記法で作成します。その際、イチからファイルの内容を作るのは手間なので、前回作った記事の保存処理を流用してテンプレートを生成できるようにします。
全体の流れ
まず index.js
に作成する処理の流れになります。 BlogNew
オブジェクトを作成して generate
メソッドで記事テンプレートを生成します。
const BlogNew = require('./libs/new');
program
.command('new')
.description('新しい記事のベースを作成します')
.action(() => {
const blogNew = new BlogNew({
dir: '.',
});
blogNew
.generate()
.then(() => {
console.log(`記事を生成しました。./${blogNew.entry.refid}.md`);
});
});
BlogNewの作成
BlogNew
は libs/new.js
として作成します。これも BlogPull
と同様に BlogBase
を継承します。
const BlogBase = require('./base');
module.exports = (() => {
class BlogNew extends BlogBase {
constructor(options) {
super(options);
this.dir = path.resolve(options.dir);
}
// ブログ記事のテンプレートを生成
generate() {
}
}
return BlogNew;
})();
generate メソッド作成
generate
メソッドはまずブログのエントリーオブジェクトを作成します。
this.entry = new this.ECHOPF.Blogs.EntryObject(this.config.blogInstanceId);
そしてユニークなIDであるReference IDを作成します。これはECHOPFのルールに従って、 年月日時分秒 で作成します。デフォルトのJavaScriptでは日付を扱うのが若干手間なので、 strftime
というライブラリを使うことにします。まず npm コマンドでライブラリをインストールします。
$ npm install strftime --save
このライブラリを new.js
で読み込みます。
const strftime = require('strftime');
そして先ほど生成したエントリーオブジェクトに適用します。
this.entry.refid = strftime('%Y%m%d%H%M%S');
後は日付や本文、カテゴリのベースを適用します。
this.entry.put('published', new Date());
this.entry.put('categories', []);
this.entry.put('contents', { main: '', detail: '' });
これらが終わったら、ファイルとして保存します。配列にしているのは saveEntries
が配列で適用する形になっているためです。
const entries = [this.entry];
return this.saveEntries(entries);
これでテンプレートの保存が完了しました。 generate
メソッドの内容は次のようになります。
generate() {
this.entry = new this.ECHOPF.Blogs.EntryObject(this.config.blogInstanceId);
this.entry.refid = strftime('%Y%m%d%H%M%S');
this.entry.put('published', new Date());
this.entry.put('categories', []);
this.entry.put('contents', { main: '', detail: '' });
const entries = [this.entry];
return this.saveEntries(entries);
}
コマンドの実行
ではコマンドを実行してみます。
$ echopf new
記事を生成しました。./20171126164915.md
このように出力されれば完成です。なお、このファイルの内容は次のようになります。
---
refid: 20171126164915
title:
description:
keywords:
robots:
link_status:
owner:
published: Sun Nov 26 2017 16:49:15 GMT+0900 (JST)
categories:
---
<!--more-->
後は記事を自由に書いていくだけです。 <!--more-->
より前が概要、後ろが本文になります。
ブログ記事の投稿処理
ブログ記事を書き終えたら、アップロード処理を作っていきます。
全体の流れについて
先ほどと同様に、まず index.js
の内容を紹介します。
const BlogPost = require('./libs/post');
program
.command('post [filePath]')
.description('記事を新規投稿します')
.action((filePath) => {
const blogPost = new BlogPost({
dir: '.',
filePath,
});
blogPost
.post()
.then((entry) => {
console.log(`記事をアップロードしました。 ${entry.get('url')}`);
const blogPull = new BlogPull({
dir: '.',
});
console.log('記事一覧を更新します。');
return blogPull.pull();
})
.then(() => {
console.log('記事を取得しました。');
})
.catch((err) => {
console.log(`エラーが発生しました。${JSON.stringify(err)}`);
});
});
処理の流れは BlogPost
オブジェクトを作成して post
メソッドでアップロードを行います。次に BlogPull
オブジェクトを生成してアップロードした記事をECHOPFのクラウドよりダウンロードします。
コマンドは次のようになります。指定したファイルパスは filePath
という変数で取得できます。
$ echopf post [Markdownファイルのパス]
BlogPostについて
BlogPost
の概要は次のようになります。こちらも BlogBase
オブジェクトを継承します。
const path = require('path');
const BlogBase = require('./base');
module.exports = (() => {
class BlogPost extends BlogBase {
constructor(options) {
super(options);
this.dir = path.resolve(options.dir);
this.filePath = path.resolve(options.filePath);
}
// ブログの投稿処理を行います
post() {
}
}
return BlogPost;
})();
post
メソッドは次のようになります。アップロード処理は事前にログインを行う点がこれまでと異なります。
post() {
return new Promise((res, rej) => {
const me = this;
// ログイン処理を行う
this.login()
.then((member) => {
if (Object.keys(member).length === 0) {
throw new Error('認証エラー');
}
// ファイルを読み込む
return this.readFile(this.filePath);
})
// Markdownの内容をブログエントリーオブジェクトに展開
.then(contents => me.setContents(contents))
// ブログエントリーをアップロード
.then((entry) => entry.push())
// アップロード成功
.then((success) => {
// 完了
res(success);
}, (err) => rej(err)) // エラー
});
}
ログイン処理について
ブログへの記事アップロードは権限が必要になりますので、事前にログイン処理を行う必要があります。これは ECHOPF.Members.login
というメソッドで行います。引数はメンバーインスタンスのID、ログインIDそしてパスワードです。
以下の login
メソッドは今後繰り返し使いますので base.js
内に定義します。
login() {
return this.ECHOPF.Members.login(
this.config.memberInstanceId,
this.config.login_id,
this.config.password
);
}
認証が終わると認証された会員オブジェクトが返ってきます。
ファイルの読み込み
指定されたMarkdownファイルの読み込みはNode.jsの標準機能を使います。こちらも今後使っていきますので base.js 内に定義します。こちらは取り立てて書くことはありませんが、デフォルトのコールバック型ではなく、Promiseオブジェクトを返すようにしています。
readFile(filePath) {
return new Promise((res, rej) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) return rej(err);
return res(data);
});
});
}
エントリーオブジェクトの作成
読み込まれたMarkdownファイルの内容からブログエントリーオブジェクトを作ります。これは難しい処理ではありませんが、若干長くなります。ヘッダー情報部と、コンテンツ部の切り出しと、ACLの設定が主な処理になります。
ECHOPFに投稿する際にはMarkdownではなくHTMLにする必要があります。そこで Showdown
というライブラリを使います。まず、これを npm
でインストールします。
$ npm install showdown --save
そして base.js
の中で読み込みます。
const showdown = require('showdown');
また、ヘッダー部はYAML記法で定義していますので、それを読み込むための js-yaml
というライブラリをインストールします。
$ npm install js-yaml --save
こちらも base.js
の中で読み込みます。
const yaml = require('js-yaml');
コンテンツの設定
エントリーオブジェクトを作成する setContents
ではカテゴリについても処理します。例えばカテゴリはMarkdownの中で次のように定義します。
categories:
- 技術系
- バージョンアップ
- テスト
これらの名前でECHOPFのクラウド上にすでに存在するかチェックし、なければ作成します。
カテゴリの取得
まずECHOPF上のカテゴリを取得します。これは ECHOPF.ContentsCategoriesMap
オブジェクトを使います。この処理は base.js
に記述します。
// 既存のカテゴリを取得します
getCategories() {
return new Promise((res, rej) => {
const categoriesMap = new this.ECHOPF.ContentsCategoriesMap(this.config.blogInstanceId);
categoriesMap
.fetch()
.then((categories) => {
res(categories);
}, (err) => {
rej(err);
})
});
}
カテゴリの作成
次にMarkdown内に設定されたカテゴリ名(配列。ここでは ary に入っているものとします)と既存カテゴリの比較を行います。カテゴリがすでにあればそれを使い、なければ ECHOPF.ContentsCategoryObject
インスタンスを作成してカテゴリ情報をセットします。なお、新しいカテゴリの refid
は cat-(マイクロセコンド) という形で作成しています。最後にまとめて Promise.all
でカテゴリを作成します。
// カテゴリの存在チェック。なければ作成します
createCategories(categories, ary) {
return new Promise((res, rej) => {
const me = this;
// 新規作成するカテゴリが入ります
const promises = [];
// 最後にカテゴリはすべてこの配列に入れます
const echopfCategries = [];
// Markdown上のカテゴリについて順番に確認します
for (let i = 0; i < ary.length; i += 1) {
let name = ary[i];
let existCategory = null;
// 既存のカテゴリ名との一致を確認します
for (let j = 0; j < categories.children.length; j += 1) {
let category = categories.children[j];
if (category.node.get('name') === name) {
existCategory = category;
}
}
if (existCategory) {
// 既存カテゴリにある場合
echopfCategries.push(existCategory);
}else{
// 既存カテゴリにない場合
const newCategory = new me.ECHOPF.ContentsCategoryObject(me.config.blogInstanceId);
newCategory.put('name', name);
newCategory.put('refid', `cat-${new Date().getTime()}`);
promises.push(newCategory.push());
}
}
// 存在しないカテゴリ名をまとめて作成します
Promise
.all(promises)
.then((ary) => {
// 作成完了した場合
for (let i = 0; i < ary.length; i += 1) {
ary[i].refid = ary[i].get('refid');
echopfCategries.push(ary[i]);
}
res(echopfCategries);
}, (err) => rej(err))
});
}
setContents
処理では上記のカテゴリ処理を実行し、その後ヘッダー部の処理とブログ記事本文の処理を行います。
// ファイルからブログエントリーオブジェクトの生成
setContents(contents) {
const me = this;
const entry = new this.ECHOPF.Blogs.EntryObject(this.config.blogInstanceId);
return new Promise((res, rej) => {
const parts = contents
.match(/---\n([\s\S]*)\n---\n([\s\S]*)$/);
// ヘッダー部をYAMLとして読み込みます
const headers = yaml.safeLoad(parts[1]);
me
// 既存のカテゴリを取得します
.getCategories()
.then((categories) => {
// 既存のカテゴリとMarkdown上のカテゴリを比較します
return me.createCategories(categories, headers.categories);
})
.then((categories) => {
// Markdown上のカテゴリは削除します
delete headers.categories;
// 取得したカテゴリを設定します
entry.put('categories', categories);
// ヘッダーを設定します
for (let header in headers) {
let value = headers[header];
switch (header) {
case 'published':
entry.put(header, new Date(value));
break;
default:
entry.put(header, value);
}
}
// 日付を設定します
const d = new Date();
entry.put('modified', d);
entry.put('created', d);
// 記事本文の処理です
const bodies = parts[2].split(me.bodySplit);
// MarkdownからHTMLに変換するコンバータを用意します
const converter = new showdown.Converter();
// 記事本文はmainとdetailに分けて設定します
entry.put('contents', {
main: converter.makeHtml(bodies[0]),
detail: converter.makeHtml(bodies[1]),
});
// 権限を設定します
const acl = new me.ECHOPF.ACL();
// get, list, edit, deleteの順
acl.putEntryForAll(new me.ECHOPF.ACL.Entry(true, true, false, false));
entry.setNewACL(acl);
res(entry);
})
});
}
処理が完了するとエントリーオブジェクトである entry
を返しますので、これを保存する push
メソッドを実行します( post.js
内)。
// ブログエントリーをアップロード
.then((entry) => entry.push())
これでアップロード処理が完了です。先ほど作成したMarkdownファイルを編集して、実際にアップロードしてみましょう。
$ echopf post 20171126164915.md
記事をアップロードしました。 http://blogcli.echopf.com/information/entry/20171126164915
記事一覧を更新します。
記事を取得しました。
管理画面でもその内容が確認できるはずです。

また、新しく追加したカテゴリも作成されているのが確認できるでしょう。

今回までの流れでブログ記事の新規作成と、そのアップロード処理が完成しました。Markdownファイルをベースにすることでコンテンツの作成が容易になるでしょう。次回はブログ記事の編集と更新、そしてブログ記事の削除までの処理を作ってみます。
ここまでのコードは echopfcom/Echopf_Blog_CLI at v2 にアップロードしてあります。実装時の参考にしてください。