fix(downloads): normalize target folder paths

This commit is contained in:
saltbo
2026-06-05 10:10:44 -04:00
parent b5154e76d9
commit b05bf4f07e
3 changed files with 61 additions and 1 deletions
@@ -423,6 +423,30 @@ describe('Download tasks API integration', () => {
})
})
it('normalizes target folder paths when creating download tasks', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
await registerDownloaderThroughDeviceLogin(app, 'target-folder-downloader')
const user = await authedHeaders(app, 'target-folder-user@example.com')
const createTaskRes = await app.request('/api/download-tasks', {
method: 'POST',
headers: { ...user, 'Content-Type': 'application/json' },
body: JSON.stringify({
source: { type: 'http', uri: 'https://example.com/fixture.txt' },
targetFolder: '/media//Movies/',
}),
})
expect(createTaskRes.status).toBe(201)
await expect(createTaskRes.json()).resolves.toMatchObject({ targetFolder: 'media/Movies' })
const rows = await db.all<{ target_folder: string }>(
sql`SELECT target_folder FROM download_tasks ORDER BY created_at DESC LIMIT 1`,
)
expect(rows[0].target_folder).toBe('media/Movies')
})
it('returns storage failure details when multipart upload completion fails', async () => {
vi.mocked(S3Service.prototype.completeMultipartUpload).mockRejectedValueOnce(new Error('InvalidPart: part missing'))
const { app, db } = await createTestApp()
+13 -1
View File
@@ -92,13 +92,25 @@ export const createDownloaderSchema = z.object({
const downloadUriSchema = z.string().min(1).max(4096)
const downloadTaskCategorySchema = z.string().trim().min(1).max(120)
const downloadTaskTagsSchema = z.array(z.string().trim().min(1).max(80)).max(20)
const targetFolderSchema = z
.string()
.max(1024)
.transform((value) =>
value
.replace(/\\/g, '/')
.split('/')
.map((part) => part.trim())
.filter((part) => part.length > 0 && part !== '.')
.join('/'),
)
.refine((value) => !value.split('/').includes('..'), { message: 'Target folder cannot contain ..' })
export const createDownloadTaskSchema = z.object({
source: z.object({
type: downloadSourceTypeSchema,
uri: downloadUriSchema,
}),
targetFolder: z.string(),
targetFolder: targetFolderSchema,
name: z.string().min(1).max(255).optional(),
category: downloadTaskCategorySchema.optional(),
tags: downloadTaskTagsSchema.optional(),
+24
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
copyMatterSchema,
createDownloadTaskSchema,
createMatterSchema,
createStorageSchema,
signInSchema,
@@ -124,3 +125,26 @@ describe('copyMatterSchema', () => {
expect(result.success).toBe(true)
})
})
describe('createDownloadTaskSchema', () => {
it('normalizes target folder paths', () => {
const result = createDownloadTaskSchema.safeParse({
source: { type: 'http', uri: 'https://example.com/file.zip' },
targetFolder: '/media//Movies\\2026/',
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.targetFolder).toBe('media/Movies/2026')
}
})
it('rejects parent directory target folder segments', () => {
const result = createDownloadTaskSchema.safeParse({
source: { type: 'http', uri: 'https://example.com/file.zip' },
targetFolder: 'media/../private',
})
expect(result.success).toBe(false)
})
})