mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 08:16:58 +08:00
fix(downloads): normalize target folder paths
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user