見出し画像

PythonでAndroidアプリ作成してみる #6 kivy UrlRequestでファイルアップロードをする

こんにちはRcatです。
前回はアプリとサーバーでのデータやり取りの部分を検討/構築しました。
しかし、PCでは上手く行ったもののAndroidでは即落ちする問題が発生してしまいました。
ということで今回はその辺解決できるのかどうか試していきます。

本シリーズはこちら


悪あがき1 threading

まずは既存の知識で悪あがきしていました。
書かなくてもいいようなことですが、試したことは書きたいので書きます。

適当に調べたところ、requestsが応答待ちの間イベントが止まるのがダメらしい?
というわけでthreadingでアップロード関数回せばいいのではと考えた
ソースは以下の通り

def BackUpButton_Click(self):
		"バックアップボタン押下時のイベント"
		if self.ids.Profiles.text in Config.Config["profiles"]:
			th = threading.Thread(target=self.BackUP_Requests)
			th.start()
#-------------------------------------------------------------------------------------------
	def BackUP_Requests(self):
		if self.ids.Profiles.text in Config.Config["profiles"]:
			p = Config.Config["profiles"][self.ids.Profiles.text]
			l = [f for f in glob.glob(os.path.join(p["dirs"][0],"**/*.*"),recursive=True) if os.
			for i in l:
				files = {'Upload': open(i, 'rb')}
				data = {'fdir': os.path.relpath(os.path.dirname(i),os.path.dirname(p["dirs"][0]))}
				response = requests.post('http://192.168.0.199:27510/backup_uploadvgi', files=files, data=data)

結果…
何も起こりませんでした。
ラベルにプリントして処理を見てみるとやはりpostの行より先に進みません。
アプリが落ちないのは、スレッドが落ちるだけなのでアプリが落ちるわけではなくなっただけのようです…。

ちなみにこの後も非同期のリクエストなど試しましたが、全てダメ。
そのまま約1日が過ぎて行ったのでした。

UrlRequest (kivy.network.urlrequest.UrlRequest) を使う

UrlRequestとは

色々調べたんですが、どうやらkivy専用のrequestライブラリが用意されているようです。わざわざライブラリを用意するあたり、標準ライブラリやその他のライブラリと相当相性が悪いのですね…。
それでリファレンスを見てみたのですが、ちょっと都合が悪い感じです。
下記を見てわかるとおりリクエストは可能なのですが、requestsライブラリでは存在したファイルを送信する引数がありません
さて、ファイルは送信できるのでしょうか?

#公式リファレンスから抜粋
from kivy.network.urlrequest import UrlRequest
req = UrlRequest(url, on_success, on_redirect, on_failure, on_error,
                 on_progress, req_body, req_headers, chunk_size,
                 timeout, method, decode, debug, file_path, ca_file,
                 verify)
#https://kivy.org/doc/stable/api-kivy.network.urlrequest.html

リファレンスから関数の定義を確認すると、ヘッダやボディを指定することができるようなので、この辺の仕組みを深掘りして直接ファイルをアタッチできればまだ希望はあるのではと考えました。
ヘッダーやボディとはPOSTでデータをアップロードする時のデータ本体のことです。

リクエストヘッダーやリクエストボディについて調べる

調べると言っても、そもそも現状どういうことをしているのかがわからないと何を調べていいのかわからないので、まずはrequestsライブラリがどういったヘッダとボディを使用しているのか見てみました。

下記のように一度手動でポストを行った後、返ってくる応答オブジェクトの中からリクエストに使用したデータを見ることができます。

>>> response = requests.post('http://192.168.0.199:27510/backup_uploadcgi', files=files, data=data)

>>> response.request.headers
{'User-Agent': 'python-requests/2.31.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 
'Content-Length': '92349', 'Content-Type': 'multipart/form-data; boundary=80d7aa3d4743f0990cf97ac287e47604'}


>>> response.request.body
b'--80d7aa3d4743f0990cf97ac287e47604\r\nContent-Disposition: form-data; name="dfir"\r\n\r\ntest/test\r\n--80d7aa3d4743f0990cf97ac287e47604\r\n
Content-Disposition: form-data; name="upload"; filename="m28409472219_2.jpg"\r\n\r\n\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00...
...中略...
7f\xff\xd9\r\n--80d7aa3d4743f0990cf97ac287e47604--\r\n'

この中で注目すべきところは2点です。
まずはContent-Typeです。これは投稿されたデータがどういうものなのかを表すもので、私も直接は触ったことはありませんがいくつか種類があることは知っていました。そのため、これの種類が何なのかをまず判別させる必要があります。
今回はmultipart/form-dataのようです。これは入力用のテキストボックスやチェック、ボックスなどを含み、さらにファイルのアップロードまで付いた複数要素のフォーム送信で使われるものですね。
確かに、私はファイルのアップロードとプロパティのアップロードを行っているので、この形式を使うのは正しいでしょう。

そしてポディの方を見てみましょう。
どうやら中身についてはそれぞれ種類の指定があるわけではなさそうです。
とりあえず送信する要素をバウンダリー文字列でスプリットしている感じでしょうか。

UrlRequestを使ってみる

とりあえず公式の例を見てみましょう。
あれ、私の使いたいものとちょっと違う感じですね。
コンテンツタイプが"application/x-www-form-urlencoded"という、また見たことないものになってます。というわけでこれは何なのか博士に聞いてみるとしましょう。

import urllib

def bug_posted(req, result):
    print('Our bug is posted!')
    print(result)

params = urllib.urlencode({'@number': 12524, '@type': 'issue',
    '@action': 'show'})
headers = {'Content-type': 'application/x-www-form-urlencoded',
          'Accept': 'text/plain'}
req = UrlRequest('bugs.python.org', on_success=bug_posted, req_body=params,
        req_headers=headers)

#公式サイトから抜粋
#https://kivy.org/doc/stable/api-kivy.network.urlrequest.html

というわけで聞いてみました。なんかGETみたいな感じですね。

今回使いたいmultipart/form-dataとの差を聞いてみました。
なるほど、バイナリを使う場合には確実にこっちを使うんですね。
確かに上側の方がテキストボックスなどの中身を送る分には軽量で適しているかもしれませんが、使い分けるのもめんどくさいですし、基本的にはこちらで良さそうですね。

リクエストヘッダーとボディを生成する

さて、例題では今回やりたいことと違うことをしていますが、要は自分でヘッダーやボディを生成してここにくっつければいいわけですね。
次の問題は、どうやって生成すればいいかというところですが、今まで使っていたrequestsライブラリーからうまく抜き出せないか見ていました。
結果、リクエストを完了さえすれば、ヘッダーやボディをオブジェクトとして参照できるのですが、そもそもリクエストが成功していればこんなことせずに済んでいるわけで…。
というわけでライブラリの中身を見て、どの部分でこれを生成しているのか確認していきます。

requests.post関数から中身を順々に追って行ったところ、次のような事実が判明しました。
数階層先に進んだところで、ヘッダーを準備する段階を発見しました。
この関数の名前やってることがそのままでした。
この関数に至るまでファイルやデータは特に編集されていません。

この関数に対してURLが必須引数で残りが任意の引数でした。
というわけで、URLはローカルホストに仮置きして、ファイルとデータの引数を渡して肝心のデータを生成してみます。

>>> f=open("m28409472219_5.jpg","rb")
>>> file={"Upload":f}
>>> data={"fdir":"test/test2"}
>>> rp=requests.models.PreparedRequest()         
>>> p=rp.prepare(url="http://localhost",files=file,data=data)
>>> rp.headers
{'Content-Length': '70360', 'Content-Type': 'multipart/form-data; boundary=adfb020375c65aabc37758fd26b640c7'}
>>> rp.body
b'--adfb020375c65aabc37758fd26b640c7\r\nContent-Disposition: form-data; name="fdir"\r\n\r\ntest/test2\r\n--adfb020375c65aabc37758fd26b640c7\r\nContent-Disposition: form-data; name="Upload"; filename="m28409472219_5.jpg"\r\n\r\n\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\
中略
\xf2\xd5\x9e_\xff\xd9\r\n--adfb020375c65aabc37758fd26b640c7--\r\n'

予想通りリクエスト後に生成できたものと同じデータを生成することに成功しました。
これを元にアップロード用のリクエストを実装していきましょう。

実装する

最終コードの作成

さて、ここまでやっと来てまいりました。
現状の最終的な実装は下記の通りです。
for文の中でファイルを一つ一つアップロードしていきます。
ファイルを開くと、ファイルオブジェクトとデータオブジェクトの生成を行い、先ほど紹介したテクニックでリクエストヘッダーとボディを生成します。
最後にUrlRequestライブラリを使用して、その引数にボディとヘッダーを渡して実行します。

実行結果

ファイルのアップロードに成功しました。
ただ、同期したからなのか超遅いです。
少なくともrequestsなら目に追えない速度でログが流れるはずなのですが、こちら1秒に1ファイルくらいのスピードでログが流れていきます…遅…。
何はともあれkivy.network.urlrequest.UrlRequestを使用したファイルのアップロードは成功させることができました。

追記:#12にて高速化しました

どれくらい遅いかは以下の動画をご覧ください(撮影は#7で行いました)

スマホで起動する

いよいよ本番のスマホでのアップロードを試してみます。
では早速ポチ!フリーズしました☆

この後少し修正して確認してみましたが、リクエストを投げる時点で止まってるっぽいです。
何と言うか、ここまで来るとkivy云々ではなくて、スマホとアプリのネットワーク間の問題な気もしてきました…。とは言ったものの、普通アプリって通信っていう権限はないはずなんですよね…。それとも何か見落としてる?

実はインターネットアクセスに権限が必要な件

そろそろ手がないと思いつつ、適当にネットサーフィンしつつ苦手な英語のサイトもちらりと見ながら巡っていたところ"android.permissions = INTERNET"という表記を見つけました。これはつまりインターネットアクセスには権限がいるということですね。
どんなアプリでもさも当然のように通信していたので、当たり前かと思ってたんですが、そうではないんですね。

ファイル検索と権限の件で参照した権限一覧を確認するとありました。
というわけで、こちらをビルドーザーのファイルに追記します。

android.permissions = android.permission.MANAGE_EXTERNAL_STORAGE, android.permission.MANAGE_MEDIA, android.permission.READ_EXTERNAL_STORAGE, android.permission.WRITE_EXTERNAL_STORAGE,android.permission.INTERNET

やっとアップロード

インターネットアクセスの権限を追加後、再度インストール…。
ボタンを押すとアップロードに成功しました。サーバー側にアクセスログが流れていきます。

アップロードされたデータもこの通りきちんと壊れることなくアップロードできているようです。

長かった…本当に長かった。
たかがアップロード1本やるだけで2日かかって…

まとめ

今回はファイルのアップロードだけで1本終わってしまいました。
kivyは情報が少ないとは聞いていましたが、まさかファイルをアップロードしている情報が一つも出てこないとは思いませんでした。
さらに追い討ちをかけるように、サポートされるライブラリと普段使いのリクエストも使い勝手が全然違い非常に大変でした。
しかし、収穫としてポストの仕組みをより深く理解することができたのかと思います。これがわかっていれば、これから低水準のライブラリしか使えない環境でもファイルのアップロードなどができそうですね。

次回はこの辺の機能を深掘りでファイルの差分を見てアップロードできるようにしていこうと思います。また、ファイルの更新日時の変更なども行い。ファイルの有無だけでなく、更新の有無でも差分を判別できるようにします。

最後に、権限なかったから、requestsライブラリが落ちてたんじゃないかと思わなくもない…。そのうち試すかもしれない。

とりあえず本シリーズ中止は免れたので、それではまたお会いしましょう。

情報が役に立ったと思えば、僅かでも投げ銭していただけるとありがたいです。