素のPHPでさくらのオブジェクトストレージをlistobject

前回AWS-SDKを使ったが、素のPHPだとどうなるか試した。

やったこと

  • Fiddlerでリクエストを確認
  • AWS 署名V2を調べた
  • 素のPHPでListObject

Fiddlerでリクエストを確認

FiddlerでCloudBerryのリクエストをとった。

GET /<<バケット>>?prefix=&max-keys=1000&delimiter=%2F HTTP/1.1
User-Agent: CloudBerryLab.Base.HttpUtil.Client 4.3.0 (
http://www.cloudberrylab.com/)
x-amz-date: Fri, 20 Nov 2015 12:21:06 GMT
Authorization: AWS <<アクセスキー>>:xxxxxxxxxxxxxxxxxxxxxxxxxxx=
Host: b.sakurastorage.jp
Connection: Keep-Alive

アクセスキーの右ブロックは

  • 同じリクエストでも毎回変わることから時間が関係してるっぽい
  • 最後が「=(イコール)」で終わるのでBASE64っぽい

ってことが分かった。

AWS 署名V2を調べた

アクセスキーの右ブロックの文字列は「AWS 署名バージョン 2」と呼ぶようだ。 さくらのオブジェクトストレージは、AWSSDKv2は成功するがv3は失敗するので、 v2をつかうといいようだ。

V2署名文字列生成は↓が分かり易かったです。

付録 B: リクエストの認証(AWS 署名バージョン 2) https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/auth-request-sig-v2.html

PHPAmazon S3REST API を使用 #1 http://www.applelife100.com/2012/06/23/using-rest-api-of-amazon-s3-in-php-1/

素のPHPでListObject

こんな感じになりました。

<?php
$config = [
  'bucket' => '<<バケット>>',
  'accessKey' => '<<アクセスキー>>',
  'secretAccessKey' => '<<シークレットアクセスキー>>',
  'endpoint' => 'b.sakurastorage.jp',
];
$resource = "/{$config['bucket']}";

$req = makeRequest($config, $resource);
$res = file_get_contents($req['url'], false, $req['context']);
echo $res;

function makeRequest($config, $resource)
{
  $datetime = new DateTime('now', new DateTimeZone('UTC'));
  $date = $datetime->format(DateTime::RFC1123);
  $signature = v2signature($config, "GET", '', '', $datetime, '',
$resource);
  return [
    'context' => stream_context_create([
      "http" => [
        'method' => 'GET',
        'header' => implode("\r\n", [
          "Authorization: AWS {$config['accessKey']}:{$signature}",
          "Date: {$date}",
        ]),
      ],
    ]),
    'url' => "https://{$config['endpoint']}{$resource}",
  ];
}

function v2signature($config, $httpVerb, $contentMd5, $contentType,
$datetime, $canonicalizedAmzHeaders, $resource)
{
  $stringToSign =
    $httpVerb ."\n"
    . $contentMd5 ."\n"
    . $contentType ."\n"
    . $datetime->format(DateTime::RFC1123) ."\n"
    . $canonicalizedAmzHeaders
    . $resource;
  return base64_encode(hash_hmac('sha1', $stringToSign,
$config['secretAccessKey'], true));
}

参考

www.applelife100.com

docs.aws.amazon.com

さくらのオブジェクトストレージをAWS SDK for PHP 2で

PHPからさくらのオブジェクトストレージを利用したくなり、AWS SDKを使えるか試したときのメモ。 AWS SDK for PHP v2 を使った。

list objects

composer.json

{
    "require": {
        "aws/aws-sdk-php": "2.*"
    }
}

list_objects.php

<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;

$client = S3Client::factory([
    'key'    => '<<アクセスキー>>',
    'secret' => '<<シークレットキー>>',
    'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの
]);

$iterator = $client->getIterator('ListObjects', [
    'Bucket' => '<<バケット名>>',
]);

foreach ($iterator as $object) {
    echo $object['Key'] . "\n";
}

put object

put_object.php

<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;

$client = S3Client::factory([
    'key'    => '<<アクセスキー>>',
    'secret' => '<<シークレットキー>>',
    'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの
]);

$iterator = $client->getIterator('ListObjects', [
    'Bucket' => '<<バケット名>>',
]);

foreach ($iterator as $object) {
    echo $object['Key'] . "\n";
}
</pre>

get object

get_object.php

<?php
require 'vendor/autoload.php';

use Aws\S3\S3Client;

$client = S3Client::factory([
    'key'    => '<<アクセスキー>>',
    'secret' => '<<シークレットキー>>',
    'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの
]);

$result = $client->getObject([
    'Bucket' => '<<バケット名>>',
    'Key'    => 'data.txt'
]);

echo get_class($result['Body']) . "\n";
echo $result['Body'] . "\n";

delete object

delete_object.php

<?php
require 'vendor/autoload.php';

use Aws\S3\S3Client;

$client = S3Client::factory([
    'key'    => '<<アクセスキー>>',
    'secret' => '<<シークレットキー>>',
    'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの
]);

$result = $client->deleteObject([
    'Bucket' => '<<バケット名>>',
    'Key'    => 'data.txt',
]);

var_dump($result);

参考

http://aws.amazon.com/jp/developers/getting-started/php/ http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.S3.S3Client.html http://cloud-news.sakura.ad.jp/wp-content/uploads/2015/01/ObjectStorage_API.pdf

Beckyのフォルダ名変更はどうなんだろう

Beckyでアクティブフォルダをクリックすると、フォルダ名変更になる。
気付かずスペース(未読メール読み)をタイプすると、フォルダ名を喪失してしまい、アンインストールを考える。

ちげーよ変更したいんじゃない、選択中のフォルダを再クリックしただけなんだ。

イライラしつつ、ふと思った。これ AutoHotkey でキャンセルできないか。

;; ### Becky ###
;; マウス左クリックによるフォルダ名変更をキャンセルする - とりあえずver
#IfWinActive, ahk_class Becky2MainFrame
~LButton::
  Sleep 700
  MouseGetPos, ,,, contrl
  IfEqual, contrl, Edit1
    Send, {Esc}
  return
#IfWinActive

これで改善するか様子を見ます。

しかし、いまどきツリー上でのフォルダ名変更はF2じゃないのか。
左クリックでフォルダ名変更は軽々しくないか。

3項演算子が読みにくいとか難しいとか

3行で書けばいいじゃない。

String s = (condition) 
        ? "hoge" 
        : "piyo"; 

PlayFramework2.1.3(Java)+JPA 使ってみる #3

play2.1(Java)でJPAを使ってみたメモの続き。

UnitTestできるようになったので、UnitTestを書いてみる。DDLJPAにやらせるので、evolution はパスで。

フィスクチャ注入

play1の play.test.Fixtures みたいのは Ebeanにはある。junit 上で投入してないけど。サンプル中のフィクスチャ投入コードがこれ。

Global.java - samples/java/zentasks

public class Global extends GlobalSettings {
    public void onStart(Application app) {
        InitialData.insert(app);
    }

    static class InitialData {
        public static void insert(Application app) {
            if(Ebean.find(User.class).findRowCount() == 0) {

                Map<String,List<Object>> all = (Map<String,List<Object>>)Yaml.load("initial-data.yml");

                // Insert users first
                Ebean.save(all.get("users"));

                // Insert projects
                Ebean.save(all.get("projects"));
                for(Object project: all.get("projects")) {
                    // Insert the project/user relation
                    Ebean.saveManyToManyAssociations(project, "members");
                }

                // Insert tasks
                Ebean.save(all.get("tasks"));
            }
        }
    }
}

おっと Global.java は GlobalSettings を継承して onStart フックし初期データを注入してる。play1 の @OnApplicationStart はこうなるのか。GlobalSettings のオーバーライドは ここ(インターセプター)で説明されてる。
h2:mem を使っているうちは、データが揮発してしまうので、onStart で注入するのもありだな。

JPAでは、サンプルのようにEBeanのフィスクチャ注入ができないので、EntityManager.createNativeQuery でやってみる。

とりあえずテスト書く

public class ModelTest
{
    @Test
    public void findByIdTest()
    {
        running(fakeApplication(), new Runnable() {
            public void run() {
                JPA.withTransaction(new play.libs.F.Callback0() {
                    public void invoke() {

                        String sql1 = "delete from company";
                        JPA.em().createNativeQuery(sql1).executeUpdate();
                        String sql2 = "insert into company (id,name) values (1,'Apple Inc.')";
                        JPA.em().createNativeQuery(sql2).executeUpdate();

                        Company apple = Company.findById(1L);

                        assertThat(apple).isNotNull();
                        assertThat(apple.name).isEqualTo("Apple Inc.");

                    }
                });
            }
         });
    }
}

これはひどい。

書き直し

ここを参考に書き直し。
SQLクエリ生成を EntityManager.createNativeQuery から DB.getConnection().createStatement() にしてみた。
DB.getConnection().createStatement() だとパラメータを渡せない(?)から使えないけど、フィクスチャ投入くらいになら使えるね。

package models;

import static org.fest.assertions.Assertions.*;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;

public class ModelTest extends utils.UnitTest
{
    @Override
    public List<String> getFixtureSource()
    {
        return Arrays.asList(
                "delete from company",
                "insert into company (id,name) values (1,'Apple Inc.')"
                );
    }

    @Test
    public void findByIdTest()
    {
        Company apple = Company.findById(1L);

        assertThat(apple).isNotNull();
        assertThat(apple.name).isEqualTo("Apple Inc.");
    }
}
package utils;

import java.sql.SQLException;
import java.util.Collections;
import java.util.List;

import org.junit.After;
import org.junit.Before;

import play.Logger;
import play.db.DB;
import play.db.jpa.JPA;
import play.db.jpa.JPAPlugin;
import play.test.FakeApplication;
import play.test.Helpers;
import scala.Option;

public abstract class UnitTest
{
    protected FakeApplication app;
    protected javax.persistence.EntityManager em;

    @Before
    public void setUp()
    {
        FakeApplication app = Helpers.fakeApplication();
        Helpers.start(app);

        Option<JPAPlugin> jpaPlugin = app.getWrappedApplication().plugin(JPAPlugin.class);
        em = jpaPlugin.get().em("default");
        JPA.bindForCurrentThread(em);
        em.getTransaction().begin();

        loadFixtures();
    }

    @After
    public void tearDown()
    {
        em.getTransaction().commit();
        em.close();
        JPA.bindForCurrentThread(null);
        Helpers.stop(app);
    }

    protected void loadFixtures()
    {
        for (String sql : getFixtureSource()) {
            execSql(sql);
        }
    }

    protected List<String> getFixtureSource()
    {
        return Collections.emptyList();
    }

    protected void execSql(String sql)
    {
        try {
            DB.getConnection().createStatement().execute(sql);
        } catch (SQLException e) {
            Logger.error("execsql failed:" + sql, e);
        }
    }
}

ましになった。fakeApplication とか JPA.withTransaction とか書かなきゃいけないのは面倒だな。
Fixture の置き場所どうしようか。confディレクトリ下なんだっけ?

PlayFramework2.1.3(Java) テスト0件問題を回避

play2.1.3(java)で UnitTestができない問題。

とのことですた。

MLからパッチをいただく

Play 2.1.3 released - groups.google.com play-framework より

testOptions in Test ~= { args =>
  for {
    arg <- args
    val ta: Tests.Argument = arg.asInstanceOf[Tests.Argument]
    val newArg = if(ta.framework == Some(TestFrameworks.JUnit)) ta.copy(args = List.empty[String]) else ta
  } yield newArg
}

パッチあて

どこにあてんの?と思ったら project/Build.scala でした。

パッチをあてた samples/java/computer-database-jpa の project/Build.scala

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

    val appName         = "computer-database-jpa"
    val appVersion      = "1.0"

    val appDependencies = Seq(
      javaCore,
      javaJdbc,
      javaJpa,
      "org.hibernate" % "hibernate-entitymanager" % "3.6.9.Final"
    )

    val main = play.Project(appName, appVersion, appDependencies).settings(
      ebeanEnabled := false,

      // PATCH : JUnit tests in Play do not execute
      testOptions in Test ~= { args =>
        for {
          arg <- args
          val ta: Tests.Argument = arg.asInstanceOf[Tests.Argument]
          val newArg = if(ta.framework == Some(TestFrameworks.JUnit)) ta.copy(args = List.empty[String]) else ta
        } yield newArg
      }
    )
}

テスト結果

[computer-database-jpa] $ test
[info] play - datasource [jdbc:h2:mem:play-test--899905098;] bound to JNDI as DefaultDS
[info] IntegrationTest
[info] + IntegrationTest.test
[info]
[info]
[info] Total for test IntegrationTest
[info] Finished in 0.018 seconds
[info] 1 tests, 0 failures, 0 errors
[info] play - datasource [jdbc:h2:mem:play] bound to JNDI as DefaultDS
[info] play - datasource [jdbc:h2:mem:play] bound to JNDI as DefaultDS
[info] play - datasource [jdbc:h2:mem:play] bound to JNDI as DefaultDS
[info] play - datasource [jdbc:h2:mem:play] bound to JNDI as DefaultDS
[info] FunctionalTest
[info] + FunctionalTest.filterComputerByName
[info] + FunctionalTest.redirectHomePage
[info] + FunctionalTest.createANewComputer
[info] + FunctionalTest.listComputersOnTheFirstPage
[info]
[info]
[info] Total for test FunctionalTest
[info] Finished in 0.01 seconds
[info] 4 tests, 0 failures, 0 errors
[info] play - datasource [jdbc:h2:mem:play] bound to JNDI as DefaultDS
[info] play - datasource [jdbc:h2:mem:play-test--1376794684;] bound to JNDI as DefaultDS
[info] ModelTest
[info] + ModelTest.findById
[info] + ModelTest.pagination
[info]
[info]
[info] Total for test ModelTest
[info] Finished in 0.004 seconds
[info] 2 tests, 0 failures, 0 errors
[info] Passed: : Total 7, Failed 0, Errors 0, Passed 7, Skipped 0
[success] Total time: 12 s, completed 2013/08/15 21:17:30

無事テストできますた。

参考

Play 2.1.3 released - groups.google.com play-framework

SBT セッティングについて - playframework-ja.org
ここの「デフォルト値の Play プロジェクトセッティング」を読んでおくといいかもしれない。scalaを読めるようになっておくといいかもしれない。最近書いてないのでまた読めなくなってきた。

PlayFramework2.1.3(Java)+JPA 使ってみる #2

play2.1(Java)でJPAを使ってみたメモの続き。

コントローラーから検証#1

UnitTestをあきらめて、コントローラーからDBアクセスを検証してみる。

app/controllers/Application.java

public class Application extends Controller {
    public static Result tests()
    {
        Company company = Company.findById(1L);
        if (company == null) {
            return notFound();
        }
        return ok(company.name);
    }
}

conf/routes

GET     /tests                       controllers.Application.tests()

play1のような感覚で実装できた。コントローラーメソッドの戻り値が void から Result に、Result は Exceptionでなくなった。
play1 の throw result は面白かったけど throw this のような違和感があった。独自Resultクラスで throw this と書いたことがあります。やめたけど。

http://localhost:9000/tests にアクセスすると、

[RuntimeException: No EntityManager bound to this thread. Try to annotate your action method with @play.db.jpa.Transactional]

と表示された。親切なメッセージだなあ。

testsアクションを @play.db.jpa.Transactional でアノテートとする。これはドキュメントにも載っている通り。2.3のころにはアノテートしなくてもよくなるかな。

アノテートして http://localhost:9000/tests へ。

[PersistenceException: org.hibernate.exception.SQLGrammarException: could not load an entity: [models.Company#1]]

h2データベースに接続したみたいだ。SQLGrammarException は create table も insert もしていないので当然か。

コントローラーから検証#2

コントローラーでDDLとテストデータを投入してみる。JPA.em() で EntityManager 取得できるのは play1と同じ感覚。

    @play.db.jpa.Transactional
    public static Result tests() {
        String sql1 = "create table company ( "
                + "id bigint not null, "
                + "name varchar(255), "
                + "constraint pk_company primary key (id))";
        String sql2 = "insert into company (id,name) values (  1,'Apple Inc.')";
        JPA.em().createNativeQuery(sql1).executeUpdate();
        JPA.em().createNativeQuery(sql2).executeUpdate();

        Company company = Company.findById(1L);
        if (company == null) {
            return notFound();
        }
        return ok(company.name);
    }

コントローラーでテストデータ投入#2

    @play.db.jpa.Transactional
    public static Result tests()
    {
        Company c = new Company();
        c.id = 2L;
        c.name = "asdf";
        c.save();

        Company company = Company.findById(2L);
        if (company == null) {
            return notFound();
        }
        return ok(company.name);
    }

DDL自動生成

play1 にあった jpa.ddl=create-drop をやりたい。
conf/META-INF/persistence.xml に次を加えたらできた。

        <properties>
            <property name="hibernate.hbm2ddl.auto" value="create-drop"/>    <--- 追加
        </properties>

DDL自動生成ができると、おおよそのモデルができるまで evolution 作成を先延ばしにできるので捗る。