解決圖片重複上傳到 AWS S3 的問題:給新手的指南

更新日期: 2024 年 12 月 21 日

在開發應用時,常常需要處理圖片上傳並存儲到雲端(如 AWS S3)。

如果你發現同一張圖片被重複上傳,這可能是因為你的代碼在 信號(signals)視圖(views) 中都執行了圖片保存邏輯。

這篇文章會用簡單易懂的方式幫助新手了解問題的原因,並提供三種解決方法,讓你的代碼更高效。


問題:為什麼圖片會被重複上傳?

圖片重複上傳的原因是 save() 方法被多次執行,具體表現如下:

信號中的邏輯

  • 當你保存 User 模型時,post_save 信號會自動觸發。
  • 在信號中,你調用了 Profile.save() 方法,這裡的邏輯包括圖片處理(如轉換為 WebP 格式並上傳到 S3)。

視圖中的邏輯

  • 當用戶提交修改個人資料的表單時,profile_form.save() 也會執行 Profile.save() 方法。
  • 這再次執行了圖片處理和上傳邏輯。

執行順序

  • 第一次執行:信號中的邏輯保存了 Profile,觸發了圖片處理和上傳。
  • 第二次執行:視圖中的邏輯又保存了一次 Profile,重複了圖片處理和上傳。

結果是,每次提交表單,圖片都會被處理並上傳兩次,造成不必要的資源浪費。


解決方法

解決圖片重複上傳的問題需要避免重複執行 Profile.save() 的圖片處理邏輯。以下是三種簡單有效的方法:

方法 1:在 save() 中加入標誌位跳過圖片處理

這種方法的核心是通過一個參數(如 skip_webp_conversion),來控制是否執行圖片處理邏輯。

具體做法

  1. 在信號中設置跳過參數: 在調用 Profile.save() 時,傳入一個參數 skip_webp_conversion=True,讓代碼知道不需要執行圖片處理邏輯。
  2. 修改 Profile.save() 方法: 在 save() 方法中檢查這個參數,決定是否執行圖片處理。

代碼示例

信號的代碼

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    # 傳遞 skip_webp_conversion=True,避免重複執行圖片處理邏輯
    instance.profile.save(skip_webp_conversion=True)

Profile.save() 方法

def save(self, *args, **kwargs):
    # 如果 skip_webp_conversion 為 True,跳過圖片處理
    if kwargs.pop('skip_webp_conversion', False):
        super().save(*args, **kwargs)
        return

    # 正常保存邏輯
    super().save(*args, **kwargs)

    if self.photo:
        # 執行圖片處理邏輯
        try:
            image = Image.open(self.photo)

            webp_image_io = BytesIO()
            image.save(webp_image_io, format="WEBP", quality=85)
            webp_image_io.seek(0)

            unique_filename = f"profile_photos/{uuid.uuid4()}.webp"

            # 上傳到 S3
            s3 = boto3.client("s3", region_name=settings.AWS_S3_REGION_NAME)
            s3.upload_fileobj(
                webp_image_io,
                settings.AWS_STORAGE_BUCKET_NAME,
                unique_filename,
                ExtraArgs={"ContentType": "image/webp"},
            )

            # 更新 photo 欄位
            self.photo = unique_filename

            # 保存更新後的 photo 欄位
            super().save(*args, **kwargs)
        except Exception as e:
            raise RuntimeError(f"Error converting image to webp: {e}")

優點

  • 保留信號的功能(如自動保存 Profile 或觸發其他操作)。
  • 確保圖片處理邏輯只執行一次。

缺點

  • 稍微增加了 Profile.save() 方法的複雜度。
補充:上述代碼解析

讓我們分析這個具體的 save 方法實現:

def save(self, *args, **kwargs):
    # 第一個分支:跳過圖片處理
    if kwargs.pop('skip_webp_conversion', False):
        super().save(*args, **kwargs)
        return

    # 第二個分支:執行完整的保存和轉換邏輯
    super().save(*args, **kwargs)
    if self.photo:
        # WebP 轉換和 S3 上傳邏輯...
        # ...
        # 最後再保存一次更新後的 photo 欄位
        super().save(*args, **kwargs)

對於每次上傳圖片的流程:

上傳圖片 A 時:

  1. 視圖中的 profile_form.save() 調用:
    • 沒有傳入 skip_webp_conversion,所以走第二個分支
    • 先保存原始檔案
    • 轉換為 WebP 並上傳到 S3
    • 用新的 S3 路徑更新 photo 欄位並再次保存
    • 這次保存會觸發 User model 的 post_save 信號
  1. 信號處理器中的保存:
   @receiver(post_save, sender=User)
   def save_user_profile(sender, instance, **kwargs):
       instance.profile.save(skip_webp_conversion=True)
  1. 傳入 skip_webp_conversion=True,走第一個分支
  2. 只執行基本的保存,不做圖片處理
  3. 避免了重複轉換和上傳

這個實現有兩個重要的特點:

  1. 三次 super().save() 調用:
    • 第一個分支中一次(跳過處理時)
    • 第二個分支中兩次(初始保存和更新 S3 路徑後)
  1. 使用 S3 而不是本地存儲:
    • 每次轉換後生成新的 UUID 文件名
    • 直接上傳到 S3,不需要處理本地文件
    • 通過 photo 欄位記錄 S3 路徑

方法 2:刪除 save_user_profile 信號

如果信號的唯一目的是保存 Profile,而這部分邏輯已經在視圖中處理了,那麼直接刪除信號會更簡單。

具體做法

刪除信號代碼,讓視圖負責保存 Profile 和圖片處理。

刪除信號

# 刪除這段信號代碼
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

視圖代碼(不需要修改):

if user_form.is_valid() and profile_form.is_valid():
    user_form.save()
    profile_form.save()  # 這裡會執行 Profile.save(),處理圖片邏輯
    messages.success(request, "個人資料已成功更新")
    return redirect("users:profile")

優點

  • 邏輯更簡單,代碼更清晰。
  • 不需要改動 Profile.save() 方法。

缺點

  • 如果信號中還有其他功能(如初始化 Profile 或觸發其他操作),刪除信號可能會影響這些功能。

方法 3:在視圖中跳過 profile_form.save()

如果你希望信號處理圖片邏輯,而視圖只處理其他字段,可以在視圖中跳過 profile_form.save()

具體做法

在視圖中,只保存 User 表單,跳過 Profile 表單的保存。

視圖代碼

if user_form.is_valid() and profile_form.is_valid():
    user_form.save()
    # 跳過 profile_form.save(),避免重複執行圖片處理
    messages.success(request, "個人資料已成功更新")
    return redirect("users:profile")

優點

  • 保留信號的圖片處理邏輯。
  • 不需要修改 Profile.save() 方法。

缺點

  • 如果 profile_form 包含非圖片字段,這些字段的更新可能不會生效。

哪種方法適合你?

推薦方法

  • 方法 2:刪除信號 是最簡單且高效的解法。如果信號的唯一功能是保存 Profile,建議直接刪除,將保存邏輯完全交給視圖處理。
  • 如果信號有其他業務需求,建議使用 方法 1:加入跳過邏輯,以靈活控制圖片處理的執行。

總結

圖片重複上傳的問題通常由於 信號和視圖都執行了保存邏輯。為了解決這個問題,可以考慮:

  1. 刪除信號:最直接的解法,簡化邏輯。
  2. 引入跳過標誌位:保留信號的功能,靈活控制圖片處理。
  3. 視圖中跳過保存:僅依賴信號處理圖片邏輯。

Similar Posts