@@ -26,75 +26,9 @@ function generateFilename(extension: string): string {
2626 return `clipboard-${ date } -${ time } .${ extension } `
2727}
2828
29- // POST /api/uploads
30- // Accepts multipart form data with:
31- // - file: the image file
32- // Images are always saved to {viboraDir}/uploads/
33- app . post ( '/' , async ( c ) => {
34- const body = await c . req . parseBody ( )
35- const file = body [ 'file' ]
36-
37- if ( ! file || ! ( file instanceof File ) ) {
38- return c . json ( { error : 'No file provided' } , 400 )
39- }
40-
41- // Validate it's an image
42- if ( ! file . type . startsWith ( 'image/' ) ) {
43- return c . json ( { error : 'File must be an image' } , 400 )
44- }
45-
46- // Determine extension from mime type
47- const mimeToExt : Record < string , string > = {
48- 'image/png' : 'png' ,
49- 'image/jpeg' : 'jpg' ,
50- 'image/gif' : 'gif' ,
51- 'image/webp' : 'webp' ,
52- 'image/svg+xml' : 'svg' ,
53- }
54- const extension = mimeToExt [ file . type ] || 'png'
55-
56- // Always save to {viboraDir}/uploads/
57- const saveDir = join ( getViboraDir ( ) , 'uploads' )
58-
59- // Ensure directory exists
60- if ( ! existsSync ( saveDir ) ) {
61- await mkdir ( saveDir , { recursive : true } )
62- }
63-
64- // Generate filename and save
65- const filename = generateFilename ( extension )
66- const filePath = join ( saveDir , filename )
67-
68- const arrayBuffer = await file . arrayBuffer ( )
69- await writeFile ( filePath , Buffer . from ( arrayBuffer ) )
70-
71- return c . json ( { path : filePath } )
72- } )
73-
74- // GET /api/uploads/:filename
75- // Serve uploaded images for preview display
76- app . get ( '/:filename' , async ( c ) => {
77- const filename = c . req . param ( 'filename' )
78-
79- // Security: only allow expected filenames (clipboard-YYYY-MM-DD-HHMMSS.ext)
80- if ( ! / ^ c l i p b o a r d - \d { 4 } - \d { 2 } - \d { 2 } - \d { 6 } \. \w + $ / . test ( filename ) ) {
81- return c . notFound ( )
82- }
83-
84- const filePath = join ( getViboraDir ( ) , 'uploads' , filename )
85-
86- if ( ! existsSync ( filePath ) ) {
87- return c . notFound ( )
88- }
89-
90- const ext = filename . split ( '.' ) . pop ( ) ?. toLowerCase ( ) || ''
91- const contentType = mimeTypes [ ext ] || 'application/octet-stream'
92-
93- const content = await readFile ( filePath )
94- return new Response ( content , {
95- headers : { 'Content-Type' : contentType } ,
96- } )
97- } )
29+ // ============================================
30+ // SOUND ROUTES (must be before /:filename wildcard)
31+ // ============================================
9832
9933// POST /api/uploads/sound
10034// Upload a custom notification sound file
@@ -199,4 +133,78 @@ app.get('/sound', async (c) => {
199133 } )
200134} )
201135
136+ // ============================================
137+ // IMAGE UPLOAD ROUTES
138+ // ============================================
139+
140+ // POST /api/uploads
141+ // Accepts multipart form data with:
142+ // - file: the image file
143+ // Images are always saved to {viboraDir}/uploads/
144+ app . post ( '/' , async ( c ) => {
145+ const body = await c . req . parseBody ( )
146+ const file = body [ 'file' ]
147+
148+ if ( ! file || ! ( file instanceof File ) ) {
149+ return c . json ( { error : 'No file provided' } , 400 )
150+ }
151+
152+ // Validate it's an image
153+ if ( ! file . type . startsWith ( 'image/' ) ) {
154+ return c . json ( { error : 'File must be an image' } , 400 )
155+ }
156+
157+ // Determine extension from mime type
158+ const mimeToExt : Record < string , string > = {
159+ 'image/png' : 'png' ,
160+ 'image/jpeg' : 'jpg' ,
161+ 'image/gif' : 'gif' ,
162+ 'image/webp' : 'webp' ,
163+ 'image/svg+xml' : 'svg' ,
164+ }
165+ const extension = mimeToExt [ file . type ] || 'png'
166+
167+ // Always save to {viboraDir}/uploads/
168+ const saveDir = join ( getViboraDir ( ) , 'uploads' )
169+
170+ // Ensure directory exists
171+ if ( ! existsSync ( saveDir ) ) {
172+ await mkdir ( saveDir , { recursive : true } )
173+ }
174+
175+ // Generate filename and save
176+ const filename = generateFilename ( extension )
177+ const filePath = join ( saveDir , filename )
178+
179+ const arrayBuffer = await file . arrayBuffer ( )
180+ await writeFile ( filePath , Buffer . from ( arrayBuffer ) )
181+
182+ return c . json ( { path : filePath } )
183+ } )
184+
185+ // GET /api/uploads/:filename
186+ // Serve uploaded images for preview display
187+ app . get ( '/:filename' , async ( c ) => {
188+ const filename = c . req . param ( 'filename' )
189+
190+ // Security: only allow expected filenames (clipboard-YYYY-MM-DD-HHMMSS.ext)
191+ if ( ! / ^ c l i p b o a r d - \d { 4 } - \d { 2 } - \d { 2 } - \d { 6 } \. \w + $ / . test ( filename ) ) {
192+ return c . notFound ( )
193+ }
194+
195+ const filePath = join ( getViboraDir ( ) , 'uploads' , filename )
196+
197+ if ( ! existsSync ( filePath ) ) {
198+ return c . notFound ( )
199+ }
200+
201+ const ext = filename . split ( '.' ) . pop ( ) ?. toLowerCase ( ) || ''
202+ const contentType = mimeTypes [ ext ] || 'application/octet-stream'
203+
204+ const content = await readFile ( filePath )
205+ return new Response ( content , {
206+ headers : { 'Content-Type' : contentType } ,
207+ } )
208+ } )
209+
202210export default app
0 commit comments