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

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

# インデックス
[pysmbの使い方（接続・切断）](https://qiita.com/t2kojima/items/5db8dadaa6e07321d25f)
[pysmbの使い方（ファイル受信）](https://qiita.com/t2kojima/items/6c20a7801467221470ac)
pysmbの使い方（ファイル送信）
[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

# ファイル送信

ファイル送信は以下のように行います。（インスタンスの作成や接続・切断処理は割愛）
今回はクライアントのカレントフォルダにある`source.txt`をサーバーの共有フォルダ`Share`へ送信します。

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

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

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

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

ここまで`retrieveFile()`とほとんど同じです。`source.txt`と`dst.txt`の位置が逆なくらいですね。

ちなみにサーバー側に同名のファイルが存在する場合、**アラート無しで上書き**なので注意しましょう。

## フォルダがない場合

送信先としてフォルダを含めるパスを指定する場合、そのフォルダが存在していなければなりません。

```python
with open('source.txt', 'rb') as file:
    conn.storeFile('Share', 'temp/dst.txt', file)
```

上記の場合、`temp`フォルダが無ければ`smb_structs.OperationFailure`になります。

### フォルダを作る

フォルダが無ければ作ればいいじゃない！`SMBConnection#createDirectory`で作ります。

定義はこうで

```python
def createDirectory(service_name, path, timeout=30)
```

こう使います

```python
createDirectory('Share', 'temp')
```

一点注意点として、作成するフォルダは階層を持たない単一のフォルダである必要があります。なので親フォルダごと作成する（`temp/temp'のような）ことはできません。

ですが親フォルダがあるかどうかを１階層ずつチェックしていくのは面倒なので、以下のような関数を定義してフォルダを一気に作ります。（これも若干冗長な気がしますが…）

```python
def exists(service_name, path):
    parent = Path(path).parent.as_posix().replace('.', '/')
    if parent == '/' or exists(service_name, parent):        
        return bool([f for f in conn.listPath(service_name, parent) if f.filename == Path(path).name])


def makedirs(service_name, path):
    parent = Path(path).parent.as_posix().replace('.', '/')
    if not parent == '/' and not exists(service_name, parent):
        makedirs(service_name, parent)
    if not exists(service_name, path):
        conn.createDirectory(service_name, path)


makedirs('Share', 'aaa/bbb/ccc')
```

`makedirs()`では引数として受け取った`path`の親フォルダの存在をチェックして、「親フォルダが無ければ作成」を繰り返していきます。
`parent`を取得する際`.replace('.', '/')`としていますが、これは`makedirs()`の引数`path`の書き方として、`'aaa/bbb/ccc'`と`'/aaa/bbb/ccc'`の両方を許容する為です。

### フォルダの削除

`SMBConnection#deleteDirectory`を使用します。

これも定義はこうで

```python
def deleteDirectory(service_name, path, timeout=30)
```

こう使います

```python
deleteDirectory('Share', 'temp')
```

フォルダ削除の注意点としては、削除するフォルダが空フォルダでなければなりません。ファイルやフォルダ（空フォルダであっても）を持つ場合は`smb_structs.OperationFailure`を投げます。
フォルダに中身があっても強制的に削除するには以下のようにします。

```python
def exists(service_name, path):
    parent = Path(path).parent.as_posix().replace('.', '/')
    if parent == '/' or exists(service_name, parent):        
        return bool([f for f in conn.listPath(service_name, parent) if f.filename == Path(path).name])


def removedirs(service_name, path):
    if exists(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:
                removedirs(service_name, f'{path}/{item.filename}')                
            else:
                conn.deleteFiles(service_name, f'{path}/{item.filename}')
        conn.deleteDirectory(service_name, path)

removedirs('Share', 'aaa/bbb/ccc')
# -> 'ccc'フォルダ配下は全て削除
```

# ファイル削除

`SMBConnection#deleteFiles`を使用します。

これも定義はこうで

```python
def deleteFiles(service_name, path_file_pattern, timeout=30)
```

こう使います

```python
deleteFiles('Share', 'temp/target.txt')
```

このメソッドも注意点があります。
ドキュメントでは以下のようにワイルドカードで複数ファイルを一括削除できると書いてありますが、実際やってみると例外が飛んでできません。

> Delete one or more regular files. It supports the use of wildcards in file names, allowing for deletion of multiple files in a single request.

ただ[GitHub](https://github.com/miketeo/pysmb/blob/master/python3/tests/SMBConnectionTests/test_deletepattern.py)にはちゃんとテストが書いてあり、通っているはずなので何故例外が発生するのかわかりません…

単一ファイルをフルパス指定で削除する分には問題なく動きます。

# 読まなくてもいい話

## storeFileFromOffsetのtruncate

`SMBConnection#storeFileFromOffset`には`truncate`という引数があり、デフォルトが**False**です。
これはどのような上書きをするかの挙動を指示するもので、以下のような動きをします。

|truncate|送信元のファイル内容|送信先のファイル内容|上書き結果|
|---|---|---|---|
|True|あああ|い|あああ|
|True|あ|いいい|あ|
|False|あああ|い|あああ|
|False|あ|いいい|あいい|

**True**の場合は普通のファイル上書きをするので分かりやすいですが、**False**の場合はファイルの元の内容を残したままバイト単位で上書きします。

`SMBConnection#storeFile`も内部的に`SMBConnection#storeFileFromOffset`を呼び出していますが、`truncate`は**True**で呼んでいるので普通のファイル上書きになっています。

ちなみに`source.txt`、`dst.txt`ともに**UTF-8**で書かれている場合、以下のようなこともできます。

```python
with open('source.txt', 'rb') as file:
    conn.storeFileFromOffset('Share', 'dst.txt', file, truncate=False, offset=3)
```

|truncate|送信元のファイル内容|送信先のファイル内容|上書き結果|
|---|---|---|---|
|False|あ|いいい|いあい|

まぁ…使わないね