CarrierWave + seed

こんにちは。
麺処まつば副店長です。
今日も真面目に勤務しております。(店長へのアピール)

さて、本日は前回の予告通り
CarrierWaveについて、ボソボソと書いてみようと思います。

まず、CarrierWave?なにそれ美味しいの?ていう方はコチラへどうぞ
<CarrierWave>
https://github.com/jnicklas/carrierwave
<RailsCastによる分かりやすい説明>
http://railscasts.com/episodes/253-carrierwave-file-uploads?language=ja&view=asciicast


CarrierWaveの使い方等は既に分かりやすい記事等沢山あると思いますので…
今回は、CarrierWaveを適用したモデルのデータを
seedを使って投入する方法をまとめてみようと思います。

今回のディレクトリの構造はこんな感じです。

[RAILS_ROOT]
  ┣ app
  ┃ ┣ models
  ┃ ┃  ┗ soba.rb
  ┃ ┗ uploaders
  ┃     ┗ soba_image_uploader.rb
  ┃
  ┗ db
     ┣ data
     ┃  ┣ zarusoba.jpg
     ┃  ┣ morisoba.jpg
     ┃  ┗ tempurasoba.jpg
     ┗ seeds.rb

sobaモデルというのがあって、
その中でsoba_image_uploaderなるアップローダを使うよう指定をしています。
そして、db/dataの中に画像ファイルを3枚と、db/seeds.rbを用意します。

app/models/soba.rb

  1 # -*- encoding: UTF-8 -*-
  2 class Soba < ActiveRecord::Base
  3
  4   mount_uploader :image, SobaImageUploader
  5

上記のように、sobas.imageでUploaderを使うよう指定しています。

sobasテーブル

また、sobasテーブルは下記のような構成になっているとします。

mysql> desc sobas;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(11)      | NO   | PRI | NULL    | auto_increment |
| name         | varchar(255) | NO   |     | NULL    |                |
| image        | varchar(255) | NO   |     | NULL    |                |
| created_at   | datetime     | NO   |     | NULL    |                |
| updated_at   | datetime     | NO   |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+

db/seeds.rb

sobasテーブルに値を放り込むために、下記のようなseeds.rbを書きます。

 1 # -*- encoding: UTF-8 -*-   
 2 Soba.find_or_create_by_name(name: 'zarusoba', image: open("#{Rails.root}/db/data/zarusoba.jpg"))
 3 Soba.find_or_create_by_name(name: 'morisoba', image: open("#{Rails.root}/db/data/morisoba.jpg"))
 4 Soba.find_or_create_by_name(name: 'tempurasoba', image: open("#{Rails.root}/db/data/tempurasoba.jpg"))

この状態で、下記コマンドをたたきます。

$ rake db:seed

…とすると、ファイルも一緒に保存ができました。
いやぁ便利ですね。
これで当店のメニューも簡単に生成できます…!(ちらっちらっ>店長)


次回もCarrierWaveのネタをボソボソと書く予定です。
(…しばらく続く気がします…)

お引越ししました…(ぼそり)

あ…あけましておめでとうございます…麺処まつば副店長です(ぼそぼそ)

なんと昨年は記事を1つしか書いていないという偉業を達成してしまいました…!
今年はもうちょっと何とかせねばと思いたちまして
その先駆けとして、はてなダイアリーからはてな日記へお引越しなど致しました。

文字サイズ等、まだ調整できてない部分もあり、
見づらいトコロもあるかと思いますが…都度修正していく心づもりでおります。

ネタもそこそこ溜まってきておりますし、
ぼちぼち情報を整理しつつ、記事に出来たらなとおもっております。

目指せ記事数5!(少なっ)

ということで、次回、先日まで格闘しておりました
CarrierWaveの情報などボソボソと書いてみようと思っております。m(_ _)m

CakePHP + mroonga

麺処まつば副店長です。
あけましておめでとうございます。(遅)
年末年始から今まで、当店はモンハン一色でございます。
店長も副店長も働きもせずモンハンばっかりやっています。(半ば開き直りながら)

店長が遊んでいる=自分も遊んでいい。とか思ってダラダラしていたら
ついに(自分だって遊んでばっかりの)店長に「そろそろ書け」とか言われました…。

なんかネタあったっけな……と(遊んでばっかりの)記憶を掘り起こしてみたところ、
先日別件で CakePHP全文検索したいから mroonga を動かせるようにしといて。
というお仕事があったのです。
CakePHP は、mroonga に対応してないので
オーバーライドして無理矢理対応させた時の話でもしようと思います。

まず、CakePHP?mroonga?なにそれ美味しいの?って方はコチラへ。
CakePHPhttp://cakephp.jp/
mroonga:http://mroonga.github.com/

<使用環境>
CakePHP:2.0
mroonga:1.2
mysql:5.5
CentOS 5.7

今回書くのは下記3つです。
1.cake schema create
2.cake schema generate
3.find

では書いてみます。

cake schema create に対応する。

mroonga な全文検索に対応したテーブルを作成するには、
通常下記のような SQL でテーブルを作成します。

1  CREATE TABLE diaries (
2    id INT(10) NOT NULL AUTO_INCREMENT,
3    title VARCHAR(255) NOT NULL,
4    content VARCHAR(255) NOT NULL,
5    FULLTEXT INDEX (title),
6    FULLTEXT INDEX (content),
7    PRIMARY KEY (`id`)
8  ) ENGINE = mroonga COMMENT = 'engine "innodb"'

<5、6行目>
title カラムと content カラムを FULLTEXT INDEX に対応させています。
<8行目>
エンジンに mroonga を使用する宣言をしています。
さらに、今回はラッパーモードを使用するので、
テーブルのコメントに「engine "innodb"」を入れる必要があります。

コレを実現するために、こういう schema.php を書きます。

<?php
 1 public $diaries = array(
 2   'id'      => array('type' => 'integer', 'null' => false, 'default' => NULL, 'key' => 'primary', 'collate' => NULL, 'comment' => ''),
 3   'title'   => array('type' => 'string', 'null' => true, 'default' => NULL, 'key' => 'index', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'),
 4   'content' => array('type' => 'string', 'null' => true, 'default' => NULL, 'key' => 'index', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'),
 5   'indexes' => array(
 6     'PRIMARY' => array('column' => 'id', 'unique' => 1), 
 7     'title'   => array('column' => 'title', 'unique' => 0, 'index_type' => 'FULLTEXT'),
 8     'content' => array('column' => 'content', 'unique' => 0, 'index_type' => 'FULLTEXT'),
 9   ), 
10   'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'mroonga', 'comment' => 'engine "innodb"')
11 );

<7、8行目>
FULLTEXT INDEX (title),」と「FULLTEXT INDEX (content),」に該当する行です。
<10行目>
「ENGINE = mroonga COMMENT = 'engine "innodb"'」に該当する行です。


さらにこれを、schema createで実行できるようにオーバーライドします。

まずは、ラッパーモード使用の宣言をするため、テーブルにコメントが付けられるようにします。
対象ファイル:project_name/lib/Cake/Model/Datasource/Database/Mysql.php

<?php
1 public $tableParameters = array(
2         'charset' => array('value' => 'DEFAULT CHARSET', 'quote' => false, 'join' => '=', 'column' => 'charset'),
3         'collate' => array('value' => 'COLLATE', 'quote' => false, 'join' => '=', 'column' => 'Collation'),
4         'engine' => array('value' => 'ENGINE', 'quote' => false, 'join' => '=', 'column' => 'Engine')
5         // kokokara ------------------------------------------
6         ,'comment' => array('value' => 'COMMENT', 'quote' => true, 'join' => '=', 'column' => 'Comment')
7         // kokomade ------------------------------------------
);    

6行目を追加します。
これで schema.php 中にテーブルの「comment」があった場合の処理を追加します。


続いて、FULLTEXT INDEX宣言できるようにします。
対象ファイル:project_name/app/Model/Datasource/DboSource.php
buildIndex 関数をオーバーライドします。10〜14行目追記。

<?php
 1    public function buildIndex($indexes, $table = null) {
 2      $join = array();
 3      foreach ($indexes as $name => $value) {
 4        $out = '';
 5        if ($name === 'PRIMARY') {
 6          $out .= 'PRIMARY ';
 7          $name = null;
 8        } else {
 9          // kokokara -------------------------------------------
10          if(isset($value['index_type']) && $value['index_type'] == 'FULLTEXT' ){
11            $out = 'FULLTEXT INDEX('.$value['column'].')';
12            $join[] = $out;
13            continue;
14          }
15          // kokomade -------------------------------------------
16          if (!empty($value['unique'])) {
17             $out .= 'UNIQUE ';
18          }
19          $name = $this->startQuote . $name . $this->endQuote;
20        }
          …略…

この状態で、先程の schema.php を走らせてみます。

$ app/Console/cake schema create

対象テーブルがどうなったかを確認します。

SHOW CREATE TABLE diaries;

  CREATE TABLE `diaries` (
    `id` int(10) NOT NULL AUTO_INCREMENT,
    `title` varchar(255) NOT NULL,
    `content` varchar(255) NOT NULL,
    PRIMARY KEY (`id`),
    FULLTEXT KEY `title` (`title`),
    FULLTEXT KEY `content` (`content`)
  ) ENGINE=mroonga DEFAULT CHARSET=utf8

SHOW INDEX FROM diaries;
  +---------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
  | Table   | Non_unique | Key_name    | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
  +---------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
  | diaries |          0 | PRIMARY     |            1 | id          | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
  | diaries |          1 | title       |            1 | title       | NULL      |        NULL |     NULL | NULL   |      | FULLTEXT   |         |               |
  | diaries |          1 | content     |            1 | content     | NULL      |        NULL |     NULL | NULL   |      | FULLTEXT   |         |               |
  +---------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

できたっぽいです。

cake schema generate に対応する

上記までで、schema createはできるようになったので、その逆です。
schema generate で schema.php を出力できるようにします。

→(2012/03/21)副店長の記憶が非常に曖昧なため一旦削除します。
→(2012/04/06)副店長の記憶は正しかったです。戻します。

対象ファイル:project_name/lib/Cake/Model/Datasource/Database/Mysql.php

<?php
 1      public function index($model) {
 2        $index = array();
 3        $table = $this->fullTableName($model);
 4        $old = version_compare($this->getVersion(), '4.1', '<=');
 5        if ($table) {
 6          $indices = $this->_execute('SHOW INDEX FROM ' . $table);
 7          while ($idx = $indices->fetch()) {
 8            if ($old) {
 9              $idx = (object) current((array)$idx);
10            }
11            // kokokara -------------------------------------------
12            if($idx->Index_type == "FULLTEXT"){
13                    $col = array();
14                    $index[$idx->Key_name]['column'] = $idx->Column_name;
15                    $index[$idx->Key_name]['unique'] = intval($idx->Non_unique == 0);
16                    $index[$idx->Key_name]['index_type'] = "FULLTEXT";
17                    continue;
18            }
19            // kokomade -------------------------------------------
20            if (!isset($index[$idx->Key_name]['column'])) {
22              $col = array();
2324      }

index 関数をオーバーライドします。
12〜18行目を追加。INDEXの定義に「FULLTEXT」があったら、
その内容を出力するように書き換えました。

この状態で、schema generate をすると、先程書いたような記述が schema.php に出力されます。

CakePHP から find

mroonga で全文検索する場合は、下記のような SQL を実行します。

SELECT * FROM diaries WHERE MATCH(content) AGAINST("hoge");

このようなのを CakePHP から実行します。
特にオーバーライドなど必要なく、これで検索できます。

<?php
  $this->Diary->find('all', array(
    'conditions' => array('MATCH(content) AGAINST(?)' => array("hoge"))
  ));

SQL直ならこう。

<?php
  $this->Diary->getDatasource()->fetchAll(
    'SELECT * FROM diaries WHERE MATCH(content) AGAINST(?)',
     array("hoge")
  );

こんな感じで、なんとかそれっぽく動くようになりました。
間違えてるとか、他に良い方法あれば、どなたか教えてください
m(_ _)m""


それから… mroonga のパーサを変更する場合、
インデックスのコメントにパーサを指定することが可能なんですがそれは対応してません。
もう一息なのは分かっているのですが力尽きて、my.cnf でデフォルト指定しました(笑)
誰か書いてください(ちらっちらっ>店長)

<追記>
あとこういうネタもありますが、需要があれば書きます
・mroonga インストール時、mysql 衝突事件。(CentOS5)
・cake testsuite 対応

Padrino で Hello world!

こんにちは。麺処まつば副店長です。
1ヶ月以上ぶりの更新です。…いやちょっと本業が少々立て込んでおりまして…
決して決してゼルダとかゼルダとかゼルダで遊んでたわけじゃないですよ?

さてさて、副店長、これまで sinatra を愛用していたのですが、
ちょっと前に padrino のお勉強会なぞ参加いたしまして、こりゃ便利そうだなってことで
少々いじってみました。
#万が一、他の記事の続き早く書けというお叱りがあるようでしたら
#ご意見ポストへお願い致します(深々)
#いくつかあるのは認識しています…(店長の目がコワイ)

まず、padrino って何?ってお客様はコチラへどうぞ
http://jp.padrinorb.com/

僭越ながら、副店長なりに噛み砕いて申し上げますと…
sinatra って DB や view を使いたかったらライブラリなり何なり自分で入れる必要があるのですが、
padrino ではそれらが最初から色々用意されています。
sinatra には無い便利な機能が色々ついていて rails に近いけど、
そこまで大げさじゃなくていいんだけど…でも楽したい…といったとき便利かと思います(笑)

それでは、一番最初なのでまずは恒例の「Hello world!」をやってみたいと思います。
…が、「趣味は変なものづくり」と公言している副店長としては
普通の「Hello World!」をつくるわけにはいかないので
Hello World!」風味のちょっと変なこんなものを作ります。

画面を表示して……

「はろーわ…」をクリックすると………

ややツンデレめいたセリフと、肩透かし食らった回数を表示。

うわーホントしょうもないですねー。
でもやります(笑)


手順はこんなカンジです。
1. インストール
2. プロジェクトの作成
3. DBの設定
4. model + migrate
5. controller
6. view
7. javascript
8. 実行

では作っていこうと思います。

インストール

まずはインストールしないと何もできないのでコチラを一発叩きます。

$ gem install padrino

コマンド一発で何やら沢山入ります。

プロジェクトの作成

インストールができたら、次はプロジェクトを作ります。
padrino では、プロジェクトを生成するためのコマンドも用意されてます。
プロジェクト生成時に、使うライブラリ等の指定も出来ます。
今回は shoulda、haml、sass、jqueryActiveRecordmysqlを指定してみます。
最後の「-b」オプションは、必要なgemを勝手に入れてくれるという魔法の呪文だそうです。

$ padrino g project pad_hw -t shoulda -e haml -c sass -s jquery -d activerecord -a mysql -b
$ cd pad_hw

プロジェクト「pad_hw」が出来ました。できたプロジェクトの中を覗いてみると、
app/controller や config、publicといったおなじみのディレクトリが並んでいます。

DBの設定

折角なのでDBも使ってみます。データベース接続の設定は「config/database.rb」
プロジェクト作成時にmysqlを指定したので、もう既にmysql仕様になっていました。
DB名と接続ユーザを変更して保存します。(とりあえず development だけ)

config/database.rb

(略)
 16 ActiveRecord::Base.configurations[:development] = {
 17   :adapter   => 'mysql',
 18   :encoding  => 'utf8',
 19   :reconnect => true,
 20   :database  => "pad_hw",
 21   :pool      => 5,
 22   :username  => 'pad_hw',
 23   :password  => 'pad_hw',
 24   :host      => 'localhost',
 25   #:socket    => '/tmp/mysql.sock'
 26 
 27 }
(略)

当然、DBも用意しておきます。

mysql> create database pad_hw default character set utf8;

model + migrate

続いて、モデルなど作ってみます。今回はカウントアップのデータしか持たないので、
簡単にこんなカンジのこざっぱりとしたテーブルを使ってみます。

カラム名 デフォルト
id integer NULL
cnt integer 0
created_at datetime NULL
updated_at datetime NULL

モデルをサクッと作っちゃいます。

$ padrino g model counter cnt:integer
apply orms/activerecord
apply tests/shoulda
create models/counter.rb
create test/models/counter_test.rb
create db/migrate/001_create_counters.rb

で、counter テーブルにタイムスタンプを追加します
db/migrate/001_create_counters.rb

  1 class CreateCounters < ActiveRecord::Migration
  2   def self.up
  3     create_table :counters do |t|
  4       t.integer :cnt, :default => 0
  5       t.timestamps
  6     end
  7   end
  8 
  9   def self.down
 10     drop_table :counters
 11   end
 12 end

そしたら migrate を走らせます。

$ padrino rake ar:migrate

DBを確認して、countersテーブルができていることを確認します。

mysql> desc counters;

Field Type Null Key Default Extra
id int(11) NO PRI NULL auto_increment
cnt int(11) YES 0
created_at datetime YES NULL
updated_at datetime YES NULL

controller

続いてコントローラを作成します。
とりあえず、index(get) と カウントアップ用の countup(post) の2つのメソッドをおいておきます。

$ padrino g controller counters get:index post:countup
create app/controllers/counters.rb
create app/helpers/counters_helper.rb
create app/views/counters
apply tests/shoulda
create test/app/controllers/counters_controller_test.rb

できたコントローラに手を加えます。
app/controllers/counters.rb

  1 # encoding: utf-8
  2 
  3 PadHw.controllers :counters do
  4   get :index do
  5     render 'counters/index'
  6   end
  7 
  8   post :countup do
  9     # カウントアップ
 10     c = Counter.find_or_create_by_id(1)
 11     c.cnt = (c.cnt || 0) + 1
 12     c.save
 13     return c.cnt.to_s
 14   end
 15 
 16 end

4〜6行目の index は、とりあえずページを表示するだけ、
8〜14行目の countup は、上で作ったcountersテーブルから
IDが「1」のレコードを引っ張り出してきてカウントアップして保存。
そしてカウントアップの結果を返す、という post のみ受けつけるメソッドです。( ajax で使います)

view

そして見た目に入ります。
今回は、プロジェクト作成時に宣言したハムちゃんを使います。

app/views/counters/index.haml

  1 -# encoding:utf-8
  2 !!!XML
  3 !!!
  4 
  5 %html{:lang => "ja"}
  6   %head
  7     %meta{:content=>"text/html", :charset=>"utf-8"}
  8     %title Hello World ...?
  9     %script{:type => 'text/javascript', :src => "/javascripts/jquery.js"}
 10     %script{:type => 'text/javascript', :src => "/javascripts/countup.js"}
 11   %body
 12     %div#str1
 13       %a{:href => "#", :id => "countup"} はろーわ…
 14     %div#str2

1行目は、urf-8を使います、という宣言。
コレがないと、こんなエラーが出てしまいます……

Encoding::UndefinedConversionError at /counters/
"\xE3" from ASCII-8BIT to UTF-8

9〜10行目で使う javascript のファイルを呼出し。
13行目で、javascript を呼び出すためのリンク「はろーわ…」を置いています。

本来、layout を使うものだと思うんですが
今回は Hello world で、ページ1枚しかないので使ってません。
本気の変なものつくるときには使います…。

javascript

折角なので、jquery 使ってみます。
jquery.jsはプロジェクト作成時に指定したので勝手に入ります。便利)
public/javascripts/countup.js

  1 $(function(){
  2   $("#countup").click(function() {
  3     new $.ajax({
  4       type: "POST",
  5       url: "/counters/countup",
  6       success: function(cnt){
  7                  $("#str1").text("「…ーく」とかいって。「…るど」とか言わないんだからねっっ");
  8                  $("#str2").html("<br /><br />肩透かしを食らった人ココまで " + cnt + " 人")
  9                },
 10       error: function(){alert("error");}
 11     });
 12   });
 13 });
 14 

上で作った view の「はろーわ…」のリンクをクリックするとajax でリクエストを送信します。
すると、サーバ側で counters テーブルの値を1つカウントアップ。その結果を返してきます。
javascript では、「…ーく」とかいって。「…るど」とか言わないんだからねっっ
という言葉とともに、今まで肩透かしをくらった回数を結果として表示します。

うーん我ながら実にしょうもないですね(笑)

実行

バンドルして実行です。

$ bundle install
$ padrino start

特にポート指定してないので、これでつなげます。
http://localhost:3000/

実際にリンクをポチッとしてみましょう………。

あぁぁ……しょうもない…しょうもなさすぎます…。
こんな便利なフレームワーク使って、こんなしょうもないものを…。
コレに懲りず、今後ももっとしょうもないものをつくろうと思います。

<TODO>
padrino コマンドは色々あるようなので後日(気が向いたら)まとめてみようと思います。

GitHub を使ってみる

こんにちは。麺処まつば副店長です。

この度店長より…
github ぐらい使えるようになれという業務命令が出ました…。

副店長「…分かりました、ちょっと github と格闘してきます…。」
店 長「勝つまで戦い続けなさい。」

そして…戦いの火蓋が切って落とされたのです…。

戦法はきっとこんな感じ
1.github にアカウント作成
2.key の設定
3.git の設定
4.リポジトリの作成
5.github に push

あ。本家のヘルプはこちらです
http://help.github.com/win-set-up-git/

github にアカウント作成

これが無いと何もできませんね…
さくっと会員登録してしまいましょう。
https://github.com/repository

公開鍵 の設定

github と ローカルをつなぐための公開鍵を作ります。
ローカルで作っちゃいましょう。/home/hoge/.sshとかで。

$ ssh-keygen -C "hoge@hoge.com" -t rsa

公開鍵の生成ができたらコレを github に設定します。
github の「AccountSetting」のページの左メニューに「SSH Public Keys」てのがあるので
このページの中の「Add another public key」から公開鍵を登録します。

github に公開鍵を登録したら、ローカルからつながるか試してみましょう。

$ ssh -T git@github.com

「やっほー副店長☆認証成功したよー。でもGigHubはshellのアクセスはできないからね☆」
ものすごく意訳ですが、こんな感じのメッセージがでれば成功です。

ローカルの git 設定

今度はローカル側の git の設定に入ります。
(あ。git 入ってなかったらインストールしといてくださいね。)

ユーザ名とメールアドレスを設定します。

$ git config --global user.name "Firstname Lastname"
$ git config --global user.email "your_email@youremail.com"

お名前とメールアドレスのところは適宜変えてくださいね。

あとなんか、API Token の設定とかもあるみたいですが、
今回はスキップして必要になったら登録しようと思います。

リポジトリの作成

github にログインして、リポジトリ作りましょう
ココ(→https://github.com/)の右下あたりに
「New Reposity」とかいうリンクがあるのでポチるとリポジトリの作成画面が現れます
https://github.com/repositories/new

ここで新しくリポジトリをサクサクっと作ってしまいましょう。

ファイルをpush

github で管理するファイルを用意します。
ここでは、いったん「README」を作ってそれだけ github にのせます。

$ mkdir [dir-name]
$ cd [dir-name]
$ git init
$ touch README
$ git add README
$ git commit -m 'hoge'
$ git remote add origin git@github.com:[git-account-name]/[repository-name].git
$ git push -u origin master

これで github にファイルが上がったはずです。画面から見てみましょう。
→READMEが表示されました。

今回の戦いはどうにかなったようです。

ですが、まだ勝利したわけではないのです。
次回は別のマシンからこのファイルを取得するという
第二戦に挑んでみようと思います…!

html5 の canvas + localStorage で画像保存

こんにちは。麺処まつば副店長です。

1ヶ月以上ぶりの更新です。
い…いや…、べ…別に遊んでいたわけではありませんよ…?
(店長からの視線に怯えながら)

さて。今日は HTML5 の WebStorage について少し調べてみましたので
それについてまとめてみます。

WebStorage には sessionStorage と localStorage の2つがあるようです。
sessionStorage は読んで字のごとく…そのセッションのみで有効で、
localStorageは、消さない限り残ってしまうものです。

両方とも key-value で値を保持するんだそうで。
副店長、お恥ずかしながら「それって、cookie と何が違うの?」って思ったのですが

・cookie より容量が大きい
・cookie みたいに毎回サーバに送信とかしない
・cookie と違って有効期限とかない(localStorageの場合)

とかいう違いがあるようです。
詳しくはこちらへどうぞ。→http://www.htmq.com/webstorage/

WebStorage はこんな感じで操作できるようです。

メソッド・属性 処理
length 中身のデータのサイズをゲット
key(n) n番目の key をゲット
getItem(key) key をもとに value をゲット
setItem(key, value) key と value で値をセット
removeItem(key) key をもとに データ削除
clear() 中身根こそぎ消す


それでは、今回は localStorage を使って行こうと思います。
先日作成した画像ドラッグドロップで画像保存するやつに少々付け足して
(→http://d.hatena.ne.jp/noodles_mtb/20110903/1315036154
canvasにドラッグドロップされた画像を localStorage に保存して、
さらに localStorage から画像を呼び出して canvas に表示する、
というのをやってみます。

作り方はこんなカンジです。
1.views/index.haml
2.public/javascripts/dragdrop.js
3.app.rb(コレは今回あんまり関係ないです)

views/index.haml

まずは見た目ですね。(悪いクセです)

  1 !!! XML
  2 !!!
  3 
  4 %html{:lang => "ja"}
  5   %head
  6     %meta{ :content=>"text/html", :charset=>"utf-8" }
  7     %title 画像
  8     %link{ :rel => "stylesheet", :href =>"/style.css" }
  9     %script{ :src => "javascript/jquery-1.6.2.min.js" }
 10     %script{ :src => "javascript/dragdrop.js" }
 11   %body
 12     #main
 13       %select#ls_select
 14         %option{value: ''} localStorage 
 15       %input{:type => "button",
 16              :id=>"clear_ls_button", :value => "localStorageをクリア"}
 17       %input{:type => "button",
 18              :id=>"save_ls_button", :value => "localStorageに保存",
 19              :style => "visibility: hidden;"}
 20       %br      
 21       %canvas{ :id=> "img_canvas",
 22                :width => "500", :height => "500",
 23                :ondragover => "onDragOver(event)", :ondrop => "onDrop(event)",
 24                :style => "border: medium dotted #666;"}

dragDrop で画像を受けるための canvas (21〜24行目)の他に、
localStorage に保存するためのボタン(17〜19行目)、
localStorage の中の画像選択するプルダウン(13〜14行目)、
localStorage の中身をお掃除するためのボタン(15〜16行目)
を配置しています。

public/javascripts/dragdrop.js

次は、今回の肝です。
canvas に dragDrop された画像をサーバに保存しつつ
localStorage に追加したり、取り出したり、削除したりする部分を作ります。

  1 var canvas;
  2 var context;
  3 
  4 
  5 $(function(){
  6   // canvas 初期化
  7   canvas = $("#img_canvas").get(0);
  8   context = canvas.getContext("2d");
  9   // プルダウンにローカルストレージの中身をセット
 10   for(var i=0 ; i<localStorage.length ; i++){
 11     $("#ls_select").append($('<option>').attr({value: i+1}).text("image"+String(i+1)));
 12   }
 13   // localStorageのプルダウンの値が変わったときの動作。
 14   // canvasに画像を反映する
 15   $("#ls_select").change(function(){
 16     if($(this).val().length > 0){
 17       // canvasにLocalStorageの中身を表示
 18       var img = document.createElement('img');
 19       img.src = localStorage.getItem($(this).val());
 20       img.onload = function() {
 21         canvas.width = img.width;
 22         canvas.height = img.height;
 23         context.drawImage(img, 0, 0);
 24       }
 25     }
 26   });
 27   // localStorage保存ボタンを押したときの動作。
 28   // localStorageに画像を保存して、プルダウン更新
 29   $("#save_ls_button").click(function() {
 30     canvas_data = canvas.toDataURL();
 31     key = localStorage.length+1
 32     localStorage.setItem(key, canvas_data);
 33     $("#ls_select").append($('<option>').attr({value: key}).text("image"+key));
 34     $("#ls_select").val("");
 35     alert("ローカルストレージに保存したよ。今"+(localStorage.length)+"個のデータがあるよ。");
 36   });
 37 
 38   // localStorageクリアボタンを押したときの動作。
 39   // localStorageをクリアして、プルダウン更新
 40   $("#clear_ls_button").click(function() {
 41     var l_length = localStorage.length;
 42     if(l_length > 0){
 43       localStorage.clear();
 44       $("#ls_select").children().remove();
 45       $("#ls_select").append($('<option>').attr({value: ""}).text("localStorage"));
 46       alert("ローカルストレージを削除したよ。" + l_length + "個のデータを削除したよ");
 47     }else{
 48       alert("ローカルストレージの中身、今空だよ。");
 49     }
 50   });
 51 
 52 });
 53 
 54 function onDrop(event){
 55   // drop されたファイルの取得
 56   var f = event.dataTransfer.files[0];
 57 
 58   // drop されたファイルが画像ファイルの場合の処理
 59   if(/^image/.test(f.type)){
 60     // 画像ファイルを読み込むための準備 
 61     var img = document.createElement('img');
 62     var fr = new FileReader();
 63 
 64     fr.onload = function(){
 65       img.src = fr.result;
 66       img.onload = function() {
 67         // jquery ajax を使ってPOST
 68         new $.ajax({
 69           url: "/post_img",
 70           type: "POST",
 71           data: {file: img.src},
 72           success: function(){
 73                      // canvas に画像描画
 74                      canvas.width = img.width;
 75                      canvas.height = img.height;
 76                      context.drawImage(img, 0, 0);
 77                      $("#save_ls_button").css("visibility","visible");
 78                    },
 79           error: function(){ alert("ごめん失敗した。");},
 80           complete: function(){ alert("保存した!"); }
 81         });
 82       }
 83     }
 84     fr.readAsDataURL(f);
 85   }
 86 
 87   // ブラウザがファイル自体を表示するのを防止
 88   event.preventDefault();
 89 } // end funciton [openDrop]
 90 
 91 function onDragOver(event){
 92   event.preventDefault();
 93 } // end function [onDragOver]

54〜93行目までは、canvas に dragDrop された場合の処理です。
今回さほどいじってません。今回追加したのは1〜52行目です。
1〜2行目で、使用する変数の宣言。5〜52行目で初期化やイベント登録などしています。

6〜8行目
canvas の初期化

  6   // canvas 初期化
  7   canvas = $("#img_canvas").get(0);
  8   context = canvas.getContext("2d");


10〜12行目
ローカルストレージに保存されたデータをプルダウンにセット
ローカルストレージの中身のサイズは「localStorage.length」で取れます。

 10   for(var i=0 ; i<localStorage.length ; i++){
 11     $("#ls_select").append($('<option>').attr({value: i+1}).text("image"+String(i+1)));
 12   }


13〜26行目
上記プルダウンの値が変更された場合のイベントを登録しています。
該当するデータをローカルストレージから引っ張ってきて canvas にセットしています。
19行目の「img.src = localStorage.getItem($(this).val());」で
ローカルストレージの該当データを引っ張り出しています。
20行目で「img.onload」を使っていますが、これは
こういう理由です(→http://www.html5.jp/canvas/how6.html

 13   // localStorageのプルダウンの値が変わったときの動作。
 14   // canvasに画像を反映する
 15   $("#ls_select").change(function(){
 16     if($(this).val().length > 0){
 17       // canvasにLocalStorageの中身を表示
 18       var img = document.createElement('img');
 19       img.src = localStorage.getItem($(this).val());
 20       img.onload = function() {
 21         canvas.width = img.width;
 22         canvas.height = img.height;
 23         context.drawImage(img, 0, 0);
 24       }
 25     }
 26   });


27〜36行目
ローカルストレージに保存するためのボタンが押された時のイベントを登録しています。
30行目の「canvas_data = canvas.toDataURL();」で canvas の画像を取得し
32行目の「localStorage.setItem(key, canvas_data);」でローカルストレージに保存しています。
で、ローカルストレージに保存しただけだと、
再読み込みしない限りプルダウンで画像が選べないので
プルダウンに選択肢(option)を追加して、さらに未選択状態にしています。

 27   // localStorage保存ボタンを押したときの動作。
 28   // localStorageに画像を保存して、プルダウン更新
 29   $("#save_ls_button").click(function() {
 30     canvas_data = canvas.toDataURL();
 31     key = localStorage.length+1
 32     localStorage.setItem(key, canvas_data);
 33     $("#ls_select").append($('<option>').attr({value: key}).text("image"+key));
 34     $("#ls_select").val("");
 35     alert("ローカルストレージに保存したよ。今"+(localStorage.length)+"個のデータがあるよ。");
 36   });


38〜52行目
ローカルストレージの中身をクリアするボタンが押された時のイベントを登録しています。
43行目の「localStorage.clear();」で、お掃除。
これだけだと、プルダウンに消されたデータが残ってしまうので
こちらもお掃除しています。

 38   // localStorageクリアボタンを押したときの動作。
 39   // localStorageをクリアして、プルダウン更新
 40   $("#clear_ls_button").click(function() {
 41     var l_length = localStorage.length;
 42     if(l_length > 0){
 43       localStorage.clear();
 44       $("#ls_select").children().remove();
 45       $("#ls_select").append($('<option>').attr({value: ""}).text("localStorage"));
 46       alert("ローカルストレージを削除したよ。" + l_length + "個のデータを削除したよ");
 47     }else{
 48       alert("ローカルストレージの中身、今空だよ。");
 49     }
 50   });
 51 
 52 });

app.rb

以前作った時から全く手を入れていませんが、ないと動かないので…。
今回は関係ないので、サーバ保存しない版を作っても良かったんですが
副店長はモノグs………(ごほんごほん)
全然いじっていないので、今回は説明省きますね。
なにこれ?って方はこちら見ていただけると幸いです。
(→http://d.hatena.ne.jp/noodles_mtb/20110903/1315036154

  1 #coding:utf-8
  2 
  3 require 'rubygems'
  4 require 'sinatra'
  5 require 'haml'
  6 
  7 configure :production do
  8 end
  9 
 10 get '/' do
 11   set :haml, :format => :html5
 12   haml :index
 13 end # end [get /]
 14 
 15 post '/post_img' do
 16   # 画像ファイルの取得
 17   img_array = params[:file].split(/\s*,\s*/)
 18 
 19   # 拡張子
 20   ext = ".dat"
 21   case img_array[0]
 22   when "data:image/jpeg;base64"
 23     ext = ".jpg"
 24   when "data:image/gif;base64"
 25     ext = ".gif"
 26   when "data:image/png;base64"
 27     ext = ".png"
 28   end
 29 
 30   # 画像ファイル保存
 31   f = File.open("tmp/image#{ext}",'w')
 32   f.puts img_array[1].unpack('m')[0]
 33   f.close
 34   return ""
 35 end
 36 
 37 get '/style.css' do
 38   content_type 'text/css', :charset => 'utf-8'
 39   sass :style
 40 end


それでは、実際に動かしてみましょう。

初期表示してみたところ


ドラッグドロップしてみた所。


ローカルストレージに保存してみたところ


プルダウンから画像を選んでみたところ



おぉぉー。何とか動いているぽいですねぇー。
次回は、さらにこれに手を加えて
副店長の本領発揮、変なものをつくろうと思います。
# …多分…。
# 「できあがったものがこちらです。」的に
# アプリ公開するだけかもしれませんが…

Git と仲良くなりたい(希望)

こんにちは。麺処まつば副店長です。

今日はふと当ブログへのリンク元を見ていたら
MacBook air 調理器具」という検索ワードが目に入りました。
どなたですか。こんな動画を検索して自分もやってみようなんていうのは。
http://www.gizmodo.jp/2011/09/macbookair_knife.html
いけませんよ。店長の大目玉をくらいますよ。

さて、先日。店長に「ちゃんと Git 使いこなしているのか」と聞かれました。
副店長 「あんまr…(ごにょごにょ)」 → 怒られました。
Git については、既に分かりやすい本や記事等沢山あるかと思いますが
自分の頭の中を整理する、という意味で簡単にまとめてみます。



がいねん(漢字で書くとおこがましいので平仮名にしてみました)

元々 Subversion 使いの副店長は、Git の概念を理解するのに少々戸惑いました。

Subversion て、こんな感じでリポジトリサーバから
各ローカルに作業コピーを持ってきて集中管理という形式だと理解しています。
(↓こんなカンジのイメージ)

一方 Git ってのは、作業コピーもリポジトリも全部自分のローカルにあると
店長に教わり、一瞬こんな状態→( ゚д゚) になりましたけども
そこは自分専用のリポジトリであるという理解で一旦納得。

そして、ワークツリー(作業コピーと同義?)とリポジトリの間にもうひとつ
「インデックス」という概念があるというので、またこんな状態→( ゚д゚)
(↓こんなカンジのイメージ)

どうもまだコミットしたくないものを一度ここに仮置きしておく、
コミット待ちのファイルをココに追加しておく、という時に使うようです。
仮コミット置き場のようなものということで理解。

↓Git の全体像はきっとこう…。あってますか…?>店長
(後日談:店長が「合ってる」と言ったので多分合ってます。)

Git というのが何となく掴めてきた(気がする)ので、
使うためのコマンド達をまとめてみようと思います。

副店長は Git まだまだ使いこなせていないので
覚えたら随時追加していくという形で書いていこうと思います。



コマンド

[2013/07/08]
なんだかとても見づらいので、じわじわページ分割します。

リポジトリの作成・コピー

コマンド 概要
git init リポジトリの新規作成
git clone 既存リポジトリをローカルにコピー。svn checkoutぽいもの

ファイルの変更・操作

コマンド 概要
git add インデックスに変更を追加
git mv ファイル名の変更・移動
git rm ファイル削除
git commit インデックスの中身をリポジトリに登録
git reset コミットの取り消し
git revert 過去のバージョンに上書きコミット

ファイルの状態・履歴

コマンド 概要
git status 変更したファイルを表示
git diff 差分を表示
git log コミットログを表示
git show コミット詳細を表示

リモートリポジトリとの連携

コマンド 概要
git pull 以下、詳細はまだ書いていません。
git fetch
git push
git remote

ブランチ周り

コマンド 概要
git branch 以下、詳細はまだ書いていません。
git checkout
git merge
git mergetool
git rebase
git stash
git show-branch
git tag