2. USBを挿さないと起動できないギミック

CustomTkinter
import subprocess
import sys
import os
import json
import sqlite3
import re
import customtkinter as ct
from tkinter import messagebox 

CONFIG_FILE = "usb_auth.json"
DB_NAME = "passwords.db"


# ==========
# 挿入されているすべてのUSB情報を取得
# ==========
def get_usb_list():
    usb_dict = {}
    try:
        # PCに認識されている倫理ディスク(logicaldisk)を取得する
        # logicaldiskにはドライブレター(C:,D:,E:等)とその説明(LocalFixedDisk,RemovalDisk等)が書かれている
        # ここで「RemovalDisk」となっているものがUSBメモリやSDカードを意味する
        drives = subprocess.check_output(
            "wmic logicaldisk get name,description",
            shell=True
        ).decode("utf-8", errors="ignore").splitlines()
        # driveからRemovableを含む行(line)を探し、line.split()[0](最初のトークン、通常はE:のようなもの)を抜き出す
        removable_drives = []
        for line in drives:
            line = line.strip()
            if "Removable" in line:
                m = re.search(r"([A-Z]:)", line)
                if m:
                    removable_drives.append(m.group(1))
        # 初期値として「不明なUSB」をセット
        for drive in removable_drives:
            usb_dict[drive] = ("不明なUSB", "UNKNOWN")
        

        # 実際にPCに接続されているハードウェア情報(diskdrive)を取得する
        # 物理ディスク(接続ディスク)の製品名(Caption)とシリアル番号を取得
        result = subprocess.check_output(
            "wmic diskdrive get Caption,DeviceID,SerialNumber",
            shell=True
        ).decode("utf-8", errors="ignore").splitlines()   
        # resultからUSBを含む行(line)を探し、line.split()して分解
        for line in result:
            if "USB" in line:
                parts = line.split()
                if len(parts) >= 3:
                    caption = " ".join(parts[:-2]) #製品名
                    device_id = parts[-2] #DeviceID
                    serial = parts[-1] #シリアル番号

                    for d in usb_dict:
                        if usb_dict[d][1] == "UNKNOWN":
                            usb_dict[d] = (caption, serial)
                            break
    
    except Exception as e:
        print("USB取得エラー:", e)
    
    return usb_dict


# ==========
# 指定したserial(USBのシリアル番号)とドライブレターをJSONファイル(CONFIGファイル)に上書き保存する
# ==========
def save_usb(serial, drive):
    with open(CONFIG_FILE, "w") as f:
        json.dump({"serial": serial, "drive": drive}, f)



# ==========
# CONFIGファイルをロードして登録したUSBのシリアル番号を取得
# ==========
def load_usb():
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r") as f:
            return json.load(f) # dictを返す(configファイルがなければFalseを返す)



# ==========
# 挿したUSBのシリアル番号が登録したものと合致するか確認する
# ==========
def check_usb():
    # configは{"serial": "xxx", "drive": "E:"}のようなdictになる
    config = load_usb()
    # CONFIGファイルが空ならFalse
    if not config:
        return False
    # dictからシリアル番号を取り出す
    saved_serial = config.get("serial")
    usb_list = get_usb_list()
    return saved_serial in [serial for _, serial in usb_list.values()]



# ==========
# USBを登録していない場合は、選択画面を表示する
# ==========
def select_usb():
    usb_list = get_usb_list()
    if not usb_list:
        messagebox.showerror("エラー", "USBが見つかりません")
        sys.exit()
    win = ct.CTk()
    win.title("USB選択")
    ct.CTkLabel(win, text="起動に使うUSBを選択してください").pack(pady=10)

    #ボタンを押すとメッセージボックスを表示
    def on_select(serial, drive):
        save_usb(serial, drive)
        messagebox.showinfo("登録完了", f"USB({serial})が登録されました。もう一度アプリを起動させてください。")
        win.destroy()
    
    #Removable - BUFFALO USB - 0F7315812030 のようなボタンが表示される
    for drive, (caption, serial) in usb_list.items():
        text = f"{drive} - {caption} - {serial}"
        ct.CTkButton(win, text=text, command=lambda s=serial, d=drive: on_select(s, d)).pack(pady=5)
    
    win.mainloop()



# ==========
# DB初期化
# ==========
def init_db(db_path):
    conn = sqlite3.connect(db_path)
    c = conn.cursor()
    c.execute("""CREATE TABLE IF NOT EXISTS accounts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        service TEXT,
        username TEXT,
        password TEXT
    )""")
    conn.commit()
    return conn


# ==========
# GUI
# ==========
def run_app(conn):
    global service_entry, user_entry, pass_entry, listbox, root

    root = ct.CTk()
    root.title("USB管理アプリ")
    root.geometry("500x450")

    #
    service_entry = ct.CTkEntry(root, placeholder_text="サービス名")
    service_entry.pack(pady=5)

    user_entry = ct.CTkEntry(root, placeholder_text="ユーザー名")
    user_entry.pack(pady=5)

    pass_entry = ct.CTkEntry(root, placeholder_text="パスワード", show="*")
    pass_entry.pack(pady=5)

    ct.CTkButton(root, text="追加", command=lambda: add_account(conn)).pack(pady=5)
    ct.CTkButton(root, text="表示更新", command=lambda: show_account(conn)).pack(pady=5)

    # 一覧表示
    listbox = ct.CTkTextbox(root, width=450, height=200)
    listbox.pack(pady=10)

    # USB変更ボタン
    ct.CTkButton(root, text="USBを更新する", command=change_usb).pack(pady=10)

    # 最初の表示
    show_account(conn)

    root.mainloop()


# ==========
# データベースに追加
# ==========
def add_account(conn):
    service = service_entry.get()
    user = user_entry.get()
    password = pass_entry.get()

    if not service or not user or not password:
        messagebox.showerror("エラー", "すべて入力してください")
    
    c = conn.cursor()
    c.execute("INSERT INTO accounts (service, username, password) VALUES (?, ?, ?)", (service, user, password))
    conn.commit()
    messagebox.showinfo("保存", "データを保存しました")
    show_account(conn)



# ==========
# アカウントの表示
# ==========
def show_account(conn):
    # listboxの1行目の1文字目(0.0)からendまでをDELETEする
    listbox.delete("0.0", "end")
    c = conn.cursor()
    c.execute("SELECT service, username, password FROM accounts")
    for row in c.fetchall():
        listbox.insert("end", f"{row[0]} | {row[1]} | {row[2]}\n")



# ==========
# USBの変更
# ==========
def change_usb():
    select_usb()
    messagebox.showinfo("USB変更", "USBが変更されました。次回起動から有効です。")




# ==========
# アプリを起動したときの処理
# ==========
# configファイルからdictを取得
config = load_usb()

if not config:
    # CONFIGファイルがない初回起動ではUSB選択画面を表示する
    select_usb()
    # 一度システム終了する
    sys.exit()

# 登録USBを挿入していなければエラーを表示する
if not check_usb():
    messagebox.showerror("エラー", "認証USBが見つかりません。アプリを終了します。")
    sys.exit()

# CONFIGファイルがある場合は、dictからドライブレターを取得
drive = config.get("drive")

# 念の為、PATHが存在するか確認
usb_path = drive + os.sep
if not os.path.exists(usb_path):
    messagebox.showerror("エラー", f"USBドライブ{drive}が見つかりません")
    sys.exit()

# DBファイルの絶対パスを作成
# os.sepは"/"と同じ意味
db_path = os.path.join(drive + os.sep, DB_NAME)

# USB内のDBを初期化する
conn = init_db(db_path)

#画面表示
ct.set_appearance_mode("dark")
ct.set_default_color_theme("blue")
run_app(conn)

BACK