まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2017/03

Google ドライブのファイルシステム(?)を fuse を使って Linux にマウントする、というためのツールを使ってみました。今回は Google Drive ocamlfuse というツールを使って、Ubuntu 14.04 環境にマウントしてみました:
https://github.com/astrada/google-drive-ocamlfuse


なお、今回紹介する手順では途中でウェブブラウザを使った OAuth 認証を行うため、GUI 環境が必要です(コマンドライン環境だけでは最後までマウントできません)。GUI アクセスできる Ubuntu とウェブブラウザをご用意ください。

まずはターミナルを開き、Google Drive ocamlfuse を apt-get でインストールできるようにするため、リポジトリを追加します:
$ sudo add-apt-repository ppa:alessandro-strada/ppa
$ sudo apt-get update

そして apt-get install を実行します:
$ sudo apt-get install google-drive-ocamlfuse

準備の最後に Google Drive をマウントする先のマウントポイントとなるディレクトリ(以下の例では ~/googledrive)を用意しておきます:
$ mkdir ~/googledrive

では Goodle Drive ocamlfuse を使って実際にマウントしてみます。初回のみコマンドラインから引数なしで実行します:
$ google-drive-ocamlfuse

するとウェブブラウザが起動し、Google の OAuth 認証が行われます。Google ドライブを使うためのユーザーおよびパスワードでログインします:
2017032201


オフラインアクセスのための許可が求められるので「許可」をクリック:
2017032202


以下のようなメッセージが表示されれば OAuth 認証完了です:
2017032203


再びターミナル画面に戻り、今度はマウントポイントを指定して google-drive-ocamlfuse を実行します。これで OAuth 認証時に使ったユーザーの Google ドライブが指定ディレクトリにマウントされます:
$ google-drive-ocamlfuse ~/googledrive


この状態で df -h コマンドを実行すると、指定したディレクトリに Google Drive がマウントされていることが確認できます:
2017032204


ls コマンドなどでこのディレクトリ内を確認すると、Google Drive 内のドキュメントが odt 等のフォーマットで存在していることを確認できます:
2017032205


アンマウントする場合は fusermount コマンドを -u オプションを付けて(アンマウントポイントを指定して)実行します:
$ fusermount -u ~/googledrive


Google ドライブがマウントできると Boostnote のドキュメント共有が異なるシステム間でも可能になったりできて、ますます便利です。



(参考)
http://o2t.hatenablog.com/entry/2014/09/08/143621
 

Java で(Web)アプリケーションから REST API を実行する時など、HTTP のクライアント機能を java.net.* から作るのは面倒です。現実的にはなんらかのライブラリを使うことになると思います。

そんな場合によく使われるのが Apache HTTP Client だと思ってます。2017/Mar/16 現在の最新バージョンは 4.5.3 でした。モジュールはこちらからダウンロードできます:
https://hc.apache.org/downloads.cgi

2017031501


上記サイトの HttpClient カテゴリから 4.5.3.zip と書かれたリンクをクリックすると 4.5.3 のバイナリが zip アーカイブとして取得できます(ファイル名は httpcomponents-client-4.5.3-bin.zip)。ダウンロードした zip ファイルを展開し、lib フォルダから jar ファイル群を取り出します(今回のサンプルで最低限必要なのは以下の6ファイルです):
  • commons-codec-1.4.jar
  • commons-logging-1.2.jar
  • httpclient-4.5.3.jar
  • httpclient-cache-4.5.3.jar
  • httpcore-4.4.6.jar
  • httpmime-4.5.3.jar

Eclipse 等で Java のウェブアプリケーションプロジェクトを作成し、lib フォルダ(Webcontent/WEB-INF/lib など)に上記作業で取り出した jar ファイル群をまとめてコピーしておきます。これで準備完了:
2017031502


では実際にこれらのモジュールを使って HTTP アクセスを実現するプログラムを書いて実行してみます。今回はスタンドアロンに HTTP GET を実行する、こんなプログラムにしてみます:
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClient1 {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    String url = "https://www.ibm.com/developerworks/jp/"; //. HTTP GET する URL

    try{
      CloseableHttpClient client = HttpClients.createDefault();
      HttpGet get = new HttpGet( url );
      CloseableHttpResponse response = client.execute( get );
      int sc = response.getStatusLine().getStatusCode(); //. 200 の想定
      HttpEntity entity = response.getEntity();
      String html = EntityUtils.toString( entity, "UTF-8" );
      System.out.println( html ); //. 取得結果をコンソールへ
      client.close();
    }catch( Exception e ){
      e.printStackTrace();
    }
  }
}

指定した URL(上記の場合は https://www.ibm.com/developerworks/jp/")に HTTP でアクセスして、GET した結果をコンソールに出力する、というものです。この内容を記述したファイル(HttpClient1.java)を Eclipse から実行します:
2017031503


で、指定した URL  の HTML が取得できることを確認します。HTTP GET は呼び出すだけなのでシンプルですね:
2017031504


アクセス先として HTML のようなテキストではなく、画像のようなバイナリデータの場合は以下のように byte 配列として結果を取得します(HTTP リクエストヘッダを設定する例も加えています):
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClient2 {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    String url = "https://dw1.s81c.com/developerworks/i/f-ii-ibmbluemix.png"; //. HTTP GET する URL

    try{
      CloseableHttpClient client = HttpClients.createDefault();
      HttpGet get = new HttpGet( url );
      get.addHeader( "User-Agent", "MyBot/1.0" );  //. HTTP リクエストヘッダの設定
      CloseableHttpResponse response = client.execute( get );
      int sc = response.getStatusLine().getStatusCode(); //. 200 の想定
      HttpEntity entity = response.getEntity();
      byte[] img = EntityUtils.toByteArray( entity );
      System.out.println( "" + img.length ); //. 取得結果をコンソールへ
      client.close();
    }catch( Exception e ){
      e.printStackTrace();
    }
  }
}

一方、HTTP POST の場合も同様ですが、GET の時との違いとしてポストデータを送信する必要もあります。以下はテキスト情報とファイルのアップロードを同時に(Multipart で)送信する場合の例です:
import java.io.File;
import java.io.FileInputStream;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClient3 {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    String url = "https://xxx.com/posturl"; //. HTTP POST する URL

    try{
      CloseableHttpClient client = HttpClients.createDefault();
      HttpPost post = new HttpPost( url );

//. 文字情報2つとファイル1つをポスト MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.addTextBody( "name", "K.Kimura", ContentType.TEXT_PLAIN ); builder.addTextBody( "email", "dotnsf@jp.ibm.com", ContentType.TEXT_PLAIN ); File f = new File( "./logo.png" ); builder.addBinaryBody( "image_file", new FileInputStream( f ), ContentType.APPLICATION_OCTET_STREAM, f.getName() ); HttpEntity multipart = builder.build(); post.setEntity( multipart ); CloseableHttpResponse response = client.execute( post ); int sc = response.getStatusLine().getStatusCode(); //. 200 の想定 HttpEntity entity = response.getEntity(); String html = EntityUtils.toString( entity, "UTF-8" ); System.out.println( html ); //. 取得結果をコンソールへ client.close(); }catch( Exception e ){ e.printStackTrace(); } } }

PUT や DELETE の場合も同様に。

Node.js で画像を扱う際に使える便利なミドルウェアモジュールの1つが easyimage です。内部的に ImageMagickを使うので別途導入が必要ですが、このモジュールを使うことで画像のリサイズや切り取りといった処理が簡単にできるようになります。以前にこのブログでも紹介した IBM Watson の類似画像検索サンプルの中でも使っています。


ところが、最近このモジュールを使って画像のリサイズを行っている中で不思議な挙動に遭遇することが何度かありました。現象の特徴はこんな感じでした:
  1. resize() メソッドを実行するとエラーが発生する
  2. エラーメッセージは [Error: File not supported.]
  3. このエラーが発生しても、リサイズ処理そのものは成功している
  4. 以前はこのようなエラーは発生しなかった
    • Node.js や easyimage モジュールのバージョンは以前から同じ
    • 以前は Linux で開発しており、その時はこのエラーには遭遇しなかった。このエラーが発生しているのは Windows 環境

要はリサイズ処理そのものは成功するのですが、Windows だとエラーが発生する(Linux だとしない)という、なんかややこしそうな、嫌な感じの特徴です。しかも Node.js なので非同期にこのリサイズ処理が実行されるため、
var easyimg = require( 'easyimage' );
  :

var option = { src: 'images/src.png', dst: 'images/dst.png', width: 800 };
easyimg.resize( option ).then(
  function( file ){
    :  // 成功した場合の処理
  },
  function( err ){
    :  // エラーになった場合の処理
  }
);

というコードになっています。そして Linux 環境で実行すると成功した場合の処理が行われ、Windows環境だとエラーになった場合の処理で 'File not supported' なエラーが発生するのですが、実際にはどちらもリサイズされた結果が images/dst.png に作られている、という状況になります。ややこしいでしょ?

最初は自分のコードのミスを疑っていたのですが、調べてみるとこの現象は Open Issue として公開されている(要するに障害としては認識されている)ことがわかりました:
https://github.com/hacksparrow/node-easyimage/issues/27


とはいえ、現時点で有効な修正が提供されているわけではないため、Linux でも Windows でも動くようにワークアラウンドを考える必要があります。自分の場合はシンプルに「成功したら普通に処理、エラーが発生しても無視して処理」するようにしました:
var easyimg = require( 'easyimage' );
  :

var option = { src: 'images/src.png', dst: 'images/dst.png', width: 800 };
easyimg.resize( option ).then(
  function( file ){
    :  // 成功した場合の処理
  },
  function( err ){
    :  // エラーになった場合
 (エラーを無視して成功した場合と同じ処理を実行)
  }
);

もう少し便利な方法ないかなと思いつつ、早くこの障害が直ることを期待しつつ、とりあえずこの方法で回避しています。

マンホールマップに「スマホからもっと簡単に投稿できるようにしたい」という要望に応える新機能を用意しました。具体的には Twitter から投稿可能にしました。というわけで、以下の機能を使う前提として、スマホに Twitter アプリが導入されている必要があります。


また、この機能を使うには、Twitter で @Manholemap_Bot をフォローしてください( #manhotalk_bot と似ていてややこしいですが間違えないでくださいw)。この機能のために作成した新しいボットのアカウントです:
2017030801


試しに三鷹のこのマンホールを投稿してみることにします。この画像がスマホの中に保存されているものとします:
mitaka


お持ちの各種スマホ(やPC)から、フォローした上記アカウントへのメンションでマンホール画像を送付してください。メンションとはメッセージの頭に @Manholemap_Bot (大文字小文字は区別しないので、全て小文字でもOKです)を付けて、画像を添付して投稿してください:
IMG_0365


基本的にスマホ側での作業はこれだけで投稿できます。以下はPCでの作業を想定しています。少し(最大5分)待つと、投稿した画像がマンホールマップに反映されます:
2017030802


投稿した本人(と同じ Twitter アカウントでログインした状態)がその画像ページにアクセスした場合は編集ボタンが表示され、投稿の編集が可能になります:
2017030803


位置やテキストなど、必要に応じて編集して、最後に「更新」します:
2017030804


残念ながらまだいくつかの制約事項があります:
(1) テキストを同時にツイートできない
(2) 元の画像に位置情報が含まれていても反映されない(Twitter の仕様)

色々調査しながらにはなりますが、今後のアップデートで少しずつ便利にしていくつもりです。


なお、この機能はアプリケーション開発者向けに公開しているマンホールマップ API を使って作成したものです。誰でも使えるものなので、興味をお持ちの方はこちらから仕様書をどうぞ:
http://manholemap.juge.me/dev.jsp



Tomcat やら Jetty やらといった Java アプリケーションコンテナ(Java アプリケーションサーバー)の種類に依存しない形でユーザー認証を実現するサンプル を作ってみました。実装にはフィルタを使います。また今回は認証の種類に Basic 認証を使っています。


まずは以下のような javax.servlet.Filter インターフェースの実装となるクラス: BasicAuthenticationFilter を作ります:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sun.xml.internal.messaging.saaj.packaging.mime.internet.MimeUtility;

public class BasicAuthenticationFilter implements Filter{
  //. レルム名
  private final String realmName = "myRealm";


  //. Filter の実装に必要なメソッド(何もしない)
  public void init( FilterConfig config ) throws ServletException{
  }
  public void destroy(){
  }

  //. フィルタリング処理の実装
  public void doFilter( ServletRequest req, ServletResponse res, FilterChain filterChain ) throws IOException, ServletException{
    ByteArrayInputStream bin = null;
    BufferedReader br = null;

    boolean isAuthorized = false; //. この値で認証の可否を判断する
    try{
      HttpServletRequest httpReq = ( HttpServletRequest )req;
      String basicAuthData = httpReq.getHeader( "authorization" );
      if( basicAuthData != null && basicAuthData.length() > 6 ){
        //. Basic認証から情報を取得
        String basicAuthBody = basicAuthData.substring( 6 ); //. 'Basic dG9tY2F0OnRvbWNhdA== ' 

        //. BASE64 デコード
        bin = new ByteArrayInputStream( basicAuthBody.getBytes() ); 
        br = new BufferedReader( new InputStreamReader( MimeUtility.decode( bin, "base64" ) ) );
        StringBuilder buf = new StringBuilder();
        String line = null;
        while ( ( line = br.readLine() )!=null ) {
          buf.append( line );
        }

        //. 入力された username と password を取り出す
        String[] loginInfo = buf.toString().split( ":" );
        String username = loginInfo[0];
        String password = loginInfo[1];
//.     System.out.println( "Basic " + username + ":" + password );

        //. 取り出した username と password で認証可否を判断する

        //. 実際にはここで LDAP やユーザー情報データベースと比較して判断することになる
        isAuthorized = true; //. 今回の例ではとりあえず何かが入力されていれば認証 OK とする
      }

      if( !isAuthorized ){
        //. (認証に何も指定されていなかった場合も含めて)認証 NG だった場合はブラウザに UnAuthorized エラー(401)を返す
        HttpServletResponse httpRes = ( HttpServletResponse )res;
        httpRes.setHeader( "WWW-Authenticate", "Basic realm=" + this.realmName );
        httpRes.setContentType( "text/html" );
        httpRes.sendError( HttpServletResponse.SC_UNAUTHORIZED ); //. 401

        //. 最初に認証なしでアクセスした場合はここを通るので、その結果ブラウザが認証ダイアログを出す、という流れ
      }else{
    	//. 認証 OK だった場合はそのまま処理を続ける
        filterChain.doFilter( req, res );
      }
    }catch( Exception e ){
      throw new ServletException( e );
    }finally{
      //. ストリームのクローズ
      try{
        if( bin!=null ) bin.close();
        if( br !=null ) br.close();
      }catch( Exception e ){
      }
    }
  }
}



また、web.xml の 内に以下の <filter> と <filter-mapping> の記述(青字部分)を追加します。この例では全ての URL (/*) に対して認証をかけるよう指定して います:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 

xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 

http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>BasicAuth</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>

  :
  :

  <!-- Filter Configuration -->
  <filter>
    <filter-name>basicAuthFilter</filter-name>
    <filter-class>me.juge.basicauth.BasicAuthenticationFilter</filter-class>
  </filter>

  <!-- Filter Mapping -->
  <filter-mapping>
    <filter-name>basicAuthFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>



後は確認用の index.html として適当な内容のものを用意します:
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>ハローワールド!</h1>
</body>
</html>
↑超適当!


こうして作成した Java アプリケーションを動かして、ウェブブラウザからコンテキストルート(/) にアクセスすると、(初回は認証情報を付けずにアクセスする ので)作成したフィルタから 401 が返され、結果以下のような認証ダイアログが表示されるはずです:
2017031101


ここに適当な内容の文字列を入力して再度アクセスすると、(今度は何かが入っていたことになるので)上記サンプルでは認証 OK という判断になり、用意した index.html が表示される、という流れが実現できます:
2017031102



今回のサンプルはこちらに公開します:
https://github.com/dotnsf/BasicAuth












 

このページのトップヘ