[python] Dropboxの容量不足でお困りの方集合! 簡単なアーカイブツールを作ってみた [解説付]

#序
<b>Dropbox便利ですよね。</b>スマホで撮った写真が自動でクラウド上にアップロードされて、過去の写真を携帯の容量を気にせず一括で管理することができます。
ただ、自動アップロードされていくと<b>容量の5GB制限</b>にかかってしまって、これ以上写真を追加できないウェーン(泣)となってしまいます。
その事態に備えて、<b>「自動でDropbox内の写真をローカルPCにアーカイブする」</b>pythonスクリプトを作成しましたので、内容をご紹介します。

まぁ有料版にすれば5GB -> 2TBに増量できるのですが、月間1500円は我々庶民には高いですし、エンジニアたるものテクノロジーの力で解決しちゃいましょう(´・ω・`)

#筆者環境
Mac OS: Darwin Kernel Version 18.7.0
Python 3.6.8 :: Anaconda, Inc. (必要ライブラリはpipを用いてインストール済み)
Dropbox: PCにインストール済み(=共有ディレクトリ作成済み)

#やりたいこと
1. 自動でDrooboxの写真ファイルをアーカイブすることで、<b>容量パンクを防ぎたい！</b>
2. <b>写真の世代管理</b>もしたい！

#Dropboxの仕組みとpythonスクリプト仕様
DropboxをPCにインストールすると、PCのファイルシステムに共有ディレクトリが作成され、その配下がDropboxクラウドと同期します。

つまり、ここから写真ファイルをDropbox外に移動すれば、サーバ上からもそのファイルは消えます。
絵にすると以下のとおりです。

![dropboxアーカイブツール絵_mac.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165041/7e6f7b3a-f95d-57bc-36e1-1071671bdc1d.png)


1. スマホで撮った写真がDropboxに自動同期される(①)
2. Dropbox上の写真ファイルがPCの共有ディレクトリに同期される(②)
3. pythonスクリプトによって、共有ディレクトリの写真ファイルがPCローカルに移動(=アーカイブ)される。つまり、物理的には共有ディレクトリから写真ファイルが削除される。(③)
4. ③により共有ディレクトリから写真ファイルが削除されたので、その情報が同期され、Dropboxサーバからも写真ファイルが削除される。(④)

<b>定期的に③のpythonスクリプト(以下に詳細記載)を実行することで、Dropboxの容量パンクを防ぐことができます。</b>また、アーカイブ先のDropbox_arcディレクトリも<b>年&月ごとに階層化することで、世代管理を実現</b>します。
さすがにすべてのファイルをアーカイブするのも脳みそ筋肉ですので、パラメータでDropbox上に保持する日数を設定する仕様にしています。


# コード
### 1: shebang&ライブラリインポート
```python

#!/usr/bin/env python
# coding: utf-8

# -----------------------
# 1: Import libraries
# -----------------------
import os
import shutil
from glob import glob
from datetime import datetime
from dateutil.relativedelta import relativedelta
```
+ [os](https://www.sejuku.net/blog/67787) -> pathからファイル名のみ取得したり、osコマンドを実行したり、osレイヤーに関する作業を実施するのに必要なモジュールです。
+ [shutil](https://narito.ninja/blog/detail/71/) -> osコマンドをpythonの文法で実行できるモジュールです。最後に登場。
+ [glob](https://docs.python.org/ja/3/library/glob.html) -> ファイル名を取得するのに必要なモジュールです。
+ [datetime](https://note.nkmk.me/python-datetime-usage/) -> datetime型データを扱うのに必要なモジュールです。
+ [relativedelta](https://python.civic-apps.com/add-month-relativedelta/) -> 年・月の足し算、引き算をするのに必要なモジュールです。

### 2: カスタムパラメータの設定
```python

# -----------------------
# 2: Set parameters
# -----------------------
ARC_PATH = '/Users/<ユーザ名>/Dropbox/Camera Uploads'
DEST_PATH = '/Users/<ユーザ名>/Dropbox_arc'
REMAINING_MONTHS = 6
CRITERIA_DATE = datetime.now() - relativedelta(months=REMAINING_MONTHS)
```
初期設定はここのパラメータをいじります。
・ARC_PATH ... アーカイブしたい写真を含むディレクトリ(Camera Uploads)を指定します。
・DEST_PATH ... アーカイブするディレクトリ先を指定します。
・REMAINING_MONTHS ... Dropboxクラウド上に保持する月数を記載します。この例だと半年間はクラウド上に保持する仕様です。
・CRITERIA_DATE ... "現在の日付 - REMAINING_MONTHS"を[relativedelta](https://python.civic-apps.com/add-month-relativedelta/)を用いて計算することで、アーカイブ開始月を計算しています。

### 3: Camera Uploads内のファイル一覧を取得
```python

# ----------------------------------------------
# 3: Get existing file list on dropbox
# ----------------------------------------------
# FILELISTのデータイメージ: 写真ファイル名をフルパスで保持
# ex) FILELIST = [
#  '/Users/<ユーザ名>/Dropbox/Camera Uploads/2017-01-01 15.31.08.jpg',
#  '/Users/<ユーザ名>/Dropbox/Camera Uploads/2017-01-01 17.30.00.jpg',
#  ...
#  '/Users/<ユーザ名>/Dropbox/Camera Uploads/2019-08-26 23.38.25.jpg'
#  ]

FILELIST = glob(f'{ARC_PATH}/????-??-?? *')
```
ARC_PATH内に存在する該当ファイルを"FILELIST"というstring型のリストとして保持します。
[glob表記](https://docs.python.org/ja/3/library/glob.html)で"????-??-?? *"の命名規則に当てはまるファイルを検索対象とします。拡張子はjpgだったりpngだったりmovだったりするので指定していません。シングルクオートの前にあるfは、[「フォーマット済み文字列リテラル」](https://note.nkmk.me/python-f-strings/)のことです。
この段階では、新しいファイルも古いファイルもごっちゃでFILELIST内に保持されています。

### 4: ファイル一覧から年月情報を取得
```python

# ----------------------------------------------
# 4: Get unique year-month list 
# ----------------------------------------------
# DATELISTのデータイメージ:　1ヶ月ごとのタイムスタンプをuniqueな値で格納
# ex) DATELIST = [
#   datetime.datetime(2017, 1, 1, 0, 0),
#   datetime.datetime(2017, 2, 1, 0, 0),
#   datetime.datetime(2017, 3, 1, 0, 0),
#   ...
#   datetime.datetime(2019, 7, 1, 0, 0),
#   datetime.datetime(2019, 8, 1, 0, 0)
#   ]

DATELIST = [] #空のリスト箱を用意
for i in FILELIST:
    # FILELISTのフルパスからファイル名のみ抜き出し、7文字目までをスライス
    str_year_month = os.path.basename(i)[:7]

    # スライスした文字('2017-01'など) をdatetime型に変換
    datetime_year_month = datetime.strptime(str_year_month, '%Y-%m')

    # DATELISTにappend
    DATELIST.append(datetime_year_month)

DATELIST = list(set(DATELIST))
```
FILELIST内のファイル名からタイムスタンプを抜き出し、その範囲を1ヶ月ごとに記録した"DATELIST"という[datetime型](https://note.nkmk.me/python-datetime-usage/)のリストを作成します。日付情報以下は入力していないので、勝手に1やら0やらが入っています。また、[set関数](https://www.sejuku.net/blog/21923)を使用することで、重複エントリを削除してuniqueなデータとして保持しています。

ちなみに、["map関数"と"lambda記法"](https://www.sejuku.net/blog/24759)を用いればこの部分はforを使わず非常に簡潔に書けます。しかし今回は初心者への可読性重視ということで採用していません。ちなみに、その記法では下のように書くことができます。

```python

# 参考コード; 下記でも可能
# DATELIST = list(map(lambda x: datetime.strptime(os.path.basename(x)[:7], '%Y-%m'),  FILELIST))
# DATELIST = list(set(DATELIST))
```

ここから、REMAINING_MONTHSの値をもとにアーカイブ対象となるタイムスタンプ(年月)を算出します。

### 5: REMAINING_MONTHSの値をもとに、アーカイブする年月リストを算出
```python

# ----------------------------------------------
# 5: Get months which have to be archived
#     - older than REMAINING_MONTHS
# ----------------------------------------------
# ARCLISTのデータイメージ: 計算の結果、アーカイブする月を写真ファイルの命名規則に沿った形で格納。
# ex) ARCLIST = [
#   '2017-01',
#   '2017-02',
#   ...
#   '2019-01',
#   '2019-02'
#   ]

ARCLIST = [] #空のリスト箱を用意
for i in DATELIST:
    # CRITERIA_DATEと比較して、DATELISTの要素が昔だった場合、ARCLISTにappend。
    if i < CRITERIA_DATE:
        ARCLIST.append(i.strftime('%Y-%m'))
```
DATELISTに対してfor文を実行させます。
結果、CRITERIA_DATEよりも古いタイムスタンプ(年月)はアーカイブ対象として"ARCLIST"に記録されます。"ARCLIST"はstring型のリストで、アーカイブ対象ファイル名の先頭7文字と同じ命名規則となるように保存されます。


### 6: アーカイブ実行
```python

# ----------------------------------------------
# 6: Archive pic. files with each ARCLIST element's prefix
# ----------------------------------------------
# 命名規則に沿う写真ファイルをアーカイブ。アーカイブ先のディレクトリがない場合は適宜作成。
for prefix in ARCLIST:
    archive_year = prefix[:4] # ARCLISTの文字列から年だけ=最初4文字を抜き出す
    YEAR_DIRNAME = f'{archive_year}_Camera' #アーカイブ先フォルダ名(年レベル)
    MONTH_DIRNAME = prefix #アーカイブ先フォルダ名(月レベル)

    # 1. ディレクトリの存在をチェック。ない場合は、作成。
    if not os.path.exists(f'{DEST_PATH}/{YEAR_DIRNAME}/{MONTH_DIRNAME}'):
        os.makedirs(f'{DEST_PATH}/{YEAR_DIRNAME}/{MONTH_DIRNAME}')
            
    # 2. ファイル名が変数名prefixから始まるファイルを、対象ディレクトリにアーカイブする。
    ARC_PICFILE_LIST = glob(f'{ARC_PATH}/{prefix}*')
    for picfile in ARC_PICFILE_LIST:
        shutil.move(picfile, f'{DEST_PATH}/{YEAR_DIRNAME}/{MONTH_DIRNAME}')
```
ARCLISTに対してfor文を実行します。ARCLISTの文字列(例: 2017-01など)から始まるファイルは、アーカイブ対象となるようなロジックです。

1. 第二の目的として「世代管理したい」ので、今回はアーカイブディレクトリも下記のように細分化しています。もしディレクトリがない場合は、作成するようなロジックです。
![dropboxアーカイブツール絵_arc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165041/1a5ceade-777b-3436-0e0c-3c2a1739de01.png)

2. アーカイブします。 globで移動対象のファイルリスト(=ARC_PICFILE_LIST)を保持した後に、forループを回し1件1件写真ファイルをアーカイブ(移動)させます。[shutil.moveメソッド](https://narito.ninja/blog/detail/71/)を使うことで、pythonを介して写真ファイルの操作が可能です。shutil.moveはワイルドカードを使えないみたいで、、、。ワイルドカードを使用できるようにosコマンドを発行させる案も検討しましたが、プラットフォーム依存の書き方になるので、泣く泣くforループを採用。。。

# まとめ(一括記載)
上記紹介したコードを連結させただけです。

```python

#!/usr/bin/env python
# coding: utf-8

# -----------------------
# 1: Import libraries
# -----------------------
import os
import shutil
from glob import glob
from datetime import datetime
from dateutil.relativedelta import relativedelta

# -----------------------
# 2: Set parameters
# -----------------------
ARC_PATH = '/Users/<ユーザ名>/Dropbox/Camera Uploads'
DEST_PATH = '/Users/<ユーザ名>/Dropbox_arc'
REMAINING_MONTHS = 6
CRITERIA_DATE = datetime.now() - relativedelta(months=REMAINING_MONTHS)

# ----------------------------------------------
# 3: Get existing file list on dropbox
# ----------------------------------------------
# FILELISTのデータイメージ: 写真ファイル名をフルパスで保持
# ex) FILELIST = [
#  '/Users/<ユーザ名>/Dropbox/Camera Uploads/2017-01-01 15.31.08.jpg',
#  '/Users/<ユーザ名>/Dropbox/Camera Uploads/2017-01-01 17.30.00.jpg',
#  ...
#  '/Users/<ユーザ名>/Dropbox/Camera Uploads/2019-08-26 23.38.25.jpg'
#  ]

FILELIST = glob(f'{ARC_PATH}/????-??-?? *')

# ----------------------------------------------
# 4: Get unique year-month list
# ----------------------------------------------
# DATELISTのデータイメージ:　1ヶ月ごとのタイムスタンプをuniqueな値で格納
# ex) DATELIST = [
#   datetime.datetime(2017, 1, 1, 0, 0),
#   datetime.datetime(2017, 2, 1, 0, 0),
#   datetime.datetime(2017, 3, 1, 0, 0),
#   ...
#   datetime.datetime(2019, 7, 1, 0, 0),
#   datetime.datetime(2019, 8, 1, 0, 0)
#   ]

DATELIST = [] #空のリスト箱を用意
for i in FILELIST:
    # FILELISTのフルパスからファイル名のみ抜き出し、7文字目までをスライス
    str_year_month = os.path.basename(i)[:7]

    # スライスした文字('2017-01'など) をdatetime型に変換
    datetime_year_month = datetime.strptime(str_year_month, '%Y-%m')

    # DATELISTにappend
    DATELIST.append(datetime_year_month)

DATELIST = list(set(DATELIST))

# 参考コード; 下記でも可能
# DATELIST = list(map(lambda x: datetime.strptime(os.path.basename(x)[:7], '%Y-%m'),  FILELIST))
# DATELIST = list(set(DATELIST))

# ----------------------------------------------
# 5: Get months which have to be archived
#     - older than REMAINING_MONTHS
# ----------------------------------------------
# ARCLISTのデータイメージ: 計算の結果、アーカイブする月を写真ファイルの命名規則に沿った形で格納。
# ex) ARCLIST = [
#   '2017-01',
#   '2017-02',
#   ...
#   '2019-01',
#   '2019-02'
#   ]

ARCLIST = [] # 空のリスト箱を用意
for i in DATELIST:
    # CRITERIA_DATEと比較して、DATELISTの要素が昔だった場合、ARCLISTにappend。
    if i < CRITERIA_DATE:
        ARCLIST.append(i.strftime('%Y-%m'))

# ----------------------------------------------
# 6: Archive pic. files with each ARCLIST element's prefix
# ----------------------------------------------
# 命名規則に沿う写真ファイルをアーカイブ。アーカイブ先のディレクトリがない場合は適宜作成。
for prefix in ARCLIST:
    archive_year = prefix[:4] # ARCLISTの文字列から年だけを抜き出す
    YEAR_DIRNAME = f'{archive_year}_Camera' # アーカイブ先フォルダ名(年レベル)
    MONTH_DIRNAME = prefix # アーカイブ先フォルダ名(月レベル)

    # 1. ディレクトリの存在をチェック。ない場合は、作成。
    if not os.path.exists(f'{DEST_PATH}/{YEAR_DIRNAME}/{MONTH_DIRNAME}'):
        os.makedirs(f'{DEST_PATH}/{YEAR_DIRNAME}/{MONTH_DIRNAME}')

    # 2. ファイル名が変数名prefixから始まるファイルを、対象ディレクトリにアーカイブする。
    ARC_PICFILE_LIST = glob(f'{ARC_PATH}/{prefix}*')
    for picfile in ARC_PICFILE_LIST:
        shutil.move(picfile, f'{DEST_PATH}/{YEAR_DIRNAME}/{MONTH_DIRNAME}')
```

# おわり
これを定期実行スクリプトに組み込めば、ファイルアーカイブと写真の世代管理が自動化されます。良ければ<ユーザ名>だけご自分の環境用に書き換え、テストの上お試しください。
最近pythonを使った自動化やツール作成を勉強がてら試しているので、今後も同様のツール作ってみた系の記事を載せようと思っています。

 もっと良い書き方ある等ありましたらコメントくださるとよろこびます。