gitでlockファイルのエラーが出た時にやったこと

gitでpullしようとしたら、以下のエラーが出た

fatal: Unable to create '/XXXXX/.git/ORIG_HEAD.lock': File exists.

Another git process seems to be running in this repository, e.g.
an editor opened by 'git commit'. Please make sure all processes
are terminated then try again. If it still fails, a git process
may have crashed in this repository earlier:
remove the file manually to continue.
Completed with errors, see above

調べてみると、「lockファイルは同じgitリポジトリに対して同時に複数の操作を行われないように作られる」らしい。
確かにエラー内容を翻訳してみると、「別のgitプロセスがこのリポジトリで実行されているようです」と言われている。
別段そんなことはしてないんだけどなー。

該当のフォルダの中を見に行ってみる。

$ cd /XXXXX/.git/
$ ls
FETCH_HEAD description logs
HEAD       hooks       objects
ORIG_HEAD  index       packed-refs
branches   index.lock  refs
config     info        sourcetreeconfig

index.lockというファイルがあったので、削除してみる。

$ rm index.lock

fetchして無事にpull出来るようになった。
しかし、index.lock? ORIG_HEAD.lockは??

gitでmasterと作業用ブランチの差分をとったり、macでcommandを作ったり

現場でgitを使っているのですがその使い方が独特で、作業用に作ったブランチをmasterへ直接マージせず、masterと作業用ブランチの差分ファイルを抽出してから、その差分ファイルのみを改めてmasterへコミットするということをやっています。
どうやら、作業用ブランチのコミット履歴がmasterに大量にプッシュされるのがイヤだそうで……
とにかく、郷に入りては郷に従えということで自分もそのようにしているのですが、毎回ターミナルでコマンドを打つのが大変なのでバッチ(command)を作りました。
他にこういう運用をしている現場があるかは別として、とりあえず自分的備忘録として残しておこうかと思います。

//まずはgit管理されているフォルダへ移動
cd /Users/username/git/work/contents

//今チェックアウトしている作業ブランチとmasterを比較して、差分をzipにまとめる
git archive --format=zip --prefix=_contents/ HEAD `git diff master --diff-filter=ACMR --name-only` -o _contents.zip

//作ったzipをデスクトップに持ってくる
mv _contents.zip /Users/username/Desktop

//masterへチェックアウトしとく
git checkout master

//デスクトップへ移動して、
cd /Users/username/Desktop

//zipを解凍して、
unzip '*.zip'

//解凍し終わったzipは削除する
rm *.zip

//作業ファイル一式を格納するディレクトリを作って、
mkdir -p 作業フォルダ/work

//zipを解凍してできたファイル一式を移動させる
mv _* 作業フォルダ/work

//移動させた作業ファイルディレクトリに入って、
cd /Users/username/Desktop/作業フォルダ/work

//ディレクトリ名を変更する
for f in *; do mv "$f" "${f#_}"; done

//移動させた作業ファイルディレクトリの上の階層に移動して、
cd /Users/username/Desktop/作業フォルダ

//ゴミファイルを削除して、
find . -name "*DS_Store" -exec rm -r {} \;

//ファイル一覧を作る
find . -type f > list.txt

ファイル一覧を確認して正しく差分が抽出されていることを確認したら、

//デスクトップに持ってきたファイル一式を、改めてgit管理されているディレクトリに投入する
cp -Rf /Users/username/Desktop/作業フォルダ/work /Users/username/git

これをテキストエディタで保存して、拡張子.commandを付ければ、次回からはcommandファイルをダブルクリックするだけでOK

一部、なぜそんなことをする?という工程も混じってますが、そこはそれ、ローカルルール的なあれです。
なんか、必要なのです。
そもそもなぜ作業ブランチをmasterへマージしないのさ!?とも思いますが、この運用に慣れた今となっては、masterへ投入した際に作業ファイルとの差分が一目で分かって、これはこれで良いような気もします。

この運用を一年以上続けた弊害として、今は作業ブランチをmasterへ直接マージするのが怖くなってます。
おぉ、深刻……
別の現場に行った時に一般的な運用ができなくなっていそうだ……

macOSのアップデートでエラーがいろいろ出た対処法

現場で貸与されたmacのOSが古く、とうとうadobe系のソフトが対応してくれなくなったので、El CapitanからとりあえずSierraにアップデートしたところ、色々エラーが起こるようになった。
その対処法のまとめ。


macOSアップデートでXAMPPのApacheが動かなくなった

Starting Apache Web Server...
Exit code: 8
Stdout:
apache config test fails, aborting
Stderr:
(2)No such file or directory: AH02291: Cannot access directory '/var/log/httpd/' for error log of vhost defined at /Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf:66
(2)No such file or directory: AH02291: Cannot access directory '/var/log/httpd/' for error log of vhost defined at /Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf:59
(2)No such file or directory: AH02291: Cannot access directory '/var/log/httpd/' for error log of vhost defined at /Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf:40
AH00014: Configuration check failed
  • アップデート時にルートの /var/log/httpd/ ディレクトリが削除されてしまったらしい。
    削除されたディレクトリを作って書き込み権限を与えればOK
$ cd /var/log/
$ sudo mkdir httpd
Password:
$ sudo chmod 777 httpd/

macOSアップデートでgitのエラーが起きた

xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
  • CommandLineTools がなくなってしまっているとのこと。
    ターミナルからコマンドラインツール(xcode-select)をインストール。
$ xcode-select --install
  • 使用許諾契約についてのポップアップが出るので、「同意する」を選択。
    ダウンロードが始まります(1時間くらいかかった)。
    インストール完了すると、
$ git --version
git version 2.14.3 (Apple Git-98)

Gitが使えるようになった!

扉開閉検知システムの開発 -その1 準備、近接センサ選定-

E-kanのラボ主です。
先日、IoT関連の開発の一環として、扉の開閉検知を軽く作ってみたので、
それに関する事を少し記事にしてみようと思います。

ここのところ、アニメ記事ばかりでしたからね、
少しは技術記事を増やしていきましょう。

扉開閉検知の全体像

最終目標としては、
『デバイス側のセンサで扉の開閉を検知してネット経由でslackの特定チャンネルに通知する』
を想定しています。
この目標を実現するには、いくつもの工程や検討事項が必要です。
よって、いきなり全てを作っていくのは、分量が多くなってしまうので、
複数回に分けて少しずつ記事にしていこうと思います。

今回は、その中でも要となる近接センサについて、語ろうと思います…が、
実はこのセンサについてが一番、考えることが多いので、
センサ検知だけでも2〜3回に分けて考えていきます。

扉開閉検知センサの種類

さて、前項で近接センサと言う言葉をいきなり使ってしまいましたが、
扉の開閉を検知するにはどんな仕組みが必要でしょうか?

一番簡単なのは、部屋の電気を点けたり消したりするスイッチです。
これを接点スイッチとか物理スイッチとか言います。単にスイッチと言うとこれを指したりもします。
IoTとして使うのは、マイクロスイッチが代表的ですね。

他にも、超音波を使うセンサや赤外線を使うセンサ、
金属を検知するセンサなど様々なものがあります。
こう言ったものは、実際に扉に接触せずに検出するので、近接センサと呼ばれます。

本当は、それぞれのセンサの解説をしていきたいところですが、
それをすると文章量も、執筆時間も大変なことになるので、割愛します。
もし、機会があれば、別で解説しますね。

扉開閉検知センサの選定

さて、今回ですが、扉の開閉検知には、近接センサの1つであるフォトリフレクタ(反射型フォトセンサ)を使おうと思います。
簡単な選定理由理由ですが…
・マイクロスイッチのような物理センサは接触するので故障率が高くなる
(特に扉とかは勢い良く閉じられたりする可能性もあるので)
・マイクロスイッチは接触させるので、センサの設置位置調整が必要
・超音波センサはサイズが大きめ&値段も少し高め
・金属センサは値段が高め&物によっては制御が面倒。入手性も悪い。
・フォトリフレクタは小型で安価、検出距離も数ミリ〜十数ミリあるので、設置位置の調整が容易
となります。
実際に選定する時は、それぞれの検出距離や制御方法、単価などを表にして比較する必要がありますが、
今回はそこまでする必要も無いので、扱いやすいセンサでいきます。

フォトリフレクタ(反射型フォトセンサ)

さて、ここまででセンサの種類が決まりました。
ここで、今回扱うセンサ、フォトリフレクタの仕組みを簡単に説明します。
このセンサの中には光を出す素子(発光素子)と、光を受ける素子(受光素子)が組み込まれています。
受光素子は、光を受けていない時はOFF状態となっていて電流を流しませんが、
受光するとONとなって電流を流す特性があります。
この仕組みを利用して、障害物の検出を行います。

そして、今回利用するのは、
ROHM 反射型フォトセンサ(フォトリフレクタ) RPR-220
となります。
色々見ていると、LBR-127HLDの方が良く使われているみたいですが、
私が秋葉原に買いに行った時には見当たらなかったので、
適当に購入したRPR-220を使って作っていきます。
(実際にちゃんと物を作る時は、こんな適当じゃダメですよ!)

LBR-127HLDとRPR-220の違いも何処かで比較検証したいところですね。

データシート

一般的に電子素子を扱う時には、その素子の特性をしっかり調べる必要があります。
特性を知らずに扱うと、素子が異常過熱したり、破裂したりするので危険です。
実際、私も学生時代にはトランジスタを破裂させたりしましたし、
つい先日も、電源を入れたままデバイスにLEDを接続させて、LEDを破裂させてしまいました。
あれは少しビックリしました…手に持っている状態で破裂しましたからね…

と言う訳で、RPR-220のデータシートが こちら になります
さて、このデータシートをどう見て、どう活用するのか。
ここからが、電子工作&IoTの肝となる訳ですが…
キリが良いので、それは次回にしたいと思います。

今回のまとめ

今回は1回目と言うことで、
これから作ろうとしている物の説明や、センサについての説明で終わってしまいました。
デバイスや素子を扱う場合は、事前準備や知識が色々必要になるので大変ですね。
次回は、データシートの読み方を中心に説明していきます。
(もしかしたら、それだけで終わってしまうかも…)

iOSでdropbox連携を行う(第5回ー最終回ー)

あれよあれよと言う間に、いつのまにか2018年も半分過ぎようとしています。
こんにちは、E-kanのラボ主です。
Dropbox連携の連載1回目って、もう1年以上前ですよね。
1年で連載5回(3.5回含む)、しかも完結していないってどうなんでしょうね?
と、そんな訳で、残りはサクッと進めて終わらせてしまいましょう!

Dropbox…に限らずストレージ連携で重要なのは、
・ログイン(認証)
・リストの取得
・ダウンロード
・アップロード
・削除
あたりだと思います。
連載4回までで、ダウンロードまで終わっていますので、今回は『アップロード』と『削除』を実現させて、
連載を終わらせたいと思います。

アップロード

それでは、ファイルのアップロードを実装して行きましょう。

コード実装

まずは、ViewControllerのソースファイルに以下を追加します。
このコードがアップロードを行うコードになります。

    @IBAction func pushUpload(_ sender: Any){
        guard let client = DropboxClientsManager.authorizedClient else {
            return
        }
        let fileData = "testing data example".data(using: String.Encoding.utf8, allowLossyConversion: false)!
        
        _ = client.files.upload(path:”/myUploadTestFile.txt”, input: fileData)
            .response { response, error in
                if let response = response {
                    print(response)
                }else if let error = error {
                    print(error)
                }
            }
            .progress { progressData in
                print(progressData)
            }
    }

今回は、ボタンが押されたらアップロードを行う処理とするので、
@IBAction func pushUpload(_ sender: Any)
としてボタン押下イベントと関連付けできる様にメソッドを作っています。
この中のlet fileDataの部分が、実際にアップロードするファイルデータを作成している部分です。
今回は、文字列を直接テキストデータに見立てていますが、
ここの記述を、アプリ内のデータや写真などにすることで、任意のデータをアップロードすることができます。
client.file.uploadの部分が実際にアップロードをする記述ですね。
pathの部分に“パス&ファイル名”、inputの部分に“アップロードするデータ”を設定します。
サンプルコードは、トップディレクトリに”myUploadTestFile.txt”というファイル名でアップロードします。
アップロードが完了、失敗した時の処理はprint(response)print(error)の部分に記載します。

ボタンの配置

次は、アプリの画面にボタンを配置して、コードとの関連付けを行います。
まずは、画面にボタンを配置します。
ボタンのタイトルは適当に”アップロード”としました。
ボタンのサイズとかも適当に調整してしましょう。

次に、ボタンが押された時のイベントと実行メソッドを関連付けます。
1. 左のメニューのViewControllerを選び、
2. 右側のメニューの左端のタブを開きます。
3. 先に実装していた pushUpload: が表示されていますので、
その右側の + をクリックして、先ほど作成したボタンの上までドラッグします。
4. メニューが表示されますので、Touch Up Insideを選びましょう。

これで、イベントの関連付けが完了です。

アップロードの実行

さて、それでは実際に動かしてみましょう。
アプリが立ち上がったら、”アップロード”ボタンを押下してください。
成功したら、以下のログが出てくると思います。

アップロード成功ログ
 : Parent: 0x0 / Fraction completed: 1.0000 / Completed: 20 of 20  
{
    "client_modified" = "2018-06-09T09:48:53Z";
    "content_hash" = 926f8518aad676605dd0b45a61d0777d73e63123203eefc9ac58336422316cae;
    id = "id:bLMfqIGYb7MAAAAAAAAOGQ";
    name = "myUploadTestFile.txt";
    "path_display" = "/myUploadTestFile.txt";
    "path_lower" = "/myuploadtestfile.txt";
    rev = cf0647110c;
    "server_modified" = "2018-06-09T09:48:53Z";
    size = 20;
}

実際に、ドロップボックスのサイトでトップディレクトリを見ると、”myUploadTestFile.txt”ができあがっているのが確認できます。

やったー。
これでアップロード機能の完成です。
もし、アップロードが失敗する場合は、ログイン認証が出てきていない可能性があるので、先に認証をしてください。

ファイルの削除

続いて、ファイルの削除機能を作ってみましょう。

実装

アップロードと同じ様に、ViewControllerのソースファイルに以下を追加します。
このコードが削除を行うコードになります。

    @IBAction func pushDelete(_ sender: Any){
        guard let client = DropboxClientsManager.authorizedClient else {
            return
        }
        _ = client.files.delete(path: "/myUploadTestFile.txt")
            .response{ response, error in
                if let response = response {
                    print(response)
                }else if let error = error {
                    print(error)
                }
            }
    }

client.files.delete(path: “***”)
の部分が実際に削除を行う部分になります。
pathの部分に削除したいファイルをパス込みで指定します。
アップロードと同じく、print(response)の部分に成功したときの処理、
print(error)の部分に失敗した時の処理を書くことになります。

ボタンの配置

アップロードと同じ様に、削除ボタンを配置して、ボタンの関連付けを行いましょう。

削除の実行

それでは、実際に動かして削除してみましょう。
削除ボタンを押下して、次の様なログが出力されれば成功です。

{
    "client_modified" = "2018-06-09T09:48:53Z";
    "content_hash" = 926f8518aad676605dd0b45a61d0777d73e63123203eefc9ac58336422316cae;
    id = "id:bLMfqIGYb7MAAAAAAAAOGQ";
    name = "myUploadTestFile.txt";
    "path_display" = "/myUploadTestFile.txt";
    "path_lower" = "/myuploadtestfile.txt";
    rev = cf0647110c;
    "server_modified" = "2018-06-09T09:48:53Z";
    size = 20;
}

実際にドロップボックスのサイトを見ると、ちゃんと削除されていますね。

ちなみに、存在しないファイルを削除しようとした場合は、エラーとなって、次のログが出力されます。

[request-id 9b3c89589bd3669b1abbcbaf9b6d2b0e] API route error - {
    ".tag" = "path_lookup";
    "path_lookup" =     {
        ".tag" = "not_found";
    };
}

path_lookupの.tagに”not_found”と出力されているので、エラー内容が分かり易いですね。

まとめ

少し駆け足となりましたが、これで、一通りの機能を使うことができるようになりました。
今までの連載の内容を応用していけば、ドロップボックスを使った色々なアプリが作れるようになると思います。
ぜひ、何か便利アプリを作ってみてください。

最後に … one more

今回でdropbox連携の連載は完結となりますが、後一回だけ。
番外応用編として、カメラで撮影した画像をそのままdropboxにアップロードするアプリを作ってみたいと思います。
どの様なアプリになるか、どのくらいのソース量になるかまだ未定ですが、
お楽しみに。

iOSでdropbox連携を行う(第4回)

2018年、明けましておめでとうございます。E-kanのラボ主です。
おそらく、新年一発目の記事投稿だと思うので、新年の挨拶をしてみました。
昨年は投稿頻度も記事の内容も中途半端だったので、今年は頻度を上げて充実な内容の記事投稿を心掛けたいと思います。

iOSでdropboxのファイルをダウンロード

さて、そんな新年一発目の記事は…
もの凄く間が空いてしまっていた、iOSのdropbox連携の連載第4回です。
前回は3.5回だったので、実質は5回目でしょうか?
前回までで、Dropboxのファイルリストまで取れるようになりました。覚えてますか?
今回はファイルをダウンロードして、表示してみたいと思います。
と言っても、いきなりPDFとかwordとかExcelとかを表示できるようにするのは厳しいので、
単純なテキストファイルを表示してみます。
(と言うか、wordとかExcelって自前での表示は厳しいきがする)

1. 事前準備

まずは事前準備として、dropboxにテキストファイルを入れて置かないとダウンロードできないので、トップディレクトリに適当なテキストファイルを配置してください。
1点注意ですが、この時にUTF8で保存したファイルを入れるようにして下さい。
他のコードで保存している場合は、ダウンロード後の表示で文字化けしてしまいます。

私は下の内容のファイルを作りました。

test.txt
dropboxダウンロード
テストです。

以下ではこの test.txt をダウンロードして行きます。

2. ファイルのダウンロードメソッド

ダウンロードファイルが作成できたので、次は実装するメソッドを調べます。
と言っても、公式マニュアルに Download-style request という項があるので、これを使うだけです。ですが、マニュアルには2つのメソッドが載っています。

client.files.download(path: “/path”, overwrite: true, destination: destination)
client.files.download(path: "/path")

1つ目はファイルをダウンロードして、端末に保存するメソッドで、
2つ目はファイルをダウンロードして、メモリ上に読み込むメソッドのようです。
ソーシャルゲームみたいに、データを端末にダウンロードする場合は1つ目を使って、
すぐにファイルを表示して端末に保存する必要がない場合は2つ目を使う、
というような使い分けになりそうです。

3. ダウンロードの実装

それでは実際に実装をしていきます。
今回は、端末に保存する必要がないので、前項の2つ目のメソッドを実装します。
ViewController.swift に次のコードを追加しましょう。

    @IBAction func pushDownload(_ sender: Any){
        let path = "/test.txt"
        guard let client = DropboxClientsManager.authorizedClient else {
            print(“認証されていません”)
            return
        }
        client.files.download(path: path).response { (response, error) in
            if let response = response {
                print(response)
            } else if let error = error {
                print(error)
            } else {
                print("response and error is nil")
            }
        }.progress { (progress) in
            print(progress)
        }
    }

画面のボタンを押したら実行させたいので、IBでボタンと関連付けできるように@IBActionを付けています。
また、dropboxの認証が行われていない(あるいは期限切れ)場合もあるので、その場合は”認証されていません”をログに出力する様にしています。

次は、画面にダウンロードボタンを追加しましょう。
Main.storyboardを開いて、UIButtonをドラッグして追加、ボタン名を適当に変更して、
ViewControllerのpushDownloadをボタンのTouchUpInsideに関連付けます。
(詳しいやり方は、連載の2回目かどこかのWEBサイトを参照ください)

さて、これでダウンロードの実装完了です。
それでは、実際に動かして観ましょう!
こんな感じのログが出て来たら成功です。

<NSProgress: 0x60000012d2a0> : Parent: 0x0 / Fraction completed: 1.0000 / Completed: 44 of 44  
({
    "client_modified" = "2017-12-31T09:32:53Z";
    "content_hash" = a7188a8df4cffdbf1cb7bfe9ab33b5d7997f3f9c80e2a6b087c5fe8b2ef7968f;
    id = "id:bLMfqIGYb7MAAAAAAAAOEQ";
    name = "test.txt";
    "path_display" = "/test.txt";
    "path_lower" = "/test.txt";
    rev = cc0647110c;
    "server_modified" = "2017-12-31T09:32:53Z";
    size = 44;
}, 44 bytes)

もし、”認証されていません”と出たら、一度認証ボタンで認証を行ってください。

4. テキストの表示

続いて、ダウンロードデータをテキストに変換して出力しましょう。
前項では response を得ただけで、実際のデータは見れていなかったので、responseメソッド内のブロック部分を次の様に変えてみましょう。

            if let response = response {
                let responseMetadata = response.0
                print(responseMetadata)
                let text = String(data: response.1, encoding: .utf8)
                print(text ?? "empty")
            } else if let error = error {
                print(error)
            } else {
                print("response and error is nil")
            }

これを実行して、次の様なログが出れば成功です。

(---略---)
dropboxダウンロード
テストです。

少し解説しますと、
response.0 はファイルのメタデータ、response.1 は実データが入っています。
response.1 をファイル形式に応じて処理していく訳ですが、
今回はテキストデータ固定と決め打ちして、UTF8エンコードで文字列に変換して、ログに出力しています。
ちなみに、dropboxに入れたテキストファイルがUTF8以外の文字コードで保存されている場合は、文字化けしますので注意して下さい。
その場合は、.utf8 の部分を .shiftJIS などの保存した文字コードに変えると上手く行きます。

5. エラー

ここまでで、今回の目的となるテキストファイルのダウンロードと表示は完了です。
最後に、私が今回試したエラーを紹介します。
・ファイルが無い(パス誤り)

<NSProgress: 0x604000131800> : Parent: 0x0 / Fraction completed: 0.0000 / Completed: 97 of -1  
[request-id 750f5b0c170eda099b9b6ee9df53c064] API route error - {
    ".tag" = path;
    path =     {
        ".tag" = "not_found";
    };
}

存在しないファイルをダウンロードしようとすると、こんなエラーが発生しました。
・パスの先頭に”/“がない
ダウンロードのファイルパスを”test.txt”のようにして、頭に”/“を忘れると、downloadメソッドでパターンエラーとなってクラッシュしました。
・通信エラー(ネットワーク切断)

Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSErrorFailingURLStringKey=https://api-content.dropbox.com/2/files/download, NSErrorFailingURLKey=https://api-content.dropbox.com/2/files/download, _kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=60, NSLocalizedDescription=The request timed out.}

ネットワークを切断した時のエラーはよくみるエラーでした。

次回はアップロード

これでiOSでのdropbox連携の連載4回目が終了です。
今回やったのはテキストファイルだけですが、ダウンロードデータはData型で取得できるので、今回のやり方でどんなファイルでも対応できると思います。

次回は、基本機能の1つであるファイルのアップロードをやってみたいと思います。
なるべく期間を空けずに書こうと思いますので、暫くお待ちください。

iOSでdropbox連携を行う(第3.5回)

iOSのdropbox連携の第3.5回。
第3回でファイルリストの取得が中途半端になったので、その補足というか、完結回です。
前回では、リストを取得して名前を出力したけど、それがフォルダなのかファイルなのか判断がつかない状況でした。
今回は、それをちゃんと判断して扱えるようにします。

1.最終的なコード

いきなりですが、今回の完成コードが次になります。
ViewController.swiftの全コードです。

import UIKit
import SwiftyDropbox

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let client = DropboxClientsManager.authorizedClient
        
        client?.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in
            if let result = response {
                for elem in result.entries {
                    if let file = elem as? SwiftyDropbox.Files.FileMetadata {
                        print("filename = " + file.name)
                    }else if let folder = elem as? SwiftyDropbox.Files.FolderMetadata {
                        print("foldername = " + folder.name)
                    }else{
                        print("other")
                    }
                }
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func pushAuthentication(_ sender: Any) {
        DropboxClientsManager.authorizeFromController(UIApplication.shared,
                                                      controller: self,
                                                      openURL: { (url: URL) -> Void in
                                                        UIApplication.shared.openURL(url)
        })
    }
}

赤字の部分が今回のポイントとなるコードとなります。
これを実行すると次のようなログとなります。

foldername = プロジェクト
foldername = Photos
foldername = Public
foldername = private_data
foldername = カメラアップロード
foldername = program
foldername = 共有テスト
foldername = 勉強会資料
filename = はじめに.pdf
filename = ゲーム作りのフロー_120302.ppt
filename = SDキャララフ.lip
filename = test120509.java
filename = memo.txt
filename = MP_プロット
filename = 画面仕様書考案メモ

ちゃんとフォルダとファイルが判断できていますね。
ちなみに、ファイル名を出力しているところを次のように変えると、ファイルサイズも取れます。

print("filename = ",file.name,", size = ",file.size)
foldername = プロジェクト
foldername = Photos
foldername = Public
foldername = private_data
foldername = カメラアップロード
foldername = program
foldername = 共有テスト
foldername = 勉強会資料
filename =  はじめに.pdf , size =  231991
filename =  ゲーム作りのフロー_120302.ppt , size =  122368
filename =  SDキャララフ.lip , size =  1111040
filename =  test120509.java , size =  23479
filename =  memo.txt , size =  69
filename =  MP_プロット , size =  4166
filename =  画面仕様書考案メモ , size =  4107

これで、第3回分の完成となります。
ですが、結果だけ示して終わりでは少し味気ないので、この結果に至った経緯を説明したいと思います。
Tips的なことだけ知りたい方は、これ以降を読まなくても大丈夫ですので、そのまま次回をお待ちください。

2.情報の整理

さて、それではSwiftyDropboxのインターフェイス部分を少し探って見ましょう。
探るためのキーとなるのは次の事柄です。
・ resultをprintで表示した場合は色々なデータが取れている
・ result.entriesで要素を取り出すと名前のメンバくらいしか取れない
・ そもそもresultとかresult.entriesってどんな型(クラス)なの?
それと、正確な情報が欲しいので、次のサイトも参照します。
Dropbox API v2 Swift SDK
Dropbox API v2
1つ目はSwiftyDropboxのリファレンス、2つ目はDropboxのAPIのリファレンスです。
最終的に2つ目は特に使う必要がなかったのですが、一応載せておきます。
この情報と実際のコードを元に動きを紐解いていきましょう。

3.コードの分解

まずは問題となったコードを少し整理して見ます。

        let client = DropboxClientsManager.authorizedClient
        client?.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in
            if let result = response {
                for elem in result.entries {
                        ……
                }
            }
        }

これがポイントとなるコードの全体です。
1行目の client を取得している部分は単にAPIのクライアントを取得しているだけなので問題ないでしょう。
ポイントは2行目のブロックです。
このままだと解りにくいので、少し分解しましょう。

client?.files
.listFolder(path: "")
.response(queue: DispatchQueue(label: "MyCustomSerialQueue"))
{ response, error in …… }

ずらーっと書かれていた部分は4つの要素で構成されています。
メソッド名から推理して行くと、

client?.files

ファイル関連のインスタンスを取得している

.listFolder(path:"")

pathが””のフォルダリスト取得する(実際には次が実際の実行文になるので準備する)

.response(…)

応答を得る(前回記載通り、別スレッドで実行します)

{}

このブロックで実行結果が返ってくるので処理を行います。
responseに取得情報が入って、何らかのエラーがあればerrorにエラー情報が入るのでしょう。

4.解析1

今回、応答を処理するブロックのresponseは一体何なのか?がポイントです。
Swiftは型推論なのでぱっと見では解りませんが、Quick Helpでは推論した型が確認できます。
実際に確認してみると、response変数は

(Files.ListFolderResult)?

と出ます。
このFilesクラスのListFolderResultは何でしょう?名前からして結果のクラスなのは確実です。
ここからは検索を行うか、Commandを押しながらコードをクリックして定義を追って行きます。
検索してみたら、次のコードが出てきました。

    /// The ListFolderResult struct
    open class ListFolderResult: CustomStringConvertible {
        open let entries: Array<Files.Metadata>
        open let cursor: String
        open let hasMore: Bool
        public init(entries: Array<Files.Metadata>, cursor: String, hasMore: Bool) {
            ……
        }
        open var description: String {
            return "\(SerializeUtil.prepareJSONForSerialization(ListFolderResultSerializer().serialize(self)))"
        }
    }

ここでのポイントは

open let entries: Array<Files.Metadata>

です。
このentriesメンバにファイル情報が入っているのは明らかですので、次はこのMetadataが何かを探りましょう。

    /// Metadata for a file or folder.
    open class Metadata: CustomStringConvertible {
        open let name: String
        open let pathLower: String?
        open let pathDisplay: String?
        open let parentSharedFolderId: String?
        public init(name: String, pathLower: String? = nil, pathDisplay: String? = nil, parentSharedFolderId: String? = nil) {
            ……
        }
        open var description: String {
            return "\(SerializeUtil.prepareJSONForSerialization(MetadataSerializer().serialize(self)))"
        }
    }

見やすくするためにコメント関係は削除していますが、上のように定義されています。
前回はこのnameを取得していたのですが、やはりそれ以外には有益な情報が取れないみたいです。
さて、これ以上は探れるものが無くなってしまいました。
……
実は、前回はここで時間切れとなってしまったのです。

5.解析2

普通にライブラリの設計を考えて、名前だけ取れてそれがフォルダなのかファイルなのか判断できないなんてことはあり得ないです。そんなライブラリ使いにくいです。
ということで、4.で追ったMetadataクラスにもう少し注目してみます。
何か情報がないかと Dropbox API v2 Swift SDK を眺めていたら見つけました。
どうやら FolderMetadata や FileMetadata というクラスが存在するみたいです。
そして、どちらのクラスもFiles.Metadataを継承しています。
つまり、フォルダならFolderMetadata、ファイルならFileMetadataで情報を格納していて、
レスポンスを返す時には親クラス型のアレイでまとめているのでは?となります。
ここまで推測できれば試しに、レスポンスで帰ってきた情報を FolderMetadataやFileMetadataにキャスト出来るか試してみるだけです。

        client?.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in
            if let result = response {
                for elem in result.entries {
                    if let file = elem as? SwiftyDropbox.Files.FileMetadata { // File?
                        print("file")
                    }else if let folder = elem as? SwiftyDropbox.Files.FolderMetadata { // Folder?
                        print("folder")
                    }else{
                        print("other")
                    }
                }
            }
        }

これで実行すると”file”や”folder”が出力されます。予想通りの動きです。
ちなみに、”other”が出力される場合 DeletedMetadata というクラスと思われます。
念のためにMetadataを継承しているクラスを調べたところ、DeletedMetadata も出てきました。
名前からして削除されたフォルダなりファイルということだと思いますが、
今回の目的としては現存しているデータを取るのが目的なので無視することにします。

ここまで調べて完成したコードが初めに載せたものとなります。
少し長くなりましたし、調査方法の細部(Quick Helpの味方など)を省いたので分かりにくい部分があるかもしれませんが、
これでやっと次の段階に進めます。

次回は、実際にファイルをダウンロードしてみたいと思います。
画像とかPDFだと新たにViewを作らないといけないので、テキストだけに絞ってログ出力だけにしようかな?
…… 実際にどうなるか、乞うご期待!

iOSでdropbox連携を行う(第3回)

iOSのdropbox連携の第3回。
前回でやっとdropboxのログイン認証までできましたので、
今回からは実際にデータを取得していきます。
実は前回までの内容は公式のマニュアルを見れば全て乗っているので、
そのままの手順を踏めば問題なく実行できるものでした。(多少の補足などは行いましたが)
私もここまでは何の問題もなくすんなりといきました。
ですが、ここから先が少し手探りとなります。

1.ファイルリストの取得

まずは、dropbox内のファイルリストを取得したいと思います。
先述の公式マニュアルを見ると、Specify API call response queueの項目にそれっぽいのがあります。

let client = DropboxClientsManager.authorizedClient!

client.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in
    if let result = response {
        print(Thread.current)  // Output: {number = 4, name = (null)}
        print(Thread.main)     // Output: {number = 1, name = (null)}
        print(result)
    }
}

説明分を読むと
『デフォルトではリクエスト/レスポンスはメインスレッドで実行されるので、
別スレッドで実行するには上のようなresponseメソッドを使って実行スレッドを設定します。』
という感じの事が書かれています。
つまりは、このコードを実行すると、メインスレッドとは別のスレッドで処理が行われるという事みたいですね。
そして、実行内容としては files.listFolder(path:””) とあるので、おそらくこれでTopフォルダのファイルリストが得られるのでしょう…
試しに、ViewControllerの viewDidLoad で記述してみましょう。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let client = DropboxClientsManager.authorizedClient
        
        client?.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in
            if let result = response {
                print(Thread.current)  // Output: {number = 4, name = (null)}
                print(Thread.main)     // Output: {number = 1, name = (null)}
                print(result)
            }
        }
    }
    ……
}

これで実行すると…
あれ?何も起きない…ログに何も表示されない……
多分、dropboxの認証が行われていないのでエラーになったのでしょう。
前回作成した認証ボタンを使って一度ログイン認証を行ってからもう一度アプリを立ち上げてみます。

{number = 4, name = (null)}
{number = 1, name = (null)}
{
    cursor = “****************”;
    entries =     (
                {
            ".tag" = folder;
            id = "id:bLMfqIGYb7MAAAAAAAABGg";
            name = "\U30d7\U30ed\U30b8\U30a7\U30af\U30c8";
            "path_display" = "/\U30d7\U30ed\U30b8\U30a7\U30af\U30c8";
            "path_lower" = "/\U30d7\U30ed\U30b8\U30a7\U30af\U30c8";
            "shared_folder_id" = 105655634;
            "sharing_info" =             {
                "no_access" = 0;
                "read_only" = 0;
                "shared_folder_id" = 105655634;
                "traverse_only" = 0;
            };
        },
……
                {
            ".tag" = file;
            "client_modified" = "2012-02-29T10:31:46Z";
            id = "id:bLMfqIGYb7MAAAAAAAABHQ";
            name = "\U306f\U3058\U3081\U306b.pdf";
            "path_display" = "/\U306f\U3058\U3081\U306b.pdf";
            "path_lower" = "/\U306f\U3058\U3081\U306b.pdf";
            rev = 70647110c;
            "server_modified" = "2012-02-29T10:31:46Z";
            size = 231991;
        },
……
    );
    "has_more" = 0;
}

今度は何か取れました。
先ほどのは、やっぱり認証ができていなくてエラーになってたみたいですね。
レスポンス内容を見ると、“.tag”のfolderとかfileとかでフォルダ/ファイルが判別できるみたいですね。
そして、”name”にフォルダやファイル名が入っているようです。
Unicodeエンコードされているので分かりにくいですが、ちゃんとファイル名が取れています。
試しにコードをこのサイトでデコードしてみたら、ちゃんと日本語のファイル名が取れます。

2.データの取り出し

1.にてTop階層のフォルダとファイルが取得できているのを確認できました。
ですが、result変数にデータが入っているのはわかりますが、どうやって取り出すのでしょう?
公式のドキュメントを見ても分かりやすく記載されていなかったので色々を探ったところ、

                for elem in result.entries {
                    print(elem.name)
                }

でファイル名のリストが取れました。

プロジェクト
Photos
Public
private_data
カメラアップロード
program
共有テスト
勉強会資料
はじめに.pdf
ゲーム作りのフロー_120302.ppt
SDキャララフ.lip
test120509.java
memo.txt
MP_プロット
画面仕様書考案メモ

ですが、これではそれがフォルダなのかファイル名なのか判断がつきません。
他のメンバも確認して見ましたが、どれを使ってもダメでした。
どうやら、この取り方では情報が欠落してしまうようです。
1.の結果を見る限りではresultの中に”.tag”という情報は入っているのに…

とりあえず、今回の目的である、フォルダ名・ファイル名は取得できました。
ですが、今回の結果では、フォルダならさらにその中を見に行くなどの対応が取れません。
さらにはファイルの更新日時やサイズなども取れません。
(正確には取れているけど、オブジェクトから取り出せません)

実は、SwiftyJSONというJSONライブラリを使っての解析など色々試して見たのですが、どうもうまくいきませんでした。
(公式マニュアルで記載しているのとは別のメソッドを使った時はうまく取れたのですけど…)
今回はここでタイムアップしてしまいましたので、
次はもう少しこのあたりを掘り下げていこうと思います。
(補足回として第3.5回で掘り下げました)

iOSでdropbox連携を行う(第2回)

数回に渡ってお送りしているiOSでのdropbox連携。その2回目です。

前回は、SwiftyDropboxを新規プロジェクトに組み込むところまで行いました。
今回は、実際にライブラリを使ってDropboxにログインして見たいと思います。

1. DropboxClientの初期化

Dropbox連携はDropboxClientというクラスを使用して行いますが、その初期化を行います。
まずは、Application delegeteあたりに下のコードを記述しましょう。
基本的にはどこで初期化をしても良いのですが、アプリ起動時に初期化してしまうのが楽だと思いますので、そのタイミングで呼ばれる application:didFinishLaunchingWithOptions: で記述しちゃいます。
“”の部分には、前回のアプリ登録で確認したAppKeyに置き換えてください。
また、SwiftyDropboxライブラリを使いますので、import文も追加するのを忘れずに。

import UIKit
import SwiftyDropbox

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    …

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        DropboxClientsManager.setupWithAppKey(“”)
        return true
    }

これでクライアントに固有キーを設定できるようになりました。

2. ログイン認証の準備(認証ボタンの作成)

次にdropboxの認証を行います。 …が。
アプリが起動したら勝手に認証するのでは、ユーザーがビックリするので、
認証用のボタンを用意することにします。

まずは、左側のMain.storyboardをクリックして、InterfaceBuilderを開きます。(①)
そして、左下のウィンドウからButtonを画面レイアウトの上にドラッグします。(②)

次にボタンの位置を左下で固定するための設定を行います。
左下の方にあるアイコンをクリックして、設定画面を出します。(③)

設定画面の上側にある上下左右のスペースの設定で、下と右に16を入力します。(④)
ついでにその下にあるチェックボックスもチェックを外します。(⑤)
これがチェックされていると、思ったよりスペースが空いて表示されたりします。

次に右上の方に並んでいるアイコンの左から2つ目をクリックします。(⑥)

すると画面が分割されてコードが出てきます。
次に配置したボタンの上で”control”キーを押しながらクリックします。(⑦)
黒いメニューが出てきたら、”Touch Up Inside”という項目の右側にある”+”をソースコードにドラッグします。(⑧)

追加するメソッド名を入力して、”connect”をクリックします。
今回は、”pushAuthentication” というメソッド名にしました。
これで、ボタンが押されたら”pushAuthentication”が呼ばれるように設定できたことになります。

3. ログイン認証

さて、今度は右上に並んでいるアイコンの一番左をクリックしてから、ソースに戻りましょう。
それでは、認証コードを記述します。
追加されたpushAuthenticationメソッドに下記を書きましよう。
ここでもSwiftyDropboxライブラリを使いますので、import文も追加するのを忘れずに。

import UIKit
import SwiftyDropbox

class ViewController: UIViewController {
    ……
    @IBAction func pushAuthentication(_ sender: Any) {
        DropboxClientsManager.authorizeFromController(UIApplication.shared,
                                                      controller: self,
                                                      openURL: { (url: URL) -> Void in
                                                        UIApplication.shared.openURL(url)
        })
    }

これだけで、ログインの認証画面が出るようになりました。
試しに実行してみると…
※ ネットワークに繋がっていないと、エラーダイアログが出ますので、インターネットに繋がっている状態で実行してください。

こんな感じでログインしたような感じがありますね。
ただ、これではちゃんとログインができたのかよくわからないので、
最後に結果を受け取れるようにしましょう。

4. 結果の受け取り

次のコードをApplication delegeteに追加します。

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    if let authResult = DropboxClientsManager.handleRedirectURL(url) {
        switch authResult {
        case .success:
            print("Success! User is logged into Dropbox.")
        case .cancel:
            print("Authorization flow was manually canceled by user!")
        case .error(_, let description):
            print("Error: \(description)")
        }
    }
    return true
}

これで、認証の成功、キャンセル、エラーがログに出力されるようになります。
では、もう一度実行して見ましょう。
下のようにログが出れば、認証が成功しています。

2017-05-25 16:25:22.479267+0900 dropboxtest[24899:9093631] [MobileAssetError:29] Unable to copy asset information from https://mesu.apple.com/assets/ for asset type com.apple.MobileAsset.TextInput.SpellChecker
Success! User is logged into Dropbox.

ちなみに、ログイン画面でキャンセルすると、次のようになります。

017-05-25 16:28:05.973 dropboxtest[24899:9093044] -canOpenURL: failed for URL: "dbapi-2://1/connect" - error: "The operation couldn’t be completed. (OSStatus error -10814.)"
2017-05-25 16:28:05.974 dropboxtest[24899:9093044] -canOpenURL: failed for URL: "dbapi-8-emm://1/connect" - error: "The operation couldn’t be completed. (OSStatus error -10814.)"
Authorization flow was manually canceled by user!

エラーは発生のさせ方が分からなかったので、発生させることができたら、追記しようと思います。

さ〜、これでやっとログイン完了まできました。
次はいよいよ、Dropboxからデータを取ってくる部分となります。
それでは、また次回をお楽しみに!

iOSでdropbox連携を行う(第1回)

現在、E-kanのiOSアプリ開発勉強会でdropbox連携について勉強していますが、
折角ですので、その方法についてこのブログで連載しようと思います。

最終的には、dropboxのファイル一覧を表示させて、
ファイルを選択したらそのファイルを開くというアプリを作る予定で、
全部で4~5回くらいの連載になるかな?と思います。

さて、早速ですがその初回では、
新規プロジェクトの作成とdropboxライブラリの使用準備まで進めたいと思います。

1. プロジェクト作成

では、XCodeで新規プロジェクトを作成しましょう。
ここは基本的な部分ですので、詳細手順は割愛させてもらいます。
今回は、単純な画面のアプリとなるので、
テンプレートは”Single View Application”で作成してください。
プロジェクト名は単純に “dropboxtest” とします。

2. SwiftyDropboxの組み込み

次に作成したプロジェクトへdropboxライブラリを組み込みましょう。
ライブラリの組み込みはCocoaPodsを使います。
他にもCarhageを使う方法などがありますので、そちらを使いたい方は公式ドキュメントを見てください。

もし、CocoaPodsを使ったことがないという場合は、先にCocoaPodsをインストールしてください。
ターミナルで下を実行するだけでインストールできます。

$ gem install cocoapods

(実行でエラーが出てしまう場合は、”sudo”を追加して実行してみてください)

それでは、実際に組み込んでいきましょう。
まずは、CocoaPodsでのライブラリ設定を行うので、
ターミナルで作成したプロジェクトのディレクトリに移動します。
そこで下のコマンドを実行します。

$ pod init

実行が終わると、”Podfile” というファイルが作られるので、
それを開いて、次のように赤字部分を追記します。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'dropboxtest' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for dropboxtest
  pod 'SwiftyDropbox'

  target 'dropboxtestTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'dropboxtestUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

Podfileの修正が終わったら、インストールします。
下記コマンドを実行してください。

$ pod install

これで、プロジェクトにSwiftyDropboxというライブラリが取り込まれました。
確認の為にプロジェクトを開いて見ましょう。
開くのは dropboxtest.xcworkspace です。(xcodeprojではないので注意してください)

ちゃんと SwiftyDropbox のフレームワークが追加されています。

3. アプリ登録

次は、dropboxのサイトにてアプリ登録を行います。
普通に dropbox のサイトでログインすると、右下の”…”があると思いますので、
そこの”開発者向け情報”をクリックします。

すると、次のような画面になるので、”My apps”の”Create app”をクリックします。

アプリの情報入力画面になりますので、適当に入力しましょう。
1つ目の設問は”Dropbox API”になると思います。
2つ目は使い方に合わせて選択してください。
App folder  ・・・ アプリ用のフォルダを作り、その中だけの読み書きを行う
Full Dropbox ・・・ 全てのフォルダ内の読み書きを行う
3つ目はアプリ名の入力です。
いくつか入力に制約があるので、気をつけてください。
(dropbox とか test とかつけたら弾かれました)
実際のアプリ名と同じじゃなくても大丈夫みたいです。

次は色々な設定画面です。
ここでは2点だけやることがあります。
まずは、”Development users” の “Enable additional users”をクリックします。

ダイアログが表示されるので、”Okay”を押します。

これをやらないとログイン認証することができないので、必ずログインが失敗します。

次に”App key”と”App secret”の値をメモします。
(App secretは show をクリックすると文字列が表示されます)

これでアプリ登録が完了しました。

4.キー情報の設定

初回の最後は Info.plist の編集です。
公式Dropboxアプリと認証のやりとりをする為に2点の設定追加が必要となります。
Info.plistはプロジェクトフォルダの”dropboxtest”の中にあります。
もちろん、XCode上で編集しても大丈夫です。(コツが必要ですが)

1つ目
公式Dropboxアプリがインストールされているかどうかを判断できるようにする設定です。

<key>LSApplicationQueriesSchemes</key>
   <array>
      <string>dbapi-8-emm</string>
      <string>dbapi-2</string>
   </array>

2つ目
公式DropboxアプリからのURLスキーム起動を行えるようにする設定です。
※ db-の後ろのモザイク部分は手順3で確認した “App key”を入力してください。

<key>CFBundleURLTypes</key>
   <array>
      <dict>
         <key>CFBundleURLSchemes</key>
         <array>
            <string>db-<APP_KEY></string>
         </array>
         <key>CFBundleURLName</key>
         <string></string>
      </dict>
   </array>

この2つの修正が終わったら下のような感じになります。

ちなみに、XCode上ではこんな感じ。

さぁ、これで、SwiftyDropboxを使う準備が整いました。
次回は実際に使って、ログイン認証を行おうと思います。