pysmbを使ってPythonでWindowsファイルサーバにアクセスする

Windowsファイルサーバ(SMB)をスクリプトから操作するとき、ネイティブにはPowerShellですが、Pythonで実装するとLinuxでも使えるので便利です。
昨今Linuxのファイラなら大抵のディストリビューションでサポートされていますし、コマンドラインだとmount cifsでマウントして操作する手もありますが、やはり自動化したいケースがあります。

Pythonだとpysmbが有名なようです。そこで今回はpysmbを使ってみます。

もくじ:

はじめに

pysmbも、みんな大好きpipでインストールできます。

基本はSMBConnectionクラスを使ってコネクション作成、ファイル参照・操作、コネクション破棄、という流れになるようです。

毎度長くなりますが、はじめにサンプルSmbClient.pyを記載しておきます。pysmbのAPIを組み合わせて、ファイル操作周りの機能を補助的に追加したSmbClientクラスを定義しています。例外の補足が面倒だったのでちゃんとやっていませんが、基本的には問題があれば止まるようになっています。
なお、環境はWindows 10 + Python3.6で検証しています。

まあ見たまんまなのですが、内部動作と落とし穴っぽいところを以降に記載していきます。

サービス名とパス

用語の話なのですが、サービス名(service_name, svc_name)やらパス(remote_path, path_)という言葉が出てきます。

サービス名はSMBサーバにアクセスしたときに、ディレクトリのアイコンではなくこんなアイコン↓で表示されるやつ(共有フォルダ)です。

(リモート)パスは残りのパスです。
つまり、UNCで書けば「\\(ホスト名)\(サービス名)\(パス)」という構造になっています。

コネクションの作成

サンプルでは、コンストラクタでコネクションの作成に必要な情報confをセットしています。
このクラス独自に定義しているものですが、SMBConnectionオブジェクトの作成に必要な項目をまとめたものと考えて下さい。
単なるディクショナリなので地道に定義してもいいのですが、既定値をスタティックメソッドで取得できるようにしています。

この情報を使って、プライベートメソッド__get_connectionで内部的にSMBConnectionオブジェクトを作成しています。
渡す値が間違っていたり、サーバと通信できないときは例外が投げられます。

内部的にはサーバのIPアドレスやらクライアントのホスト名が必要になるようなのですが、これはコンストラクタ内でsocketモジュールを使って解決させています。

Windowsユーザ諸兄にはお馴染みだと思いますが、それなりに歴史の長いSMBにはいくつかバリエーションがありますので、うまく行かない場合はいろいろ設定を試してみて下さい。

ファイルの参照

ではまずサンプルの.list_dirメソッドを使って、ファイルやディレクトリの一覧を取得する例です。パスワードはgetpassモジュールを使って、実行中にコンソールから入力させています。

実行結果はこんな感じ。この使い方だと、指定されたパス(ディレクトリ)にあるサブディレクトリとファイルの情報をリストとして返します。

以下、pysmbのAPIを呼び出している部分の解説です。

リモートパスの確認

ファイルやディレクトリの内容ではなく属性(メタ情報)を知りたい場合には、SMBConnection.getAttributes()を使います。
指定されたリモートパスが存在しない場合や、アクセス権限がなかった場合には例外を投げてきます。

ここではディレクトリか否かを判定するためにisDirectoryプロパティを参照しています。

ディレクトリ内の要素取得

SMBConnection.listPath()は、ローカルで言うところのos.listdir()みたいなやつです。ここではisDirectoryプロパティで判定して、サブディレクトリが見つかれば再帰的に取得し、ファイルであれば整形してリストに格納しています。

SharedFileインスタンスの解釈

SMBConnection.listPath()はSharedFileインスタンスを返してきます。
ここでは全て無理やり文字列にフォーマットしていますが、フォーマットのやり方を見れば各プロパティの元の型がわかると思います。

ファイルの操作

サンプルの.receive_itemsメソッドや.send_fileメソッドの使用例はこんな感じです。

ダウンロードのときは、ローカルとリモートのディレクトリをそれぞれ指定します。また、アップロードの時はローカルにあるファイルを指定し、リモートのディレクトリに同じ名前のファイルとしてコピーします。

とくにコンソール出力はないので、実行結果は省略します。

ファイルのダウンロード

一応、リモートのファイルを指定したときとディレクトリを指定したときの両方に対応できるよう挙動を変えています。ディレクトリの場合はそのリモートディレクトリ以下のファイルやサブディレクトリも対象にできます。

リモートのファイルが指定された場合にはそのまま、もしくはディレクトリが指定された場合には上記の例と同様に.__get_filesメソッドでファイルのパスを取得し、.__downloadメソッドに渡します。内部的にはローカルファイルをバイナリ書き込みモードでオープンし、SMBConnection.retrieveFile()メソッドに渡しています。

なおサンプルの実装では、ファイルのみを対象にしているので、リモートにある空のディレクトリはローカル側で作成されません。

ファイルのアップロード

アップロードに関しては、あまり複雑なことをしていません。ディレクトリに対応するのが面倒だったので。ローカルのファイルをバイナリ読み込みモードで開き、SMBConnection.storeFile()メソッドに渡しています。