Raise non-integrated upload limit
This commit is contained in:
parent
028faaf0af
commit
fa27245c66
@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Verifies that the public production entrypoint allows product image uploads
|
||||
# below 10MB to reach the app instead of being blocked by the outer nginx layer.
|
||||
# Verifies that the public production entrypoint allows large intake uploads
|
||||
# to reach the app instead of being blocked by the outer proxy/nginx layer.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/check-prod-upload-limit.sh
|
||||
# bash scripts/check-prod-upload-limit.sh https://cp.jiansun.vip 9
|
||||
# bash scripts/check-prod-upload-limit.sh https://cp.jiansun.vip 200
|
||||
#
|
||||
# Expected result for an unauthenticated request is usually 401 from the app.
|
||||
# A 413 response means the public nginx/body-size limit is still too low.
|
||||
# A 413 response means the public proxy/body-size limit is still too low.
|
||||
|
||||
BASE_URL="${1:-https://cp.jiansun.vip}"
|
||||
TEST_SIZE_MB="${2:-9}"
|
||||
TEST_SIZE_MB="${2:-200}"
|
||||
TEST_PRODUCT_ID="upload-limit-check"
|
||||
UPLOAD_URL="${BASE_URL%/}/api/intake/non-integrated/${TEST_PRODUCT_ID}/upload"
|
||||
|
||||
@ -43,8 +43,8 @@ echo "HTTP status: $status"
|
||||
|
||||
case "$status" in
|
||||
413)
|
||||
echo "FAILED: public nginx still blocks uploads below 10MB."
|
||||
echo "Set client_max_body_size 10m; on the cp.jiansun.vip nginx server block, then reload nginx."
|
||||
echo "FAILED: public proxy still blocks ${TEST_SIZE_MB}MB uploads."
|
||||
echo "Set client_max_body_size 500m; or higher on the cp.jiansun.vip proxy/nginx entry, then reload it."
|
||||
exit 1
|
||||
;;
|
||||
401|403|400|404|405|422|200|201)
|
||||
|
||||
@ -13,12 +13,13 @@ import type {
|
||||
NonIntegratedProductRecord,
|
||||
NonIntegratedUploadFile,
|
||||
} from '@/app/intake/non-integrated/nonIntegratedStorage'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import { AppError, ErrorCode } from '@/server/core/errors'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { intakeNonIntegratedProductService } from '@/server/services'
|
||||
|
||||
const MAX_NON_INTEGRATED_UPLOAD_SIZE_MB = 10
|
||||
const MAX_NON_INTEGRATED_UPLOAD_SIZE = MAX_NON_INTEGRATED_UPLOAD_SIZE_MB * 1024 * 1024
|
||||
import {
|
||||
NON_INTEGRATED_UPLOAD_MAX_BYTES,
|
||||
buildNonIntegratedFileTooLargeMessage,
|
||||
} from '../../uploadLimit'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ id: string }>
|
||||
@ -90,15 +91,25 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
return jsonError(ErrorCode.VALIDATION_FAILED, 'The file cannot be empty.', 400, requestId)
|
||||
}
|
||||
|
||||
if (file.size > MAX_NON_INTEGRATED_UPLOAD_SIZE) {
|
||||
if (file.size > NON_INTEGRATED_UPLOAD_MAX_BYTES) {
|
||||
return jsonError(
|
||||
ErrorCode.FILE_TOO_LARGE,
|
||||
`The file is too large. Files over ${MAX_NON_INTEGRATED_UPLOAD_SIZE_MB}MB are not supported.`,
|
||||
buildNonIntegratedFileTooLargeMessage(),
|
||||
400,
|
||||
requestId,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await intakeNonIntegratedProductService.assertCanWrite(productId, user)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonError(error.code, error.message, error.statusCode, requestId)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
const uploadDir = getNonIntegratedAssetDirectory()
|
||||
if (!existsSync(uploadDir)) {
|
||||
await mkdir(uploadDir, { recursive: true })
|
||||
|
||||
@ -70,8 +70,16 @@ function createFormData(file: File) {
|
||||
return formData
|
||||
}
|
||||
|
||||
function createUploadFile(bytes: Uint8Array, fileName: string, type: string) {
|
||||
function createUploadFile(
|
||||
bytes: Uint8Array,
|
||||
fileName: string,
|
||||
type: string,
|
||||
sizeOverride?: number,
|
||||
) {
|
||||
const file = new File([bytes], fileName, { type })
|
||||
if (typeof sizeOverride === 'number') {
|
||||
Object.defineProperty(file, 'size', { value: sizeOverride })
|
||||
}
|
||||
Object.defineProperty(file, 'arrayBuffer', {
|
||||
value: async () =>
|
||||
bytes.buffer.slice(
|
||||
@ -188,4 +196,56 @@ describe('POST /api/intake/non-integrated/upload', () => {
|
||||
)
|
||||
expect(downloadedBytes.toString('utf8')).toBe('patent-certificate')
|
||||
})
|
||||
|
||||
it('allows files below the configured intake upload limit', async () => {
|
||||
const modelBytes = new TextEncoder().encode('model-placeholder')
|
||||
const uploadResponse = await POST({
|
||||
formData: async () =>
|
||||
createFormData(
|
||||
createUploadFile(
|
||||
modelBytes,
|
||||
'product-model.glb',
|
||||
'model/gltf-binary',
|
||||
499 * 1024 * 1024,
|
||||
),
|
||||
),
|
||||
} as any)
|
||||
|
||||
expect(uploadResponse.status).toBe(201)
|
||||
await expect(uploadResponse.json()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
data: expect.objectContaining({
|
||||
fileName: 'product-model.glb',
|
||||
size: 499 * 1024 * 1024,
|
||||
storageKey: expect.stringMatching(/\.glb$/),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects files above the configured intake upload limit', async () => {
|
||||
const uploadResponse = await POST({
|
||||
formData: async () =>
|
||||
createFormData(
|
||||
createUploadFile(
|
||||
new TextEncoder().encode('too-large'),
|
||||
'too-large.glb',
|
||||
'model/gltf-binary',
|
||||
501 * 1024 * 1024,
|
||||
),
|
||||
),
|
||||
} as any)
|
||||
|
||||
expect(uploadResponse.status).toBe(400)
|
||||
await expect(uploadResponse.json()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.objectContaining({
|
||||
code: 'FILE_003',
|
||||
message: '文件过大,最大允许 500MB。',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,12 +5,13 @@ import { randomUUID } from 'crypto'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { buildNonIntegratedAssetUrl } from '@/app/intake/non-integrated/nonIntegratedAssetUrl'
|
||||
import { getNonIntegratedAssetDirectory } from '@/app/intake/non-integrated/nonIntegratedAssetServer'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import { AppError, ErrorCode } from '@/server/core/errors'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { intakeNonIntegratedProductService } from '@/server/services'
|
||||
|
||||
const MAX_NON_INTEGRATED_UPLOAD_SIZE_MB = 10
|
||||
const MAX_NON_INTEGRATED_UPLOAD_SIZE = MAX_NON_INTEGRATED_UPLOAD_SIZE_MB * 1024 * 1024
|
||||
import {
|
||||
NON_INTEGRATED_UPLOAD_MAX_BYTES,
|
||||
buildNonIntegratedFileTooLargeMessage,
|
||||
} from '../uploadLimit'
|
||||
|
||||
function sanitizeExtension(fileName: string) {
|
||||
const extension = path.extname(fileName).trim().toLowerCase()
|
||||
@ -56,14 +57,27 @@ export async function POST(request: NextRequest) {
|
||||
return jsonError(ErrorCode.VALIDATION_FAILED, '文件不能为空', 400, requestId)
|
||||
}
|
||||
|
||||
if (file.size > MAX_NON_INTEGRATED_UPLOAD_SIZE) {
|
||||
if (file.size > NON_INTEGRATED_UPLOAD_MAX_BYTES) {
|
||||
return jsonError(
|
||||
ErrorCode.FILE_TOO_LARGE,
|
||||
`The file is too large. Files over ${MAX_NON_INTEGRATED_UPLOAD_SIZE_MB}MB are not supported.`,
|
||||
buildNonIntegratedFileTooLargeMessage(),
|
||||
400,
|
||||
requestId,
|
||||
)
|
||||
}
|
||||
|
||||
if (productId) {
|
||||
try {
|
||||
await intakeNonIntegratedProductService.assertCanWrite(productId, user)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonError(error.code, error.message, error.statusCode, requestId)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const uploadDir = getNonIntegratedAssetDirectory()
|
||||
if (!existsSync(uploadDir)) {
|
||||
await mkdir(uploadDir, { recursive: true })
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
const DEFAULT_NON_INTEGRATED_UPLOAD_MAX_MB = 500
|
||||
|
||||
function readConfiguredUploadLimitMb() {
|
||||
const rawValue = process.env.NON_INTEGRATED_UPLOAD_MAX_MB
|
||||
const parsedValue = Number(rawValue)
|
||||
|
||||
if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
|
||||
return DEFAULT_NON_INTEGRATED_UPLOAD_MAX_MB
|
||||
}
|
||||
|
||||
return Math.floor(parsedValue)
|
||||
}
|
||||
|
||||
export const NON_INTEGRATED_UPLOAD_MAX_MB = readConfiguredUploadLimitMb()
|
||||
export const NON_INTEGRATED_UPLOAD_MAX_BYTES =
|
||||
NON_INTEGRATED_UPLOAD_MAX_MB * 1024 * 1024
|
||||
|
||||
export function buildNonIntegratedFileTooLargeMessage() {
|
||||
return `文件过大,最大允许 ${NON_INTEGRATED_UPLOAD_MAX_MB}MB。`
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user