Wonderful! WordPress

習得中 Rust で 日付順 のファイル一覧取得と php でのコマンドラインツール

Page No.1

あるフォルダ下にある、サブフォルダにあるファイルを含めた総ファイルにおいて、日付逆順(新ー>旧)での一覧を取得したいということがあって。これ、dir コマンドで出来そうなのではあるのだけれど、いかに?

例えば、dir /s /d /Od というコマンドで、 サブフォルダのファイルも表示されるし、日付の古いものから順に並べてもくれる。しかしそれは各フォルダ内にあるファイルだけの並び替えとなる。今、自分が必要としているのは、元親にある全サブフォルダ下(孫、ひ孫フォルダなども含む)の全ファイルによる日付の並び替えなのです。これが標準装備のコマンドで出来るのかどうか、よくわからない。

それなら作ってしまったほうが早い、ってことになるね。
で、現在、習得中の Rust で経験を積むべく、挑むことにしたと。
と、また、まったく WordPress には関係のないネタだったりする。

Rust で全サブフォルダ下にある全ファイルの日付逆順リストの取得

初めてコンピュータ言語を勉強したのは、MS-DOS 時代においての Microsoft が出してた Quick-C という C言語だった。このソフトウェアを買ったら、しっかりした C言語の教本まで入っていて、別途、教本を買う必要がなかった。とても親しみを持ってたソフトウェアだったのだけれど、引っ越しの時に処分してしまって、今思うと、やっぱり持っていればよかったと少々後悔してる。まぁ、その前に Basic なんてのも少しさわっていた事はあるけれど。それから ExcelAccessVBA のあと、phpJavascriptPython という感じ。C 以外は変数の型などゆるゆるなものばかり。

それに対して Rust はこれでもかっ!ってぐらいに変数の型に対してガチガチに石頭な言語。ずっとゆるゆるに慣れ親しんできたものには、それはとてもとても厳しいコンパイラを相手にしないといけないということになる。
まぁ、まだ知らないことばかりなので、おかしな部分も多いのだろうけれど、とりあえず目的を達成してちゃんと機能してはいるということで。

必要な機能としては、先にも書いた通り、あるフォルダ下の全てのサブフォルダに含まれる全てのファイルを対象に、日付の新しい順での一覧を得る、ということ。で、ファイルが千も2千もあると表示がうっとうしいので、表示するファイルの数を指定できるということ。それぐらい。

Rust の場合、とりあえず必要なことは、外部クレートを使用するときには、Cargo.toml にそのクレートを登録しなくてはいけないこと。この場合、日付をタイムスタンプに変換したりするので、必要だったのが chrono というクレート。まずそれをやる。

[dependencies]
chrono = "0.4"
Rust
CopyExpand

Vector 版 main.rs

と、いうことでいきなり、その本丸である main.rs を。
ファイルの日付により新しい順に並べたかったので、ファイル名と日付のコレクションを作ってから並べ替えさせるということを普通に考えていたけれど、調べていると Rust には BTreeMap という便利な HashMap が備えられていた。この BTreeMap はデフォルトでキーにより昇順に並べられているということで、キーにタイムスタンプを使用すれば後でわざわざ並べ替える必要もないとのこと。

こりゃいいや!ってな具合で、実際に最初に作ったのはこの BTreeMap を使った方。が、後でふと考えてみると、デフォルトでキーによって並び替えられているということは、要素を追加するごとに並び替えをしているのではないか?という疑問。これって遅くなるんじゃない?と。Vector で値をとにかく蓄積しておいて最後に一度だけ sort させるのとどちらが負荷が少ないのだろうか?と。

まぁ、あとに書いてあるけれど、BTreeMap の場合はキーで使うタイムスタンプの重複という問題もあるし、言語の習得はとにかく思いついたら書く、ということだと思っているので、BTreeMap で書いたものを Vector に直してみた。いらないものがかなり減るので明らかに Vector の物の方がすっきりしていい。と、いうことでその Vector 版。どちらが速いか?って。同じように速いのでよくわからない。

しかし、その速さには但し書きがつく。詳しくは最後に書いているけれど、このプログラムにおいて Rust が速いのは Windows を起動してこのプログラムを動かすのが二度目以降のことに限られる。

use std::{ io, fs, path::Path };
use chrono::{DateTime, Utc, NaiveDateTime };

fn main() {
    println!( "\n\n=>>> Welcome Sort File under directry by date!" );

    let mut showstr = String::from( "Input target directry. ※need to full pass. q -> Quit. d -> default directry." );
    // get_input はコマンドラインから read_line で入力を取得する独自関数、下の方にあり
    let mut directry = get_input( showstr ).trim().to_string();

    if "q" == directry {

        println!( "=>>> Bye!" );
    } else {
        
        if "d" == directry {// "d" を入力されたときはデフォルトの値を使う
            directry = String::from( "e:\\datas\\images" );
            println!( "Target directry is used default value : {}", directry );
        }

        if directry.len() > 5 {

            showstr = String::from( "Input show list count. show all files -> 00. use default count -> 0" );
            // 表示するファイルの数の指定をコマンドラインから受け取る
            let mut query = get_input( showstr );

            if "0".to_string() == query.trim() {
                query = "30".to_string();// default show count
                println!( "show count use defalut value : 30" );
            } else if "00".to_string() == query.trim() {
                query = "0".to_string();
                println!( "show all files." )
            }

            // 入力され受け取った値を数値へと変換する
            let query: u16 = match query.trim().parse() {
                Ok( num ) => num,
                Err(_) => 20// なにかしらの原因で数値にできなかった時のデフォルト値
            };

            // 違う型の要素を2つもつタプルを要素とするvector
            // Rust の場合、配列は全ての要素の型が等しく、後で要素を追加することができない
            let mut data: Vec< ( i64, String ) > = Vec::new();

            // read_dir でフォルダにあるサブフォルダ+ファイルを取得。引数は &path だけど &strで渡している
            match fs::read_dir( &directry ) {
                Err( why ) => println!( "! {:?}", why.kind() ),
                Ok( paths ) => {
                    // 表示するファイル名から親のパスを消去するため $str の変数にいれておく
                    let host: &str = directry.as_str();

                    for path in paths {
                        let fpath = path.unwrap().path();
                        let fname = fpath.file_name().unwrap().to_string_lossy();

                        if fpath.is_dir() {

                            // それがファイルではなくフォルダの場合は、同じようにフォルダにあるものを取得する関数をよぶ
                            let mut chil: Vec< ( i64, String ) > = read_directry( fpath.as_path(), &host );

                            // append で関数から受け取ったデータを大本のデータに結合
                            data.append( &mut chil );

                        } else {
                            // ファイルの場合は、read_file 関数でそのファイルの日付をタイムスタンプで取得
                            let ts = read_file( fpath.as_path() );
                            // ファイル名が長い場合は、表示が見づらくなるので50文字で切り捨てる
                            // 日本語が入る場合に文字数で文字列を取得する場合は、以下のパターンでできるとのこと
                            let subtmp = fname.chars().take(50).collect::<String>();

                            // 追加するデータはタプル型
                            data.push( ( ts, subtmp ) );
                        }
                    }

                    let mut counter :u16 = 1;
                    println!( "\nResult - All files count : {}\n", data.len() );

                    // タプルの 要素 0、タイムスタンプで昇順に sort
                    data.sort_by( |a, b| a.0.cmp( &b.0 ) );
                    // data.sort_by( |a, b| ( -a.0 ).cmp( &( -b.0 ) ) );逆順で sort する場合

                    for v in data.iter().rev() {

                        // NaivdDateTime::from_timestamp でタイムスタンプを日付になおす
                        // Unix 日付を日本日付に直すために32,400をプラスする
                        let dt = NaiveDateTime::from_timestamp( v.0 + ( 9 * 60 * 60 ), 0  );

                        println!( " {} > {}, {}", counter, v.1, dt );
                        counter += 1;
                        if 0 != query {
                            // 指定された表示数でループを抜ける
                            if counter > query {
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
}

fn read_directry( tarpath: &Path, host: &str ) -> Vec< ( i64, String ) > {

    // フォルダ内のフォルダとファイルを取得し、ファイルならタイムスタンプを取得して返す
    // フォルダの場合は再び自分自身の関数を呼ぶ

    let mut data: Vec< ( i64, String ) > = Vec::new();

    match fs::read_dir( tarpath ) {
        Err( why ) => println!( "! {:?}", why.kind() ),
        Ok( fls ) => {

            for fl in fls {
                let flp = fl.unwrap().path();

                if flp.is_dir() {

                    let mut chil: Vec< ( i64, String ) > = read_directry( flp.as_path(), &host );

                    data.append( &mut chil );
                } else {

                    let ts = read_file( flp.as_path() );

                    match flp.to_str() {
                        None => panic!("new path is not a valid UTF-8 sequence"),
                        Some( s ) => {
                            let tmps = s.replace( host, "" );
                            let subtmp = tmps.chars().take(40).collect::<String>();
                        
                            data.push( ( ts, subtmp ) );
                        }
                    };
                }
            }
        }
    }
    data
}

fn get_input( mes: String ) -> String {

    // コマンドラインから入力を取得する関数

    let mut linestr = String::new();

    loop {
        println!( "\n=>>> {}", mes );


        io::stdin()
            .read_line( &mut linestr )
            .expect( "xxx Failed to read line! xxx" );

        let linestr = linestr.trim().to_string();

        if  linestr.len() > 0 {
            break;
        }
    }
    linestr
}

fn read_file( tarpath: &Path )  -> i64 {

    // ファイルの日付をタイムスタンプとして返す関数

    match fs::metadata( tarpath ) {
        Ok( meta ) => {

            let created_time = meta.modified().unwrap();
            let created_time: DateTime<Utc> = created_time.into();
            let ts = created_time.timestamp();
            ts
        },
        _ => return 2,
    }
}
Rust
CopyExpand

BTreeMap 版 main.rs

BTreeMap を使う場合、1つ問題としてあるのが、キーにファイルの日付を使うということで、全く同日同時刻の物の存在を否定することができないということ。ファイルの日付もいくつかあるので、どれを使用するのかも考えなくてはならない。例えば Windows の場合なら、ファイルの日付として、日付時刻、作成日時、更新日時とある。あと Rust で得られるものとして最後にアクセスした日時もある。

これは実際に検証用のフォルダを作っているときに気づいたことである。新規に作ったフォルダでサブフォルダを不規則にいくつも作り、50個ほどの画像ファイルをそこにコピーした。こうした場合の、各ファイルの作成日時はすべて全く同じ同日同時刻となった。Rust では fs::metadata().created() で得られる値がこの作成日時だった。作成日時はコピーした場合に更新されるので、まとめて一度にコピーすれば全て同じ日時になってしまうということ。

一方、fs::metadata().modified() で得られるのは更新日時の方で、これはファイル自体を上書き保存しない限り更新されない。作ったまま更新していなければ、そのファイルのオリジナルの本来の作成日時ということになると思う。と、いうことでこの場合に使用するのは迷うことなく modified の方となる。しかし、それでも、同日同時刻が存在することは完全には否定できない。秒数まで同じっていうのはなかなかありそうもないとは思うものの。

ディレクトリ構造を取得する部分を別関数としたので、main 関数はそのデータを BTreeMap で受け取って蓄積していくようにした。その場合、受け取ったデータを本データの方へ結合させていくわけなんだけれど、その場合に、キーとして使用してるタイムスタンプに重複があると上書きされて具合が悪いことになってしまう。そこのところで、append が使えないのでちょっと面倒なことをしてる。結局、この点においても、面倒なことをしなければいけないのなら、普通に Vector をつかって最後にソートさせたほうが手間がかからなかったのかも?などと思いつつ・・・。まぁ、いろいろと学ぶことは多かったので良しとしよう。

use std::{ io, fs, path::Path, collections::BTreeMap };
use chrono::{DateTime, Utc, NaiveDateTime };

fn main() {
    println!( "\n\n=>>> Welcome Sort File under directry by date!" );

    let mut showstr = String::from( "Input target directry. ※need to full pass" );
    // get_input はコマンドラインから read_line で入力を取得する独自関数、下の方にあり
    let directry = get_input( showstr ).trim().to_string();

    if "q" == directry {

        println!( "=>>> Bye!" );

    } else if directry.len() > 2 {

        showstr = String::from( "Input show list count. show all files -> 0." );
        // 表示するファイルの数の指定をコマンドラインから受け取る
        let query = get_input( showstr );

        // 入力され受け取った値を数値へと変換する
        let query: u16 = match query.trim().parse() {
            Ok( num ) => num,
            Err(_) => 20// なにかしらの原因で数値にできなかった時のデフォルト値
        };

        // BTreeMap は HashMap のようにキーと値をペアで蓄えていける。違いは、デフォルトでキーにおいて昇順で並び替えがされているということ
        // なので後でわざわざ並び替えをしなくとも、並び替えたいものをキーにしておけば自動でやってくれている
        let mut data: BTreeMap<i64, String> = BTreeMap::new();

        // read_dir でフォルダにあるサブフォルダ+ファイルを取得。引数は &path だけど &strで渡している
        match fs::read_dir( &directry ) {
            Err( why ) => println!( "! {:?}", why.kind() ),
            Ok( paths ) => {
                // 表示するファイル名から親のパスを消去するため $str の変数にいれておく
                let host: &str = directry.as_str();
                let mut addnum: i64;

                for path in paths {
                    let fpath = path.unwrap().path();
                    let fname = fpath.file_name().unwrap().to_string_lossy();

                    if fpath.is_dir() {

                        // それがファイルではなくフォルダの場合は、同じようにフォルダにあるものを取得する関数をよぶ
                        let chil: BTreeMap<i64, String> = read_directry( fpath.as_path(), &host );

                        for ( k, v ) in chil.iter() {
                            let mut kval = k * 1;
                            loop {
                                if ! data.contains_key( &kval ) {
                                    data.insert( kval, v.to_string() );
                                    break;
                                }
                                kval += 1;
                            }
                        }

                        // 下のようにappend を使用すれば BTreeMap の結合はできる
                        // しかしキーが同じものが存在すると後のもので上書きされてしまう
                        // キーに使っているfile の更新時刻で全く同じものが存在する可能性は捨てきれない
                        // let mut chil: BTreeMap<i64, String> = read_directry( flp.as_path(), &host );
                        // data.append( &mut chil );

                    } else {
                        // ファイルの場合は、read_file 関数でそのファイルの日付をタイムスタンプで取得
                        let ts = read_file( fpath.as_path() );
                        // ファイル名が長い場合は、表示が見づらくなるので50文字で切り捨てる
                        // 日本語が入る場合に文字数で文字列を取得する場合は、以下のパターンでできるとのこと
                        let subtmp = fname.chars().take(50).collect::<String>();
                                         
                        // キーに使っているfile の更新時刻で全く同じものが存在する可能性は捨てきれない
                        // 一時的にタイムスタンプの桁数をふやし存在しないキーの数値を探す
                        addnum = ts * 10000;
                        loop {
                            if ! data.contains_key( &addnum ) {
                                data.insert( addnum , subtmp );
                                break;
                            }
                            addnum += 1;
                        }
                    }
                }

                let mut counter :u16 = 1;
                println!( "\nResult - All files count : {}\n", data.len() );

                for ( k, v ) in data.iter().rev() {
                    // 重複するキーを避けるため一時的に桁数を増やしているタイムスタンプの桁数を元に戻す
                    // そして余分に付加した番号を切り捨てて消去する
                    // そのために数値の型を一時的に round() が使用できる f64 にキャストする 
                    let tstp: f64 = ( *k as f64 / 10000.0 ).round();
                    // NaivdDateTime::from_timestamp でタイムスタンプを日付になおす
                    // Unix 日付を日本日付に直すために32,400をプラスする
                    let dt = NaiveDateTime::from_timestamp( tstp as i64 + ( 9 * 60 * 60 ), 0  );

                    println!( " {} > {}, {}", counter, dt, v );
                    counter += 1;
                    if 0 != query {
                        // 指定された表示数でループを抜ける
                        if counter > query {
                            break;
                        }
                    }
                }
            }
        }
    }
}

fn read_directry( tarpath: &Path, host: &str ) -> BTreeMap<i64, String> {

    // フォルダ内のフォルダとファイルを取得し、ファイルならタイムスタンプを取得して返す
    // フォルダの場合は再び自分自身の関数を呼ぶ

    let mut data: BTreeMap<i64, String> = BTreeMap::new();

    match fs::read_dir( tarpath ) {
        Err( why ) => println!( "! {:?}", why.kind() ),
        Ok( fls ) => {
            let mut addnum: i64;

            for fl in fls {
                let flp = fl.unwrap().path();

                if flp.is_dir() {

                    let chil: BTreeMap<i64, String> = read_directry( flp.as_path(), &host );

                    for ( k, v ) in chil.iter() {

                        let mut kval = k * 1;
                        loop {
                            if ! data.contains_key( &kval ) {
                                data.insert( kval, v.to_string() );
                                break;
                            }
                            kval += 1;
                        }
                    }

                    // 下のようにappend を使用すれば BTreeMap の結合はできる
                    // しかしキーが同じものが存在すると後のもので上書きされてしまう
                    // キーに使っているfile の更新時刻で全く同じものが存在する可能性は捨てきれない
                    // let mut chil: BTreeMap<i64, String> = read_directry( flp.as_path(), &host );
                    // data.append( &mut chil );
                } else {

                    let ts = read_file( flp.as_path() );

                    match flp.to_str() {
                        None => panic!("new path is not a valid UTF-8 sequence"),
                        Some( s ) => {
                            let tmps = s.replace( host, "" );

                            let subtmp = tmps.chars().take(40).collect::<String>();
                        
                            // キーに使っているfile の更新時刻で全く同じものが存在する可能性は捨てきれない
                            // 一時的にタイムスタンプの桁数をふやし存在しないキーの数値を探す
                            addnum = ts * 10000;
                            loop {
                                if ! data.contains_key( &addnum ) {
                                    data.insert( addnum , subtmp );
                                    break;
                                }
                                addnum += 1;
                            }
                        }
                    };
                }
            }
        }
    }
    data
}

// コマンドラインから入力を取得する関数 fn get_input() と ファイルの日付をタイムスタンプとして返す関数 fn read_file() は Vector 版に載せたものと全く同じなので省略
Rust
CopyExpand

たったこれだけのことなんだけれど、これがちゃんと動くまでは相当な時間、型の不一致と戦ってしまった。フォルダを取得するとかファイルを読み出すとかの関数においての、引数やら返り値なんかとか、i64 と &i64 で不一致だとか(これは所有権の問題だと思う)、でつまづいてしまう。いったいどうすりゃいいのさ!となって深渕にはまってしまう。なんというか、自分で考えて作っているという気が全くしない。自分的には php Javascript と違って、作っていてもなんだかあまり楽しくない、という感じが Rust はする。

ちなみに結果はこんな具合に表示される

C:\WINDOWS\system32>sort_date


=>>> Welcome Sort File under directry by date!

=>>> Input target directry. ※need to full pass
e:\tmp\tmp

=>>> Input show list count. show all files -> 0.
60

Result - All files count : 50

 1 > 288.jpg, 2020-11-01 14:21:08
 2 > 277.jpg, 2020-11-01 14:08:22
 3 > 275.jpg, 2020-11-01 14:06:26
 4 > \e\f\218.jpg, 2020-11-01 11:19:56
 5 > \e\f\202.jpg, 2020-11-01 11:00:36
 6 > \e\f\200.jpg, 2020-11-01 10:58:35
 7 > \e\197.jpg, 2020-11-01 01:17:21
 8 > \e\193.jpg, 2020-11-01 01:13:35
 9 > \e\173.jpg, 2020-10-30 19:57:02
 10 > \e\171.jpg, 2020-10-30 19:55:08
 11 > \e\169.jpg, 2020-10-30 19:51:40
 12 > \e\166.jpg, 2020-10-30 19:48:51
 13 > \d\165.jpg, 2020-10-30 19:48:07
 14 > \d\164.jpg, 2020-10-30 19:46:58
 15 > \d\160.jpg, 2020-10-30 19:42:19
 16 > \d\159.jpg, 2020-10-30 19:41:17
 17 > \d\157.jpg, 2020-10-30 19:36:55
 18 > \c\156.jpg, 2020-10-30 19:34:06
 19 > \a\g\151.jpg, 2020-10-30 19:26:55
 20 > \a\g\148.jpg, 2020-10-30 19:23:08
 21 > \c\145.jpg, 2020-10-30 19:20:14
 22 > \c\144.jpg, 2020-10-30 19:18:37
 23 > \a\g\143.jpg, 2020-10-30 19:18:00
 24 > \c\142.jpg, 2020-10-30 19:17:07
 25 > \c\141.jpg, 2020-10-30 19:16:24
 26 > \c\134.jpg, 2020-10-30 19:07:43
 27 > \c\132.jpg, 2020-10-30 19:05:48
 28 > \c\126.jpg, 2020-10-30 18:59:34
 29 > \a\g\121.jpg, 2020-10-30 18:53:11
 30 > \c\118.jpg, 2020-10-30 18:40:10
 31 > \c\116.jpg, 2020-10-30 18:38:10
 32 > \b\109.jpg, 2020-10-30 18:28:55
 33 > \b\108.jpg, 2020-10-30 18:28:00
 34 > \c\104.jpg, 2020-10-30 18:24:36
 35 > \b\103.jpg, 2020-10-30 18:23:56
 36 > \d\l\100.jpg, 2020-10-30 18:20:13
 37 > \b\j\30.jpg, 2020-10-30 15:50:58
 38 > \b\j\29.jpg, 2020-10-30 15:50:18
 39 > \b\i\28.jpg, 2020-10-30 15:48:57
 40 > \b\i\27.jpg, 2020-10-30 15:48:02
 41 > \b\i\26.jpg, 2020-10-30 15:46:29
 42 > \b\i\25.jpg, 2020-10-30 15:45:39
 43 > \b\i\k\23.jpg, 2020-10-30 15:43:50
 44 > \b\i\k\22.jpg, 2020-10-30 15:43:13
 45 > \b\i\k\19.jpg, 2020-10-30 15:38:11
 46 > \a\15.jpg, 2020-10-30 15:34:36
 47 > \a\13.jpg, 2020-10-30 15:31:06
 48 > \c\11.jpg, 2020-10-30 15:29:38
 49 > \a\h\07.jpg, 2020-10-30 15:18:22
 50 > \a\g\05.jpg, 2020-10-30 15:15:48

C:\WINDOWS\system32>
Bash
CopyExpand

Rust でのファイルの日付を取得する方法での失敗

最後にある、ファイルの日付をタイムスタンプにて返す関数 read_file() において、最初は間違ったやり方をしていたので、自戒をこめて忘れないようにと。ファイルの日付をいかにして取得できるのかと、ネットを探していてたどり着いたのが metadata()。まぁ、それはよかったのだけれど、その時のページにはファイルを開いて、metadata を使うやり方が示してあったので、自分で書いていたのが以下のごとく。

fn read_file( tarpath: &Path )  -> i64 {

    let f = File::open( tarpath );

    let f = match f {
        Ok( file ) => file,
        _ => return 1,
    };

    match f.metadata() {
        Ok( meta ) => {

            let created_time = meta.created().unwrap();
            let created_time: DateTime<Utc> = created_time.into();
            let ts = created_time.timestamp();
            ts
        },
        _ => return 2,
    }
}
Rust
CopyExpand

これで動いてはいたのだけれど・・・。
問題はこう。PC の電源をいれ Windows が立ち上がって、初めてこのプログラムを動かした時に、ものすごく時間がかかる。ファイルが千以上もあるものなら、フリーズしているんじゃないかと思うほど。二度目からは同じフォルダならパッとあっさり表示されるのだけれど、やはりなんだかおかしい。そもそもファイルの日付を取得するのに、わざわざファイルを開く必要などないのでは?という、疑問を懐きながら関数を書いてはいたのだけれど。たしか、あまり記憶が定かではないけれど、ファイルの日付情報って FAT にあったんじゃなかろうか?と。

Leave a Reply!

JavaScript is necessary to send a comment.
You can edit and delete your comment if you input a edit key.
Edit key is necessary for attesting you when you edit and delete it.
The tag of HTML cannot be used in comment.
When you comment for the first time, it is displayed after the approval of the administrator.
Because I cannot speak English so much, it takes time to answer.
Required fields are marked *.

※Please enter more than 5 characters only alphabets.
※Edit or delete are possible for 2000 days after approval.

*

♠Simplistic Comment User Editable v4.0

♠When visitors leave comments on the site this site collect the data shown in the comments form, and also the visitor’s IP address and browser user agent string to help spam detection.
♠This site does not use cookie when visitors leave comments and commenter edit comment.
♠This site uses Akismet to reduce spam. Learn how your comment data is processed.

Comments feed

Trackback URL : https://strix.main.jp/wp-trackback.php?p=172261

Sanbanse Funabashi
2011.01.01 sunrise

Top

スクロールさせるか画像をクリックすると元に戻ります。