見出し画像

PythonでAndroidアプリ作成してみる #8 バックアップ機能詳細

こんにちは、Rcatです。
前回やっとの思いでパソコンでもスマートフォンでもファイルの送信を行えるところまでやりました。
今回はバックアップということで、ファイルの送信方法や差分の検索方法などを考えていこうと思います。

本シリーズはこちら


ファイルの存在チェック

ファイル情報のリストについて

まずはバックアップを効率的に行うため、ファイルの存在チェックを行います。
具体的には、すでに同じファイルがあるかどうかを判定し、その次に更新日時が同じかどうかを確認します。
ファイルに変更がある場合は、新しいデータで上書きをしそうでない場合はスキップすることで効率を上げます。
また、バックアップ元からなくなったデータに関しては差集合で差分を出して削除します。

フォーマットとしては次のように考えています。
このような形で確認を行おうと思います。1件ずつ確認を行ってもいいのですが、レスポンスが激遅になることを避けるためまとめてやります。

ファイル有無チェックフォーマット(JSON)
{
	"FileName":{
		"id":ファイルID,
		"mtime":更新日時,
		"kind":ファイル/フォルダ
	}
}

というわけでソースがこちら
データ容量を軽くするために、絶対パスを送るのではなくどこのフォルダかという情報とファイル名のリストを挙げています。
こうすることで、サーバー側でフォルダと名前をくっつければファイルパスになるわけですね。
また、各ファイルごとにいくつか情報を持っています。
特に大事なのはmtimeで、これはファイルの更新日時を表します。
サーバーはファイルを保存しますが、厳密な処理で言えば空のファイルを作成して、そこに受信したバイナリーを書き込むです。つまり、更新日時がそのタイミングになってしまうというわけですね。
しかし、今回はバックアップが目的なので、そのあたりの情報は保持していただかないと困ります。というわけで、更新日時の情報なども送っているわけですね。

ファイルの探索方法について

今回は次のような方法でファイルの探索を行います。
まず設定されたフォルダーをルートディレクトリーとして扱います。
そのルートディレクトリの中の全てのディレクトリを一番最初の関数で検索し、それに対してループをかけていきます。
一つ一つのフォルダに入って再帰的に検索を実行しても良かったのですが、ちょっと複雑になりそうだったので、globの再帰検索でフォルダは一気に洗い出すことにしました。
そして、各フォルダごとに先ほど紹介した中身のデータリストを作成し、サーバーに送信します。
サーバーからは必要なデータのリストが返されるので、それを使ってファイルのアップロードに進みます。

サーバー側のファイルチェックについて

サーバー側は以下のようになっています。少し長いです。
とはいえ、そんなに難しいことはありません。
まず、最初のループでクライアントから送信されたリストに対してループをかけます。そしてそのデータの有無を確認するわけですね。
この時にこちらにないデータをよこせというneedリストを作成します。
また、クライアントにあってサーバーにないファイルを検索するためにset型のリストの作成も同時に行っています。

その次はクライアント側にないアイテムの削除です。
今回はミラーリングのようなバックアップを目的としているので、クライアントから削除されたデータはサーバーからも削除します。
そのため、サーバーのバックアップデータを検索し、こちらもセット型に代入しておきます。
最後に差集合を計算し、差分があればその分を削除するという仕組みです。全ての処理が終わった後、クライアントに要求リストを返します。

後付ですが、削除に関してはスレッドで非同期でやっても良さそうですね。

ファイルのアップロード

アップロード関数

下記がファイルをアップロードする関数になります。
上1/3くらいはクロージャー関数が混ざっています。
この関数はサーバーから返ってきた必要リストに応じてファイルを送信する関数です。
アップロードに関して、前回の実験からいくつか変更があります。
まずは送るデータの量が増えていることです。前回はこのファイルをどこのディレクトリに置けばいいのかという情報のみを送っていましたが、今回からファイル名や更新日時などの情報も送っています。
また、当初の予定でもありましたが、サイズの大きなデータを送信した際に失敗したので、ファイルの分割を上で定義した関数で行っています。
あまりサイズを大きくすると、なぜかスマホでうまくいかなかったので、現状を50mbで分割ということにしています。
また、ループの待機方法にも変更があります。前回はリクエストを投げた後wait()で待機していたのですが、これが非常に遅く1秒に1リクエスト、つまり、ファイルを送信時間+1秒かかるというような状態でした。
そこでライブラリの中を少し深掘りしたところ、使って欲しくなさそうな名前ですが、"_dispatch_result"というもう少し根本的な部分の関数を見つけました。
このライブラリは結構面倒な仕様でレスポンスが完了したかどうかを確認するには一度wait()を呼び出さなくてはいけないのですが、これが内部でスリープを使っており、無駄に時間がかかります。そのため、内部で使っている関数を直接呼び出すような感じにしています。
また、スレッディングを継承しているようなので、待たなければ無限にリクエストのスレッドが増えて行くような気がするので、一応フォルダごとにファイルの送信が全て終わってから次に進むような感じにしています。
ただし、ファイルの分割部については少々違いがあります。
他のファイルは送信したら、さっさと次のリクエストを送って、全て送り終わったらリクエストの完了状況を確認するという方法なのですが、なぜかファイルを分割した場合はそれはうまくいきませんでした。
なので、ファイルを分割する場合はリクエストごとに一時停止しています。ただし、引数に小さい数値を入れることで、スリープ時間を短くできるので、少しでもレスポンスを向上させることができています。

サーバー側での受信を処理

さて、クライアントを見たところで次はサーバー側を見ていきましょう。
サーバー側で受け取るcgiは下記の通りになっています。
まず最初にヘッダー情報の取得ですね。ここでファイルの名前やフォルダの位置、更新日時などの情報を受け取ります。
今回ポイントなのは送信するファイルを分割する前提なので、直接ファイルオブジェクトをアップロードしていないため、ファイル名が取得できないところです。そのため、別途ファイル名もフォーム情報として送っています。

そしてデータの受信部分です。
ファイルの分割についてはブロックサイズという定義にしており、これが1であれば分割されていないのでそのまま保存です。
しかし分割されている場合はグローバル変数に一旦代入し、全てのブロックが揃ったところで保存ということを行っています。

細かい部分については、今後配布を予定しているソースコードの方をご確認ください。

AndroidスマホでPythonの"print"を確認する方法

話は少しそれますが、先ほどから結構printの部分が残してあるのにお気づきでしょうか?
パソコンの場合はコンソールで見られるからわかりますが、Androidの場合って確認できるのかって気になりませんか?
結論できます。

確認するにはみんな大好きUSBデバッグを使います。
開発者モードからUSBデバッグをオンにした後、ADBコマンドを使ってログを表示するだけです。
この時のポイント"-SオプションのPythonをつける"です。
きちんとフィルターをかけないと大量のログが流れてきて見たい部分がどこなのか全く分かりません。
また、トラックバックのエラーなども全てインフォメーションレベルなので、エラーレベルなどで絞ってもまず出てきません。
というわけで、下記のようなコマンド入力を行うことで、Pythonのコンソール出力を得ることができます。
例えば一番下の部分は分割データを送信した時のプリントログですね。

>adb logcat -s python
--------- beginning of main
04-28 20:09:48.455 26185 26226 I python  : Initializing Python for Android
04-28 20:09:48.554 26185 26226 I python  : [INFO   ] [Logger      ] Record log in /data/user/0/rcat999.jp.jp.rcat999.rcatbk/files/app/.kivy/logs/kivy_24-04-28_2.txt
04-28 20:09:48.554 26185 26226 I python  : [INFO   ] [Kivy        ] v2.3.0
04-28 20:09:48.554 26185 26226 I python  : [INFO   ] [Kivy        ] Installed at "/data/user/0/rcat999.jp.jp.rcat999.rcatbk/files/app/_python_bundle/site-packages/kivy/__init__.pyc"
04-28 20:09:48.554 26185 26226 I python  : [INFO   ] [Python      ] v3.11.5 (main, Apr  7 2024, 19:24:53) [Clang 14.0.6 (https://android.googlesource.com/toolchain/llvm-project 4c603efb0
04-28 20:09:48.555 26185 26226 I python  : [INFO   ] [Python      ] Interpreter at ""
04-28 20:09:48.555 26185 26226 I python  : [INFO   ] [Logger      ] Purge log fired. Processing...
04-28 20:09:48.555 26185 26226 I python  : [INFO   ] [Logger      ] Purge finished!
04-28 20:09:48.865 26185 26226 I python  : [INFO   ] [Image       ] Providers: img_tex, img_dds, img_sdl2 (img_pil, img_ffpyplayer ignored)
04-28 20:09:48.875 26185 26226 I python  : [INFO   ] [Text        ] Provider: sdl2
04-28 20:09:48.946 26185 26226 I python  : [INFO   ] [Factory     ] 195 symbols loaded
04-28 20:09:49.029 26185 26226 I python  : [INFO   ] [Window      ] Provider: sdl2
04-28 20:09:49.064 26185 26226 I python  : [INFO   ] [GL          ] Using the "OpenGL ES 2" graphics system
04-28 20:09:49.065 26185 26226 I python  : [INFO   ] [GL          ] Backend used <sdl2>
04-28 20:09:49.065 26185 26226 I python  : [INFO   ] [GL          ] OpenGL version <b'OpenGL ES 3.2 v1.r46p0-01eac1.6b76d861277b3ea6941f5aa972def735'>
04-28 20:09:49.066 26185 26226 I python  : [INFO   ] [GL          ] OpenGL vendor <b'ARM'>
04-28 20:09:49.066 26185 26226 I python  : [INFO   ] [GL          ] OpenGL renderer <b'Mali-G78'>
04-28 20:09:49.066 26185 26226 I python  : [INFO   ] [GL          ] OpenGL parsed version: 3, 2
04-28 20:09:49.066 26185 26226 I python  : [INFO   ] [GL          ] Texture max size <16383>
04-28 20:09:49.066 26185 26226 I python  : [INFO   ] [GL          ] Texture max units <64>
04-28 20:09:49.079 26185 26226 I python  : [INFO   ] [Window      ] auto add sdl2 input provider
04-28 20:09:49.080 26185 26226 I python  : [INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
04-28 20:09:49.132 26185 26226 I python  : [INFO   ] [GL          ] NPOT texture support is available
04-28 20:09:49.139 26185 26226 I python  : [WARNING] [Base        ] Unknown <android> provider
04-28 20:09:49.139 26185 26226 I python  : [INFO   ] [Base        ] Start application main loop
04-28 20:10:03.095 26185 26226 I python  : /storage/emulated/0/DCIM/Camera/VID_20190502_002956.mp4
04-28 20:10:03.529 26185 26226 I python  : リクエスト完了0 {'fdir': 'DCIM/Camera', 'fname': 'VID_20190502_002956.mp4', 'mtime': 1601736737.0, 'blocksize': 4, 'current_block': 0, 'id': 'INQMKJPHsu201003'}
04-28 20:10:05.427 26185 26226 I python  : リクエスト完了1 {'fdir': 'DCIM/Camera', 'fname': 'VID_20190502_002956.mp4', 'mtime': 1601736737.0, 'blocksize': 4, 'current_block': 1, 'id': 'INQMKJPHsu201003'}
04-28 20:10:07.031 26185 26226 I python  : リクエスト完了2 {'fdir': 'DCIM/Camera', 'fname': 'VID_20190502_002956.mp4', 'mtime': 1601736737.0, 'blocksize': 4, 'current_block': 2, 'id': 'INQMKJPHsu201003'}
04-28 20:10:08.669 26185 26226 I python  : リクエスト完了3 {'fdir': 'DCIM/Camera', 'fname': 'VID_20190502_002956.mp4', 'mtime': 1601736737.0, 'blocksize': 4, 'current_block': 3, 'id': 'INQMKJPHsu201003'}

まとめ

今回はアプリの作り込みを行っていきました。
とりあえずここまででバックアップアプリとして使用する目処が立ちました。
次回はバックアップ中、アプリは完全にフリーズしているので、そこら辺どうにかできないか、もう一度guiの深堀りを行っていきたいと思います。
それではまたお会いしましょう。


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