@@ -13,10 +13,17 @@ import Link from "next/link";
1313import type { Session } from "next-auth" ;
1414import { buildDocsNewUrl } from "@/lib/github" ;
1515import {
16- FILENAME_PATTERN ,
16+ MAX_SLUG_LENGTH ,
1717 ensureMarkdownExtension ,
18+ sanitizeSlug ,
1819 stripMarkdownExtension ,
20+ validateSlug ,
1921} from "@/lib/submission" ;
22+ import {
23+ MAX_IMAGE_UPLOAD_BYTES ,
24+ isAllowedImageContentType ,
25+ isExtensionAllowedForContentType ,
26+ } from "@/lib/uploads" ;
2027
2128interface EditorPageClientProps {
2229 session : Session ;
@@ -58,6 +65,8 @@ function buildFrontmatter({
5865 return lines . join ( "\n" ) ;
5966}
6067
68+ const MAX_IMAGE_SIZE_MB = MAX_IMAGE_UPLOAD_BYTES / ( 1024 * 1024 ) ;
69+
6170/**
6271 * 编辑器页面客户端组件
6372 * 包含表单、编辑器和发布按钮
@@ -82,6 +91,24 @@ export function EditorPageClient({ session }: EditorPageClientProps) {
8291 file : File ,
8392 articleSlug : string ,
8493 ) : Promise < { blobUrl : string ; publicUrl : string } > => {
94+ const contentType = file . type . toLowerCase ( ) ;
95+
96+ if ( ! validateSlug ( articleSlug ) ) {
97+ throw new Error ( "文章 slug 不合法,请检查文件名。" ) ;
98+ }
99+
100+ if ( ! isAllowedImageContentType ( contentType ) ) {
101+ throw new Error ( "仅支持上传 jpg/png/gif/webp 图片。" ) ;
102+ }
103+
104+ if ( ! isExtensionAllowedForContentType ( file . name , contentType ) ) {
105+ throw new Error ( "文件扩展名与图片类型不匹配。" ) ;
106+ }
107+
108+ if ( file . size <= 0 || file . size > MAX_IMAGE_UPLOAD_BYTES ) {
109+ throw new Error ( `图片大小需在 0 - ${ MAX_IMAGE_SIZE_MB } MB 之间。` ) ;
110+ }
111+
85112 // 1. 获取预签名 URL
86113 const response = await fetch ( "/api/upload" , {
87114 method : "POST" ,
@@ -90,8 +117,9 @@ export function EditorPageClient({ session }: EditorPageClientProps) {
90117 } ,
91118 body : JSON . stringify ( {
92119 filename : file . name ,
93- contentType : file . type ,
120+ contentType,
94121 articleSlug,
122+ fileSize : file . size ,
95123 } ) ,
96124 } ) ;
97125
@@ -100,15 +128,26 @@ export function EditorPageClient({ session }: EditorPageClientProps) {
100128 throw new Error ( error . error || "获取上传链接失败" ) ;
101129 }
102130
103- const { uploadUrl, publicUrl } = await response . json ( ) ;
131+ const { uploadUrl, publicUrl, fields } = ( await response . json ( ) ) as {
132+ uploadUrl : string ;
133+ publicUrl : string ;
134+ fields : Record < string , string > ;
135+ } ;
136+
137+ if ( ! uploadUrl || ! fields ) {
138+ throw new Error ( "上传参数缺失,请稍后重试。" ) ;
139+ }
104140
105141 // 2. 上传文件到 R2
142+ const formData = new FormData ( ) ;
143+ Object . entries ( fields ) . forEach ( ( [ field , value ] ) => {
144+ formData . append ( field , value ) ;
145+ } ) ;
146+ formData . append ( "file" , file ) ;
147+
106148 const uploadResponse = await fetch ( uploadUrl , {
107- method : "PUT" ,
108- headers : {
109- "Content-Type" : file . type ,
110- } ,
111- body : file ,
149+ method : "POST" ,
150+ body : formData ,
112151 } ) ;
113152
114153 if ( ! uploadResponse . ok ) {
@@ -139,8 +178,16 @@ export function EditorPageClient({ session }: EditorPageClientProps) {
139178
140179 const normalizedFilename = ensureMarkdownExtension ( filename ) ;
141180 const filenameBase = stripMarkdownExtension ( normalizedFilename ) ;
142- if ( ! filenameBase || ! FILENAME_PATTERN . test ( filenameBase ) ) {
143- alert ( "文件名仅支持英文、数字、连字符或下划线,并需以字母或数字开头。" ) ;
181+ const articleSlug = sanitizeSlug ( filenameBase ) ;
182+
183+ if (
184+ ! articleSlug ||
185+ ! validateSlug ( articleSlug ) ||
186+ articleSlug !== filenameBase
187+ ) {
188+ alert (
189+ `文件名仅支持英文、数字、连字符或下划线,并需以字母或数字开头且不超过 ${ MAX_SLUG_LENGTH } 个字符。` ,
190+ ) ;
144191 return ;
145192 }
146193
@@ -166,9 +213,7 @@ export function EditorPageClient({ session }: EditorPageClientProps) {
166213 console . log ( "文件名:" , normalizedFilename ) ;
167214 console . log ( "投稿目录:" , destinationPath ) ;
168215 console . log ( "图片数量:" , imageCount ) ;
169-
170216 let finalMarkdown = markdown ;
171- const articleSlug = filenameBase ;
172217
173218 // 如果有图片,上传到 R2 并替换 URL
174219 const editorHandle = editorRef . current ;
0 commit comments