pysmbの使い方（ファイル受信）

[前回](https://qiita.com/t2kojima/items/5db8dadaa6e07321d25f)に続き、pysmbを使ってファイルの受信をしてみたいと思います。

# インデックス
[pysmbの使い方（接続・切断）](https://qiita.com/t2kojima/items/5db8dadaa6e07321d25f)
pysmbの使い方（ファイル受信）
[pysmbの使い方（ファイル送信）](https://qiita.com/t2kojima/items/a6b81de771ac45c9948b)
[pysmbの使い方（ロギング）](https://qiita.com/t2kojima/items/4fc5e2e2c041b2c16824)
[pysmbの使い方（匿名接続）](https://qiita.com/t2kojima/items/c84a803c73daf043f053)

# 実行環境

#### クライアント
- Windows 10
- Python 3.6.3
- pysmb 1.1.22

#### サーバー
- Windows 7
    - User : IEUser
    - Pass : Passw0rd!
    - HostName : IEWIN7
    - Domain : WORKGROUP
    - IPAddress : 172.28.0.198

サーバーの共有フォルダ`Share`に`source.txt`を配置している。

# ファイル受信

ファイル受信は以下のように行います。（インスタンスの作成や接続・切断処理は割愛）

```python
with open('dst.txt', 'wb') as file:
    conn.retrieveFile('Share', 'source.txt', file)
```

`source.txt`が受信するサーバー側のファイル、`dst.txt`はクライアント側に作成されるファイルです。
`SMBConnection#retrieveFile`メソッドを使用します。定義は以下の通り。

```python
def retrieveFile(self, service_name, path, file_obj, timeout = 30):
```

- service_name  
サーバー側の共有フォルダ名
- path  
目的のファイルのパス
- file_obj  
書き込むファイルのファイルオブジェクトを渡してあげる
- timeout  
タイムアウト値です。デフォルト30秒

## フォルダがある場合

受信先がフォルダ配下の場合、当然フォルダは存在していなければなりません。

```python
with open('tmp/dst.txt', 'wb') as file:
    conn.retrieveFile('Share', 'source.txt', file)
```

この場合で`tmp`フォルダがない場合、`FileNotFoundError`になります。

受信元がフォルダ配下にある場合、下記のように指定すれば良いです。

```python
with open('dst.txt', 'wb') as file:
    conn.retrieveFile('Share', 'tmp/source.txt', file)
```

ちなみに受信元のファイルが存在しない場合は`smb_structs.OperationFailure`になります。

## 戻り値

必要になることはまず無いと思いますが…`retrieveFile()`の戻り値として**ファイル属性（int）**と**受信バイト数（int）**のタプルを受け取ることができます。
ファイル属性とは`smb_constants.py`に定義されている*SMB_FILE_ATTRIBUTE_xxx*のビット論理和です。

## 中身しかいらない場合

ファイル自身を受信する必要は無くファイルの中身だけ読めればいい場合、`io.BytesIO()`を使うことで内容だけ受信できます。

```python
import io
with io.BytesIO() as file:
    conn.retrieveFile('Share', 'source.txt', file)
    file.seek(0)
    print(file.read().decode())
```

`decode()`のエンコーディングはデフォルトで`utf-8`なので、例えば`shift-jis`のファイルであれば以下のように指定することで読み込めます。

```python
with io.BytesIO() as file:
    conn.retrieveFile('Share', 'source.txt', file)
    file.seek(0)
    print(file.read().decode('shift-jis'))
```

# ファイル一覧の取得

サーバーの共有フォルダにどのようなファイルがあるか知りたい場合、`SMBConnection#listPath`メソッドを利用してファイルのリストを取得することができます。

```python
files = conn.listPath('Share', '/')
```

上記の例では共有フォルダ直下のファイル（フォルダ）のみが取得できます。共有フォルダ内にあるフォルダ配下のリストを取得したい場合は、`path`に目的のフォルダパスを設定します。

```python
# /tmp フォルダのリストを取得
files = conn.listPath('Share', '/tmp')

# /tmp/hoge フォルダのリストを取得
files = conn.listPath('Share', '/tmp/hoge')
```

この時`path`のルート（`'/'`）はあっても無くてもいいです。
共有フォルダ直下のリストを取得する場合は`'/'`でも`''`でもいいし、tmpフォルダのリストを取得する場合は`'/tmp'`でも`'tmp'`でもいいです。

少し詳しく見てみます、`SMBConnection#listPath`の定義は以下の形です。

```python
def listPath(self, service_name, path,
             search = SMB_FILE_ATTRIBUTE_READONLY | SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM | SMB_FILE_ATTRIBUTE_DIRECTORY | SMB_FILE_ATTRIBUTE_ARCHIVE,
             pattern = '*', timeout = 30):
```

- service_name  
サーバー側の共有フォルダ名
- path  
ファイルリストを取得するフォルダパス  
- search  
検索対象の絞り込みを*SMB_FILE_ATTRIBUTE_xxx*のビット論理和で指定する。  
ただし`SMB1`でのみ有効、`SMB2`を使用する場合この値は使われない。
- pattern  
検索結果のフィルタ、例えばテキストのみ抽出する場合は`*.txt`のように指定する。
- timeout  
タイムアウト値です。デフォルト30秒

## SharedFile

`SMBConnection#listPath`の結果は`SharedFile`インスタンスの配列で返って来ます。
その為純粋なファイル名のみが必要な場合は一工夫必要です。

```python
files = [f.filename for f in conn.listPath('Share', '/')]
```

ここで返ってくる`SharedFile`クラスは共有フォルダから取得できるファイル/フォルダの情報を持ちます。具体的には以下のようなプロパティです。

- filename  
ファイル名（もしくはフォルダ名）
- short_name  
8.3形式の短いファイル名
- file_attributes  
`SMB_EXT_FILE_ATTR`の論理和、詳しくは[[MS-CIFS]: 2.2.1.2.3](https://msdn.microsoft.com/en-us/library/ee878573.aspx)を参照
- alloc_size  
ファイルを格納する為割り当てられたサイズ、いわゆるディスク上のサイズ（Byte）
- file_size
ファイルのサイズ（Byte）
- create_time  
*1970-01-01 00:00:00*からの経過秒数でファイル作成日時を表す。
- last_access_time  
*1970-01-01 00:00:00*からの経過秒数で最終ファイルアクセス日時を表す。
- last_write_time  
*1970-01-01 00:00:00*からの経過秒数で最終ファイル更新日時を表す。
- last_attr_change_time  
*1970-01-01 00:00:00*からの経過秒数で最終ファイル属性変更日時を表す。
- isDirectory  
フォルダかどうか
- isReadOnly  
読み取り専用かどうか

あくまで情報のみを持つクラスでこのインスタンスから直接受信処理などを行うことはできません。`filename`を用いて別途`retrieveFile()`で受信しなければならないです。

## 共有フォルダのファイルを全部列挙

`listPath()`である程度深さのある共有フォルダからリストを取得したい場合、フォルダを指定しなければならないので若干取り回しが悪い（と思います）。

なので以下のように共有フォルダ以下（もしくは指定のフォルダ以下）のファイルを全て列挙するようにして使うと扱いやすいです。ただファイルがべらぼうに多いと非常に時間が掛かるので見に行くフォルダがどの程度の規模かは知っておく必要があります。

```python
def list_all(service_name, path=''):
    entries = conn.listPath(service_name, path)
    for item in (e for e in entries if not e.filename in ['.', '..']):
        if item.isDirectory:
            yield from list_all(service_name, path=f'{path}/{item.filename}')
        else:
            yield f'{path}/{item.filename}'


print([f for f in list_all('Share')])
```

# 共有フォルダ一覧の取得

`SMBConnection#listShares`メソッドではサーバーの共有フォルダをリストで取得することができます。
`SharedDevice`のリストを戻り値として受け取ることができます。フォルダ名一覧を取得する場合は以下のように使用します。

```python
print([dev.name for dev in conn.listShares()])
```

`SMBConnection#listShares`の引数は`timeout`のみです。デフォルト30秒でございます。

## SharedDevice

`SharedDevice`は`SharedFile`のように、共有フォルダの情報を持ちます。以下のプロパティを持ちます。

- name  
共有フォルダ名です。
- comments  
共有フォルダのコメントです。
- isSpecial
`IPC$`や`ADMIN$`、`C$`、`D$`など予約された共有名の場合`True`を返します。
- isTemporary
データが永続化されない一時的な共有の場合`True`を返します。
- type
共有フォルダのタイプです。詳しくは[[MS-SRVS]: 2.2.2.4](https://msdn.microsoft.com/en-us/library/cc247110.aspx)を参照


# 読まなくてもいい話
読まなくてもいい話というか、どう使えばいいかわからないメソッド達

## retrieveFileFromOffset

`retrieveFile()`に対して、バイト単位でファイルの一部を受信できる`retrieveFileFromOffset()`というメソッドもあります…が、敢えてこのメソッドを使うシーンはほとんど無いと思います。

ただ`retrieveFile()`では**最初から最後まで**という条件（offset=0, max_length=-1）で、内部的に`retrieveFileFromOffset()`を利用してファイル受信しているようです。

## listSnapshots

Windows7以降（厳密にはVista Business以降？）利用可能なフォルダのスナップショット機能が有効な場合、その一覧を取得することができます。
が、戻り値は`datetime`のリストです…使い道がわからない。

# 参考

[再帰とジェネレータの組み合わせ](https://qiita.com/wsldenli/items/590e11a7110b869bc54b)