ファイル移動/コピーツール『転送助手』 ソースコード

以下は『転送助手』のソースコード全文です:


        import os
        import shutil
        import tkinter as tk
        from tkinter import filedialog, messagebox
        from collections import defaultdict
        
        class FileMoverApp:
            def __init__(self, master):
                self.master = master
                self.master.title("ファイル移動/コピーツール - 転送助手")
                self.source_folder = ""
                self.dest_folder = ""
                self.extension_vars = {}
                self.backup_log = defaultdict(list)
        
                self.create_widgets()
        
            def create_widgets(self):
                # 対象フォルダ選択
                tk.Button(self.master, text="対象フォルダ選択", command=self.select_source_folder).pack(pady=3)
                self.source_label = tk.Label(self.master, text="対象フォルダ: 未選択", fg="gray")
                self.source_label.pack()
        
                # 処理先フォルダ選択
                tk.Button(self.master, text="移動/コピー先フォルダ選択", command=self.select_dest_folder).pack(pady=3)
                self.dest_label = tk.Label(self.master, text="処理先フォルダ: 未選択", fg="gray")
                self.dest_label.pack()
        
                # ファイル情報取得
                tk.Button(self.master, text="ファイル情報取得", command=self.get_file_info).pack(pady=5)
        
                self.extension_frame = tk.LabelFrame(self.master, text="拡張子の選択")
                self.extension_frame.pack(padx=10, pady=5, fill="both")
        
                # 処理方法(移動 or コピー)
                self.action_var = tk.StringVar(value="move")
                tk.Radiobutton(self.master, text="移動", variable=self.action_var, value="move").pack()
                tk.Radiobutton(self.master, text="コピー", variable=self.action_var, value="copy").pack()
        
                # 実行 & 元に戻す
                tk.Button(self.master, text="実行", command=self.execute).pack(pady=5)
                tk.Button(self.master, text="元に戻す", command=self.undo).pack(pady=5)
        
                # 処理状況ウィンドウ
                self.log_text = tk.Text(self.master, height=10, width=60)
                self.log_text.pack(padx=10, pady=10)
                self.log_text.insert(tk.END, "処理状況がここに表示されます。\n")
        
            def log(self, message):
                self.log_text.insert(tk.END, message + "\n")
                self.log_text.see(tk.END)
        
            def select_source_folder(self):
                self.source_folder = filedialog.askdirectory(title="対象フォルダを選択")
                if self.source_folder:
                    self.source_label.config(text=f"対象フォルダ: {self.source_folder}", fg="black")
        
            def select_dest_folder(self):
                self.dest_folder = filedialog.askdirectory(title="移動/コピー先フォルダを選択")
                if self.dest_folder:
                    self.dest_label.config(text=f"処理先フォルダ: {self.dest_folder}", fg="black")
        
            def get_file_info(self):
                for widget in self.extension_frame.winfo_children():
                    widget.destroy()
                self.extension_vars.clear()
        
                if not self.source_folder:
                    messagebox.showwarning("警告", "対象フォルダを選択してください")
                    return
        
                ext_set = set()
                for filename in os.listdir(self.source_folder):
                    if os.path.isfile(os.path.join(self.source_folder, filename)):
                        _, ext = os.path.splitext(filename)
                        if ext:
                            ext_set.add(ext)
        
                for ext in sorted(ext_set):
                    var = tk.BooleanVar()
                    tk.Checkbutton(self.extension_frame, text=ext, variable=var).pack(anchor="w")
                    self.extension_vars[ext] = var
        
            def generate_unique_filename(self, folder, filename):
                base, ext = os.path.splitext(filename)
                counter = 1
                new_filename = filename
                while os.path.exists(os.path.join(folder, new_filename)):
                    new_filename = f"{base}({counter}){ext}"
                    counter += 1
                return new_filename
        
            def execute(self):
                if not self.source_folder or not self.dest_folder:
                    messagebox.showwarning("警告", "対象フォルダと処理先フォルダを両方選択してください")
                    return
        
                action = self.action_var.get()
                selected_exts = [ext for ext, var in self.extension_vars.items() if var.get()]
        
                if not selected_exts:
                    messagebox.showwarning("警告", "拡張子を選択してください")
                    return
        
                self.log("処理開始...")
                count = 0  # 処理件数カウンタ
        
                for filename in os.listdir(self.source_folder):
                    filepath = os.path.join(self.source_folder, filename)
                    if not os.path.isfile(filepath):
                        continue
        
                    _, ext = os.path.splitext(filename)
                    if ext in selected_exts:
                        safe_filename = self.generate_unique_filename(self.dest_folder, filename)
                        dest_path = os.path.join(self.dest_folder, safe_filename)
        
                        if action == "move":
                            shutil.move(filepath, dest_path)
                            self.backup_log[safe_filename].append(filepath)
                            self.log(f"移動: {filename} → {safe_filename}")
                        elif action == "copy":
                            shutil.copy2(filepath, dest_path)
                            self.backup_log[safe_filename].append(None)
                            self.log(f"コピー: {filename} → {safe_filename}")
        
                        count += 1
        
                self.log(f"処理完了。{count}個のファイルを{'移動' if action == 'move' else 'コピー'}しました。")
                messagebox.showinfo("完了", f"{count}個のファイルを{'移動' if action == 'move' else 'コピー'}しました。")
        
            def undo(self):
                self.log("元に戻し中...")
                for filename, paths in self.backup_log.items():
                    if paths[-1]:  # move のとき
                        current_path = os.path.join(self.dest_folder, filename)
                        if os.path.exists(current_path):
                            shutil.move(current_path, paths[-1])
                            self.log(f"戻した: {filename}")
                    else:  # copy のとき
                        copy_path = os.path.join(self.dest_folder, filename)
                        if os.path.exists(copy_path):
                            os.remove(copy_path)
                            self.log(f"削除: {filename}(コピー戻し)")
                self.backup_log.clear()
                self.log("元に戻しました。")
                messagebox.showinfo("元に戻しました", "処理前の状態に戻しました。")
        
        # アプリ起動
        if __name__ == "__main__":
            root = tk.Tk()
            app = FileMoverApp(root)
            root.mainloop()