2025 OEM 網站開發筆記 [999-2] - martor 編輯器圖片上傳至 localstrage 並自動以文章 slug 命名創建資料夾

不用再使用 imgur 上傳圖片、不用租用線上圖片儲存空間、直接上傳至本地 Server 專案資料夾中

Posted by Young on 2025-04-21
Estimated Reading Time 5 Minutes
Words 1.3k In Total

自訂上傳圖片目的地

透過 imgur 來管理專案圖片是不理想的且 imgur 的圖片連結也會隨著時間而失效

  • MARTOR_UPLOAD_PATH : 儲存圖片的資料夾路徑目的地
  • MARTOR_UPLOAD_URL : 上傳圖片的 API URL(views)
1
2
3
4
5
6
# 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 - 第一版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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

1
2
3
4
5
6
# backend/api/blog/urls.py
urlpatterns = [
# martor 文章編輯器 上傳圖片的 URL
path('image-uploader/', test_view, name='test_image_uploader'),
...
]

結果:

upload_success

views - 第二版本

那確定上傳至本地資料夾能成功運作後,就來升級一下功能,從原本的「生成一個隨機的文件名」- img_uuid = "{0}-{1}".format(uuid.uuid4().hex[:10], filename.replace(' ', '-')) 改成「根據文章的 slug 來生成對應的資料夾名稱」,這樣就能將同一個文章的所有圖片都放在同一資料夾中,提高日後圖片管理的效率跟方便性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@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 請求")

可以看到相較與第一個版本的,這一段多了:

  1. 獲取當前編輯的文章的 slug HTTP_REFERER
  2. 從 referer 中獲取文章 ID 的程式
  3. 保存文件的路徑 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 新增的自訂欄位,如圖:

django_admin_url

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

成功示意圖:

slug_folder_name

settings 更新 MEDIA_ROOT

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

cant_find_img

所以在 settings.py 中將 MEDIA_ROOT 更新成:

1
2
# MEDIA_ROOT = BASE_DIR / 'media'  # 存放用戶上傳的檔案,圖片會存放在 [專案名稱]/backend/media/ 目錄下
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'media') # 上傳的檔案存放路徑,這裡是上傳的圖片會存放在 [專案名稱]/media/ 目錄下

這樣未來用戶上傳圖片就不會存放在 backend/media/ 目錄下了,而是直接存放在 media/ 目錄下,這樣前端的圖片即可正常顯示

update_media_dir

成功畫面

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

image_success


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版