解決圖片重複上傳到 AWS S3 的問題:給新手的指南
更新日期: 2024 年 12 月 21 日
本文為 Django 圖片轉 webp 系列教學,第 10 篇:
- 圖片轉換為 WebP 格式並存儲到 AWS S3 的完整指南
- 如何決定儲存 WebP 圖片的方式:覆蓋與直接存儲解析
- 如何在 Django 中處理用戶圖片並自動轉換為 WebP 格式
- 使用 Django 和 AWS S3 實現圖片存儲:基礎指南
- 如何在 Django 中使用 Pillow 處理圖片並轉換為 WebP 格式
- 如何使用 UUID 為圖片生成唯一文件名:Django 文件處理實例
- 使用 Boto3 將 WebP 圖片上傳到 AWS S3:完整指南
- 解決 AWS S3 HeadObject 錯誤 (403 Forbidden):詳細指南
- 解決圖片重複上傳到 AWS S3 的問題:給新手的指南 👈 所在位置
- 如何避免重複存儲不同格式圖片在 AWS S3:新手指南
- 理解 Django 文件字段的行為:新手指南
建議閱讀本文前,先閱讀完 圖片上傳 AWS 功能 系列文
在開發應用時,常常需要處理圖片上傳並存儲到雲端(如 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
),來控制是否執行圖片處理邏輯。
具體做法
- 在信號中設置跳過參數: 在調用
Profile.save()
時,傳入一個參數skip_webp_conversion=True
,讓代碼知道不需要執行圖片處理邏輯。 - 修改
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 時:
- 視圖中的
profile_form.save()
調用:- 沒有傳入
skip_webp_conversion
,所以走第二個分支 - 先保存原始檔案
- 轉換為 WebP 並上傳到 S3
- 用新的 S3 路徑更新 photo 欄位並再次保存
- 這次保存會觸發 User model 的 post_save 信號
- 沒有傳入
- 信號處理器中的保存:
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save(skip_webp_conversion=True)
- 傳入
skip_webp_conversion=True
,走第一個分支 - 只執行基本的保存,不做圖片處理
- 避免了重複轉換和上傳
這個實現有兩個重要的特點:
- 三次
super().save()
調用:- 第一個分支中一次(跳過處理時)
- 第二個分支中兩次(初始保存和更新 S3 路徑後)
- 使用 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:加入跳過邏輯,以靈活控制圖片處理的執行。
總結
圖片重複上傳的問題通常由於 信號和視圖都執行了保存邏輯。為了解決這個問題,可以考慮:
- 刪除信號:最直接的解法,簡化邏輯。
- 引入跳過標誌位:保留信號的功能,靈活控制圖片處理。
- 視圖中跳過保存:僅依賴信號處理圖片邏輯。