# 自訂上傳圖片目的地
透過 imgur 來管理專案圖片是不理想的且 imgur 的圖片連結也會隨著時間而失效
MARTOR_UPLOAD_PATH: 儲存圖片的資料夾路徑目的地MARTOR_UPLOAD_URL: 上傳圖片的 API URL(views)
# backend/settings.py
...
# Upload to locale storage
MARTOR_UPLOAD_PATH = 'images/uploads/{}'.format(time.strftime("%Y/%m/%d/"))
MARTOR_UPLOAD_URL = '/api/blog/image-uploader/'
MAX_IMAGE_UPLOAD_SIZE = 5 * 1024 * 1024 # 5MB
# views - 第一版本
# backend/api/blog/views.py
@csrf_exempt
def test_view(request):
if request.method == 'POST':
if 'markdown-image-upload' in request.FILES:
image = request.FILES['markdown-image-upload']
# 獲取文件名
filename = image.name
# 生成一個隨機的文件名
img_uuid = "{0}-{1}".format(uuid.uuid4().hex[:10], filename.replace(' ', '-'))
# 構建存儲路徑
upload_path = os.path.join(settings.MEDIA_ROOT, 'images/uploads/')
if not os.path.exists(upload_path):
os.makedirs(upload_path)
# 保存文件
file_path = 'images/uploads/' + img_uuid
saved_path = default_storage.save(file_path, ContentFile(image.read()))
# 返回正確格式的JSON響應
return JsonResponse({
'status': 200,
'link': settings.MEDIA_URL + saved_path,
'name': filename
})
else:
return JsonResponse({
'status': 400,
'error': 'No image found'
})
# 如果不是POST請求,返回測試信息
return HttpResponse(f"Method {request.method} received!")
# urls
# backend/api/blog/urls.py
urlpatterns = [
# martor 文章編輯器 上傳圖片的 URL
path('image-uploader/', test_view, name='test_image_uploader'),
...
]
結果:

# views - 第二版本
那確定上傳至本地資料夾能成功運作後,就來升級一下功能,從原本的「生成一個隨機的文件名」- img_uuid = "{0}-{1}".format(uuid.uuid4().hex[:10], filename.replace(' ', '-')) 改成「根據文章的 slug 來生成對應的資料夾名稱」,這樣就能將同一個文章的所有圖片都放在同一資料夾中,提高日後圖片管理的效率跟方便性
@csrf_exempt
def test_view(request):
if request.method == 'POST':
if 'markdown-image-upload' in request.FILES:
image = request.FILES['markdown-image-upload']
# 獲取當前編輯的文章的 slug
post_slug = None
referer = request.META.get('HTTP_REFERER', '')
# 從 referer 中獲取文章 ID (Django 後台文章編輯路徑為:/admin/blog/post/5/change/)
if 'admin/blog/post/' in referer:
try:
# 從 URL中提取文章 ID,例如 /admin/blog/post/5/change/
post_id = referer.split('/admin/blog/post/')[1].split('/')[0]
if post_id.isdigit():
post = Post.objects.get(id=int(post_id))
post_slug = post.slug
except (IndexError, ValueError, Post.DoesNotExist):
# 如果出錯,使用預設路徑
pass
# 獲取文件名
filename = image.name
# 生成一個隨機的文件名
img_uuid = "{0}-{1}".format(uuid.uuid4().hex[:10], filename.replace(' ', '-'))
# 根據文章 slug 構建儲存路徑
if post_slug:
relative_path = f'posts_images/{post_slug}/'
else:
# 如果無法獲取 slug,使用默認路徑
relative_path = 'posts_images/uploads/'
# 確保目錄存在
upload_path = os.path.join(settings.MEDIA_ROOT, relative_path)
if not os.path.exists(upload_path):
os.makedirs(upload_path)
# 保存文件
file_path = os.path.join(relative_path, img_uuid)
saved_path = default_storage.save(file_path, ContentFile(image.read()))
# 返回正確格式的JSON響應
return JsonResponse({
'status': 200,
'link': settings.MEDIA_URL + saved_path,
'name': filename
})
else:
return JsonResponse({
'status': 400,
'error': '未找到上傳的圖片'
})
# 如果不是POST請求,返回測試信息
return HttpResponse(f"收到 {request.method} 請求,但上傳圖片需要 POST 請求")
可以看到相較與第一個版本的,這一段多了:
- 獲取當前編輯的文章的 slug
HTTP_REFERER - 從 referer 中獲取文章 ID 的程式
- 保存文件的路徑
file_path從'images/uploads/' + img_uuid 改成posts_images/{post_slug}/,這樣就能將同一篇文章的所有圖片都放在同一個資料夾中
這邊要注意為何是要先從 referer 中獲取文章 ID,而不是 slug?我們不是要創建以該文章的 slug 命名的資料夾嗎?
A:因為 Django Admin URL 結構的關係,Django Admin 預設使用主鍵 ID 而非 slug,slug 反而是後來透過 models.py 新增的自訂欄位,如圖:

所以我們要先從 referer 中獲取文章 ID,然後再透過 Post.objects.get(id=int(post_id)) 來獲取該篇文章的 slug,這樣即使在創建新文章時(slug 可能還沒有設置),也能通過 ID 找到正確的文章
成功示意圖:

# settings 更新 MEDIA_ROOT
重點是原本由於 MEDIA_ROOT 初始設定的關係,導致上傳的圖片會存放在 backend/media/ 目錄下,前端會無法正確顯示圖片,因為前端的圖片路徑是直接從 media/ 目錄下讀取的

所以在 settings.py 中將 MEDIA_ROOT 更新成:
# MEDIA_ROOT = BASE_DIR / 'media' # 存放用戶上傳的檔案,圖片會存放在 [專案名稱]/backend/media/ 目錄下
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'media') # 上傳的檔案存放路徑,這裡是上傳的圖片會存放在 [專案名稱]/media/ 目錄下
這樣未來用戶上傳圖片就不會存放在 backend/media/ 目錄下了,而是直接存放在 media/ 目錄下,這樣前端的圖片即可正常顯示

# 成功畫面
這樣特定的文章的圖片就會存放在以該文章的 slug 命名的資料夾中了,也不怕所有文章的圖片都混雜在同一個資料夾中,不好管理的問題

