Skip to content

Commit b85a6e1

Browse files
Fix custom notification sound not playing
Move /sound routes before /:filename wildcard route to fix route matching order. The wildcard was matching /sound requests first, causing the filename regex check to fail and return 404.
1 parent 29e65a0 commit b85a6e1

1 file changed

Lines changed: 77 additions & 69 deletions

File tree

server/routes/uploads.ts

Lines changed: 77 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -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 (!/^clipboard-\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 (!/^clipboard-\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+
202210
export default app

0 commit comments

Comments
 (0)