iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Build on Google AI

給藥袋裝一張嘴:30 天用 Android 與 Google VLM 實作高齡語音用藥助手系列 第 20 篇

Day 20|資料安全不漏接!實作 SQLite 資料庫自動定期備份與災難復原機制

  • 分享至 

  • xImage
  •  

✏️【本日實作紀錄:資料庫自動化備份與熱還原防線】

在 Day 19 完成 Redis SHA-256 雜湊快取 與 Flask-Limiter 限流機制 後,PrescriptionVLM 系統已具備高效能的回應速度與 API 流量防範。

然而,後端系統中儲存的「長輩歷史用藥紀錄」與「關懷卡片狀態」是整個應用的核心資料。如果宿主機突發斷電、磁區損壞或容器遭誤刪,導致 SQLite 資料庫檔案(prescription_vlm.db)損毀,會失去長期紀錄。

今天我們將為 PrescriptionVLM 系統建立 資料庫自動化備份與備援復原機制。完成項目包含:

  • APScheduler 自動化定時排程 — 在 Flask 背景服務中建立 CRON 任務,定期執行 SQLite 線上熱備份。
  • AES-256 備份加密與滾動保留 — 備份檔自動加解密並限制保留最新的 7 天備份,防止硬碟空間爆滿。
  • 災難復原 CLI 工具 (restore_db.py) — 提供一鍵解密與資料庫結構快速還原腳本,系統遭遇損壞時能快速復原服務。

一、SQLite 線上熱備份的設計

在生產環境中直接複製 SQLite 檔案容易造成資料損毀,因為系統可能正在寫入資料。

為此我們採用 SQLite 官方提供的 Online Backup API,其優勢如下:

  • 零停機熱備份:無須暫停 Flask 服務或鎖定資料庫,即可在背景取得一致性快照。
  • 滾動保留機制:自動清理超過指定天數的舊備份,維持健康的磁碟空間使用率。
  • 安全防護:備份檔案經由 Fernet (AES-128/256) 加密,即使備份檔外流也無法直接存取長輩的用藥隱私。

二、升級專案依賴(requirements.txt)

請開啟專案根目錄的 requirements.txt,加入背景排程與加密套件:

APScheduler==3.10.4
cryptography==42.0.5

三、實作資料庫備份模組(backup_service.py)

在專案根目錄新建 backup_service.py 模組,處理排程執行、熱備份與滾動清理:

import os
import sqlite3
import shutil
from datetime import datetime, timedelta
from cryptography.fernet import Fernet
from logger import logger

DB_PATH = "prescription_vlm.db"
BACKUP_DIR = "backups"
RETENTION_DAYS = 7

# 從環境變數讀取金鑰,若無則自動生成
ENCRYPTION_KEY = os.getenv("BACKUP_ENCRYPTION_KEY", Fernet.generate_key().decode())
fernet = Fernet(ENCRYPTION_KEY.encode() if isinstance(ENCRYPTION_KEY, str) else ENCRYPTION_KEY)

def perform_sqlite_backup():
    """執行 SQLite 安全熱備份與加密檔案寫入"""
    if not os.path.exists(DB_PATH):
        logger.warning(f"資料庫檔案 {DB_PATH} 不存在,跳過備份")
        return

    os.makedirs(BACKUP_DIR, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    temp_backup_file = os.path.join(BACKUP_DIR, f"temp_{timestamp}.db")
    final_enc_file = os.path.join(BACKUP_DIR, f"backup_{timestamp}.db.enc")

    try:
        # 1. 使用 sqlite3 online backup API 進行一致性快照
        src_conn = sqlite3.connect(DB_PATH)
        dst_conn = sqlite3.connect(temp_backup_file)
        with dst_conn:
            src_conn.backup(dst_conn)
        src_conn.close()
        dst_conn.close()

        # 2. 對備份檔案進行 AES 加密
        with open(temp_backup_file, "rb") as f_in:
            data = f_in.read()
        encrypted_data = fernet.encrypt(data)

        with open(final_enc_file, "wb") as f_out:
            f_out.write(encrypted_data)

        # 清除未加密的暫存檔
        os.remove(temp_backup_file)
        logger.info(f"資料庫熱備份與加密成功: {final_enc_file}")

        # 3. 滾動清理超過 7 天的舊備份
        cleanup_old_backups()

    except Exception as e:
        logger.error(f"資料庫備份失敗: {str(e)}")

def cleanup_old_backups():
    """清理超過 Retention 天數的備份檔"""
    now = datetime.now()
    for filename in os.listdir(BACKUP_DIR):
        file_path = os.path.join(BACKUP_DIR, filename)
        if filename.endswith(".db.enc"):
            file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
            if now - file_time > timedelta(days=RETENTION_DAYS):
                os.remove(file_path)
                logger.info(f"已自動清理過期備份檔: {filename}")

四、在 Flask 中掛載背景排程(app.py)

開啟 app.py,導入 APScheduler 並於服務啟動時建立備份任務。

from apscheduler.schedulers.background import BackgroundScheduler
from backup_service import perform_sqlite_backup

# 初始化 BackgroundScheduler
scheduler = BackgroundScheduler()
# 設定每天凌晨 02:00 執行熱備份(測試階段可改為每 1 小時或每 10 分鐘)
scheduler.add_job(func=perform_sqlite_backup, trigger="cron", hour=2, minute=0, id="db_backup_job")
scheduler.start()

logger.info("資料庫定時備份排程器已啟動")

五、實作災難復原工具(restore_db.py)

在專案根目錄建立一鍵復原腳本 restore_db.py:

import sys
import os
import shutil
from cryptography.fernet import Fernet

BACKUP_DIR = "backups"
TARGET_DB = "prescription_vlm.db"

def restore_latest_backup():
    """解密並還原最新的加密備份檔"""
    key = os.getenv("BACKUP_ENCRYPTION_KEY")
    if not key:
        print("錯誤:未設定 BACKUP_ENCRYPTION_KEY 環境變數")
        return

    fernet = Fernet(key.encode())
    enc_files = [os.path.join(BACKUP_DIR, f) for f in os.listdir(BACKUP_DIR) if f.endswith(".db.enc")]
    
    if not enc_files:
        print("未找到任何加密備份檔 (.db.enc)")
        return

    # 取得最新修改時間的備份檔
    latest_enc_file = max(enc_files, key=os.path.getmtime)
    print(f"找到最新備份檔: {latest_enc_file}")

    try:
        with open(latest_enc_file, "rb") as f:
            encrypted_data = f.read()

        decrypted_data = fernet.decrypt(encrypted_data)

        # 備份現有毀損的 DB (若存在)
        if os.path.exists(TARGET_DB):
            shutil.copy(TARGET_DB, f"{TARGET_DB}.corrupted.bak")

        with open(TARGET_DB, "wb") as f:
            f.write(decrypted_data)

        print(f"資料庫已成功還原至: {TARGET_DB}")

    except Exception as e:
        print(f"還原過程發生錯誤: {str(e)}")

if __name__ == "__main__":
    restore_latest_backup()

六、測試與成果驗證

1. 手動觸發備份驗證

在 Terminal 進入容器或直接於本地執行測試備份:

python -c "from backup_service import perform_sqlite_backup; perform_sqlite_backup()"
  • 預期成果:專案根目錄會產生 backups/ 資料夾,且裡面包含 backup_YYYYMMDD_HHMMSS.db.enc 加密檔。

2. 模擬資料庫損壞與一鍵復原

故意刪除目前的資料庫檔案:

rm prescription_vlm.db

執行一鍵復原腳本:

python restore_db.py
  • 預期成果:Terminal 印出 資料庫已成功還原至: prescription_vlm.db,且 Flask 服務重啟後能正常讀取之前的用藥歷史紀錄。

七、版本控制與提交 GitHub

測試通過後,將 Day 20 的修改提交至 GitHub:

git add .
git commit -m "保留雙引號 改填寫自己要記錄的標記 ex.鐵人賽第二十天"
git push

八、本日小結與明日預告

今天我們透過 SQLite Online Backup API、Fernet 加密 與 APScheduler 背景排程,為 PrescriptionVLM 系統建構了自動化熱備份與災難復原防線,確保長輩的用藥資料即使遭遇硬體障礙也能快速還原。

明天(Day 21),我們將針對整體系統進行 Docker 容器多階段建構與映像檔輕量化,優化部署體積,打造更輕量的生產環境版本!


上一篇
Day 19|拒絕重複花費與爆額度!實作 Redis 雜湊快取與 API 限流防護
下一篇
Day 21|映像檔瘦身 60%!實作 Docker 多階段建構與生產環境極速部署
系列文
給藥袋裝一張嘴:30 天用 Android 與 Google VLM 實作高齡語音用藥助手 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言