ドラッグ&ドロップで実行形式ファイルも添付可能なGmail用zipファイルを処理

プログラミング

.exeや.vhdなどの拡張子を持つファイルをgmailに添付できるようにするWindowsアプリをPythonで作成してみました。gmailサーバが実行形式などのファイルの添付を拒否する問題はone driveを使うなどの方法で回避できます。ですから,このアプリにはそれほど需要があるとは思えません。しかし,ドラッグ&ドロップでの起動など,いろいろ勉強になったので,記録に残しておきます。

きっかけは,ある時期からVHDLのソースコード(拡張子は.vhd)をgmailの添付で送信できなくなったからです。中身はテキストデータなのに拡張子が.vhdになっているだけでgmailのシステムが添付を拒否してしまいます。(.vhdはVertual Hard Driveファイルの拡張子でもあるので,安全性を考えれば仕方がないことですが。)

VHDLソースコードをgmailで送受したい場合は,拡張子.vhdを.txtに書き直して送信し,受信したら拡張子を.vhdに戻せばよいのです。でもファイルの数が多くなると面倒なので,Pythonで拡張子変更とzip圧縮/解凍をするPythonコードを作ってみました。

そこで止めておけばいいものを,実行形式ファイルも送受できるコードにしてみました。Pythonのzip圧縮/解凍のコードの挙動やドラッグ&ドロップでの起動に興味があったからです。

注意:

<注意を読む>

この投稿は,十分にテストをしていないプログラムの開発過程の備忘録として書かれたものです。紹介されているコードにはバグや冗長な部分が含まれています。また,十分な例外処理を組み込んでいないので,重要なファイルの入っているフォルダを削除したり上書きしたりする可能性もあります。コードを使ったり参考にしたりする場合は注意をお願いします。

また,将来Pythonライブラリやgmailの仕様が変更されると機能しなくなる可能性もあります。

<閉じる>

使い方

実行ファイルは以下のボタンをクリックしてダウンロードしてください。

添付用フォルダを作る

空のフォルダを準備し,添付したいファイルを格納します。サブフォルダは作らないでください。

フォルダのアイコンを,gmailZippy.exeのアイコンにドラッグ&ドロップします。

ドラッグ&ドロップ

zipファイルができる

ドラッグしたフォルダと同じディレクトリに圧縮されたzipファイルが現れます。これをgmailに添付して送ります。

受信したファイルを解凍する

gmailで受信したzipファイルをgmailZippy.exeにドラッグ&ドロップします。解凍されたフォルダの中には,送信したファイルが格納されています。拡張子の書き替え/復元をしたファイルは,exe,vhdなどの名前のフォルダごとに整理されています。

zipファイルをD&D       →         復元された添付フォルダ

処理の流れ

送信側では空のフォルダを用意します。仮に名前をXXXとします。その中に送信したいファイルを格納します。

作成したコードは圧縮用と解凍用の2つの関数を持ち,圧縮/解凍と拡張子の書き換えを自動で行います。コードの処理の手順は以下のようになっています。

圧縮:

  1. フォルダXXXの中にある.exe,.vhdなどの拡張子を持つファイルの拡張子を.txtに変換しサブフォルダを作って整理
  2. フォルダ“XXX”をzip圧縮 → XXX.zipが生成される
  3. XXX.zipの拡張子を.zippyに変更してXXX.zippyにする
  4. XXX.zippyをフォルダに格納してzip圧縮 → XXX.zipが生成される

このXXX.zipをgmailに添付して送信します。

なお,上の1~4の操作をWindows OSを使った手動操作で行う場合は,XXX.zippyをフォルダに入れないでそのまま“右クリック>ZIPファイルに圧縮する”によりXXX.zipというファイルができ,これをzip展開すると,XXXフォルダの中にXXX.zippyが格納されたものが得られます。しかし,Pythonコードで実行する場合は,1度目の圧縮で生成されたXXX.zippyをフォルダに格納してから2度目の圧縮をする必要があります。

解凍:

  1. XXX.zipをzip解凍 → XXX.zipyが格納されたXXXフォルダが得られる
  2. XXX.zippyをXXX.zipにリネーム
  3. XXX.zipをzip解凍 → 解凍されたファイルを格納したフォルダXXXが得られる
  4. フォルダ内のファイルの拡張子を元に戻す

以上の圧縮と解凍の手順は手動で実行してもよいのです。なお,中間の圧縮ファイルの拡張子として.zippyを使っています。使われていない拡張子なら何でもよいと思いますが,確認はしていません。また,XXX.zippyをXXXにリネームしても動作するように思えますが,その場合の処理手順についても試しておりません。

Pythonコードの説明

ソースコードは1つのファイル“gmailZippy.py”にまとめられています。リストを上から順に説明していきます。

ライブラリ

ライブラリ宣言部分です。

# gmailZippy.py gmail添付用の拡張子変更・圧縮・解凍
# Change extension and compress/decompress to attach with gmail
# Copyright © 2023 Tamlab All rights reserved. ver. 1.0
from tkinter import filedialog
from tkinter import messagebox # for debugging
import os
import sys
import shutil
import glob
  • from tkinter import messagebox
    公開しているコードの中では使ってないが,デバッグの際にmessagebox関数を使用した。生成したファイルの上書や処理できないファイルの入力などの際の表示に使う予定。

拡張子書き換え

ファイルの拡張子を.txtに書き換える関数です。書き換えたファイルは書き換え前の拡張子の名前をつけたサブフォルダを作って,その中に格納します。

def ext_change(ext,temp_dir):
    # ext: file extention tring, temp_dir: working directory
    # 文字列extで与えられる拡張子を.txtに変更,Change the file extention to '.txt'
    path_name = temp_dir + '/*.' + ext
    files = glob.glob(path_name)
    if len(files) != 0: # If the corresponding file exists, do the following
        target_dir = temp_dir + '/' + ext
        os.makedirs(target_dir)  
        for file in files:  
            file_no_ext = os.path.splitext(os.path.basename(file))[0]
            name_dst = temp_dir + '/' + file_no_ext + '.txt'
            os.rename(file, name_dst) 
            shutil.copy(name_dst, target_dir) # ファイルを格納用フォルダに移動
            os.remove(name_dst)
  • def ext_change(ext,temp_dir):
     ext:拡張子の“.”を除いた文字列
     temp_dir:拡張子を書き換えたファイルを格納する一時フォルダの名前

拡張子の復元

拡張子を元に戻す関数です。解凍したフォルダに格納されているファイルの拡張子.txtを元のファイルの拡張子に戻します。

def ext_recovery(ext, folder_path): # 拡張子を復元, file extention recovery
    target_dir = folder_path + '/' + ext 
    path_name = target_dir + '/*'  # 対象とするフォルダ内の全てのファイル
    files = glob.glob(path_name)
    for file in files:    
        file_no_ext = os.path.splitext(os.path.basename(file))[0]
        name_dst = target_dir + '/' + file_no_ext + '.' + ext
        os.rename(file, name_dst) 
  • def ext_recovery(ext, folder_path):
     ext:戻したい拡張子の“.”を除いた文字列
     folder_path:拡張子を書き換えるファイルを格納しているフォルダの名前
  • path_name = target_dir + ‘/*’
    files = glob.glob(path_name)
     target_dirはフォルダパスの文字列。この文字列を”XXX””とすると,path_nameは”XXX/*”となる。次のglob関数でフォルダXXXの下にある全てのファイルのリストが得られる。

圧縮

フォルダを圧縮する関数です。まずext_change関数でフォルダ内のファイルの拡張子を書き換え,次に圧縮します。圧縮では,まずzip圧縮し,得られた圧縮ファイルの拡張子を.zipから書き換え,再度zip圧縮しています。

zip圧縮を2回実行するのですが,2回目の圧縮で,ちょっとひっかりました。

Windows OSの操作ではXXX.zippyをzip圧縮すると,XXX.zipというファイルができます。これを解凍するとXXX.zippyを格納したXXXフォルダが得られます。

ところが,Pythonコードでshutil.make_archiveを使うと,2回目の圧縮ではXXX.zippy.zipというファイルができますが,中身の情報が失われています。そこで,XXX.zippyをXXXフォルダに格納したものを作り,これをshutil.make_archiveで圧縮しています。

def compress(path_name):
    # 非圧縮状態のフォルダを指定,Specify a object folder in uncompressed state.
    file_dir = path_name
    file_name = os.path.splitext(os.path.basename(path_name))[0]
    os.chdir(file_dir) # Sets the current directory to the specified folder.
    os.chdir('../')
    target_dir = os.getcwd()

    temp_dir = file_dir + '/temp' # Create temporary directory.
    if os.path.isdir(temp_dir):
       shutil.rmtree(temp_dir)
    shutil.copytree(file_dir , temp_dir) # Copy all files to temp_dir. 

    ext_change('vhd', temp_dir) # Change file extentions to '.txt'.
    ext_change('exe', temp_dir)
    ext_change('py', temp_dir)

    # フォルダをzipにアーカイブ, Archive folder to zip file.
    # First compression
    shutil.make_archive(temp_dir, format='zip', root_dir=temp_dir)
    # Rename .zip -> .zippy
    temp_name = temp_dir + '.zip'
    temp_name_zippy = temp_name + 'py'
    os.rename(temp_name, temp_name_zippy)

    # Renew temp directory
    shutil.rmtree(temp_dir)
    os.mkdir(temp_dir)

    # Copy the .zippy file to temp directory
    temp_dir_file = temp_dir + '/temp.zippy'
    shutil.copyfile(temp_name_zippy, temp_dir_file)   

    # Second compression
    shutil.make_archive(temp_dir, format='zip',root_dir=temp_dir)

    # Copy the compressed folder to destination directory.
    dst_name = file_name + '.zip'
    shutil.copy(temp_name, dst_name)
    
    # 一時フォルダの削除
    os.remove(temp_name)
    shutil.rmtree(temp_dir)
    os.remove(temp_name_zippy)
  • ext_change(‘vhd’, temp_dir)
    ext_change(‘exe’, temp_dir)
    ext_change(‘py’, temp_dir)
     書き直す拡張子は,現在,”.vhd”,”.exe”,”.py”の3種類。.pyは書き直さなくても大丈夫だと思うが,サブフォルダに整理する機能を利用するため使っている。将来,対応したい拡張子が増えた場合は,この関数を追加していけばよい。
  • shutil.make_archive(temp_dir, format=’zip’, root_dir=temp_dir)
     1回目のzip圧縮。
  • shutil.rmtree(temp_dir)
    os.mkdir(temp_dir)
     作業ディレクトリを一旦,中のファイルと共に削除し,その後で同じ名前のディレクトリを作っている。
  • temp_dir_file = temp_dir + ‘/temp.zippy’
    shutil.copyfile(temp_name_zippy, temp_dir_file)
     空になっている作業用ディレクトリに.zippyファイルを格納。
  • shutil.make_archive(temp_dir, format=’zip’,root_dir=temp_dir)
     2回目のzip圧縮。

解凍

解凍するための関数です。zip展開し,得られたファイルの拡張子を.zippyから.zipに戻し2度目のzip展開をして,最後にファイルの拡張子を元に戻します。

def decompress(path):
    # Decompress the zippy archive.
    fileName = path
    # Get the string of the file name without the file extension.
    folder_path = fileName.replace('.zip', '')
    if os.path.exists(folder_path):
        folder_path = filedialog.askdirectory(
            title = "Specify a folder.",
            initialdir = dir) 
    # zipファイルを展開,Unzip the file.
    zippy_path = folder_path + '/temp.zippy'
    zip_path = folder_path + '/temp.zip'
    shutil.unpack_archive(fileName, folder_path) # 1回目の展開, first extraction
    os.rename(zippy_path, zip_path)
    shutil.unpack_archive(zip_path,folder_path)  # 2回目の展開, second extraction
    os.remove(zip_path)  # 一時フォルダの削除, Remove temp folder. 
    ext_recovery('vhd', folder_path) # File extention recovery.
    ext_recovery('exe', folder_path)
    ext_recovery('py', folder_path)
  • folder_path = fileName.replace(‘.zip’, ”)
    if os.path.exists(folder_path):
    folder_path = filedialog.askdirectory(
    title = “Specify a folder.”,
    initialdir = dir)
     zip展開された後に得られる“XXX”と同じ名前のフォルダが存在する場合は,フォルダ名を改めて指定するようにしている。
  • shutil.unpack_archive(fileName, folder_path)
    os.rename(zippy_path, zip_path)
    shutil.unpack_archive(zip_path,folder_path)
     zipファイルを展開し,拡張子を.zippyから.zipに戻してから2度目のzip展開をしている。
  • ext_recovery(‘vhd’, folder_path)
    ext_recovery(‘exe’, folder_path)
    ext_recovery(‘py’, folder_path)
     拡張子を復元する関数。復元したい拡張子の数だけ関数を呼び出す。

main関数

main関数です。引数path_nameでフォルダまたはzipファイルのパスを指定します。パスがフォルダのものであればcompress関数を,zipファイルのものであればdecompress関数を呼び出します。

def main(path_name):
    if os.path.isdir(path_name):
        compress(path_name)
    else :
        path_ext = os.path.splitext(path_name)[1]
        if path_ext == '.zip':
            decompress(path_name) 

ドラッグ&ドロップで起動する

main関数を呼び出す部分です。これにより,フォルダやファイルのアイコンをドラッグ&ドロップすることでアプリケーションを起動するようになっています。

if __name__ == '__main__': # If the script is activated as main,
    # sys.argv[1] is the dragged and dropped path name.
    main(sys.argv[1])
  • if __name__ == ‘__main__’:
     変数__name__ は,このPythonコードがメインとして起動されたとき’__main__’という文字列になる。つまり,Pythonコードがメインとして起動されたときに,ifブロック内の式文が実行される。
  • main( sys.argv[1])
     フォルダやファイルをアプリのアイコンにドラッグ&ドロップして起動すると,sys.argv[1]に そのファイルやフォルダのパスが格納される。このパスを引数としてmain関数を呼び出す。

<VSCodeでのデバッグのときは・・・>

コードの開発はVisual Studio Codeを使っています。デバッグモードで実行させることで,設定したブレークポイントの位置で変数の値などをチェックできます。しかし,ドラッグ&ドロップで実行させると,VSCodeを介さず実行させるらしく,ブレークポイントが機能しません。それでデバッグ中は,main関数の部分を,以下のように変更しています。

  • if __name__ == __main__’:
    arg_d = ‘aaa・・・a’
    main(arg_d)
     文字列aaa・・・aには動作テストに使うフォルダやzipファイルのパス文字列を記しておきます。

私が知らないだけで,VSCodeを使ってドロップ&ドロップで起動するコードをデバッグする方法はあるのかもしれません。

<閉じる>

コードの全体

改めてコード全体のリストを示します。

# gmailZippy.py gmail添付用の拡張子変更・圧縮・解凍
# Change extension and compress/decompress to attach with gmail
# Copyright © 2023 Tamlab All rights reserved. ver. 1.0
from tkinter import filedialog
from tkinter import messagebox # for debugging
import os
import sys
import shutil
import glob

def ext_change(ext,temp_dir):
    # ext: file extention tring, temp_dir: working directory
    # 文字列extで与えられる拡張子を.txtに変更,Change the file extention to '.txt'
    path_name = temp_dir + '/*.' + ext
    files = glob.glob(path_name)
    if len(files) != 0: # If the corresponding file exists, do the following
        target_dir = temp_dir + '/' + ext
        os.makedirs(target_dir)  
        for file in files:  
            file_no_ext = os.path.splitext(os.path.basename(file))[0]
            name_dst = temp_dir + '/' + file_no_ext + '.txt'
            os.rename(file, name_dst) 
            shutil.copy(name_dst, target_dir) # ファイルを格納用フォルダに移動
            os.remove(name_dst)

def ext_recovery(ext, folder_path): # 拡張子を復元, file extention recovery
    target_dir = folder_path + '/' + ext 
    path_name = target_dir + '/*'  # 対象とするフォルダ内の全てのファイル
    files = glob.glob(path_name)
    for file in files:    
        file_no_ext = os.path.splitext(os.path.basename(file))[0]
        name_dst = target_dir + '/' + file_no_ext + '.' + ext
        os.rename(file, name_dst) 

def compress(path_name):
    # 非圧縮状態のフォルダを指定,Specify a object folder in uncompressed state.
    file_dir = path_name
    file_name = os.path.splitext(os.path.basename(path_name))[0]
    os.chdir(file_dir) # Sets the current directory to the specified folder.
    os.chdir('../')
    target_dir = os.getcwd()

    temp_dir = file_dir + '/temp' # Create temporary directory.
    if os.path.isdir(temp_dir):
       shutil.rmtree(temp_dir)
    shutil.copytree(file_dir , temp_dir) # Copy all files to temp_dir. 

    ext_change('vhd', temp_dir) # Change file extentions to '.txt'.
    ext_change('exe', temp_dir)
    ext_change('py', temp_dir)

    # フォルダをzipにアーカイブ, Archive folder to zip file.
    # First compression
    shutil.make_archive(temp_dir, format='zip', root_dir=temp_dir)
    # Rename .zip -> .zippy
    temp_name = temp_dir + '.zip'
    temp_name_zippy = temp_name + 'py'
    os.rename(temp_name, temp_name_zippy)

    # Renew temp directory
    shutil.rmtree(temp_dir)
    os.mkdir(temp_dir)

    # Copy the .zippy file to temp directory
    temp_dir_file = temp_dir + '/temp.zippy'
    shutil.copyfile(temp_name_zippy, temp_dir_file)   

    # Second compression
    shutil.make_archive(temp_dir, format='zip',root_dir=temp_dir)

    # Copy the compressed folder to destination directory.
    dst_name = file_name + '.zip'
    shutil.copy(temp_name, dst_name)
    
    # 一時フォルダの削除
    os.remove(temp_name)
    shutil.rmtree(temp_dir)
    os.remove(temp_name_zippy)

def decompress(path):
    # Decompress the zippy archive.
    fileName = path
    # Get the string of the file name without the file extension.
    folder_path = fileName.replace('.zip', '')
    if os.path.exists(folder_path):
        folder_path = filedialog.askdirectory(
            title = "Specify a folder.",
            initialdir = dir) 
    # zipファイルを展開,Unzip the file.
    zippy_path = folder_path + '/temp.zippy'
    zip_path = folder_path + '/temp.zip'
    shutil.unpack_archive(fileName, folder_path) # 1回目の展開, first extraction
    os.rename(zippy_path, zip_path)
    shutil.unpack_archive(zip_path,folder_path)  # 2回目の展開, second extraction
    os.remove(zip_path)  # 一時フォルダの削除, Remove temp folder. 
    ext_recovery('vhd', folder_path) # File extention recovery.
    ext_recovery('exe', folder_path)
    ext_recovery('py', folder_path)

def main(path_name):
    if os.path.isdir(path_name):
        compress(path_name)
    else :
        path_ext = os.path.splitext(path_name)[1]
        if path_ext == '.zip':
            decompress(path_name) 
            
if __name__ == '__main__': # If the script is activated as main,
    # sys.argv[1] is the dragged and dropped path name.
    main(sys.argv[1])

使ってみた感想

圧縮のときに,動作が遅いように感じます。2回の圧縮の間に実行しているファイルの削除やコピーなどの手間が解凍処理のときよりも多いためかもしれません。コードも圧縮処理の方がゴチャゴチャしています。時間があれば見直したいです。

圧縮したファイルをgmailに添付すると,アップロードの際,プログレッシブバーの表示があるところで停止して,しばし“考え込んで”いるような挙動を示します。gmailが添付可能なファイルを判断するアルゴリズムが不明なのですが,何やら怪しんでいるような雰囲気です。

実際にファイルをgmailで他のPCとやりとりしてみると,実行形式ファイルも送受できています。しかし,他のPCでは解凍の際と解凍した実行ファイルを実行させる際にWindowsのセキュリティシステムが警告を表示してきます。開発に使ったPCでは,そのような警告は出てきません。

さて,このコードで気になるのは安全性の問題です。zip圧縮は自己解凍の機能が無いはずだし,受け手側で幾つかのステップの処理をするので,比較的安全だと考えています。しかし,自己解凍や自動起動の機能を持つ悪意のあるプログラムの送受に利用できてしまうことは,問題かもしれません。

実際にgmailでのファイル添付に使う場合は,用途を教育用のコードにやり取りに限って利用していただくのが適切だろうと考えています。

コメント