mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-04 07:46:16 +02:00
fix: standardize API error responses, fix parseInt radix, harden Docker service
- Create api_response.ts helper for consistent { success, error/data } format
- Add radix parameter (10) to all parseInt calls across controllers and services
- Fix race condition in DockerService by making in-memory guard atomic
- Fix container command splitting to handle quoted arguments properly
- Stop leaking internal error.message to API responses; log details server-side
https://claude.ai/code/session_01JFvpTYgm8GiE4vJ4cJKsFx
This commit is contained in:
parent
9ca20a99e5
commit
def1a0733f
|
|
@ -1,5 +1,6 @@
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import logger from '@adonisjs/core/services/logger'
|
||||||
import { BenchmarkService } from '#services/benchmark_service'
|
import { BenchmarkService } from '#services/benchmark_service'
|
||||||
import { runBenchmarkValidator, submitBenchmarkValidator } from '#validators/benchmark'
|
import { runBenchmarkValidator, submitBenchmarkValidator } from '#validators/benchmark'
|
||||||
import { RunBenchmarkJob } from '#jobs/run_benchmark_job'
|
import { RunBenchmarkJob } from '#jobs/run_benchmark_job'
|
||||||
|
|
@ -52,9 +53,11 @@ export default class BenchmarkController {
|
||||||
result,
|
result,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const detail = error instanceof Error ? error.message : String(error)
|
||||||
|
logger.error(`[BenchmarkController] Sync benchmark failed: ${detail}`)
|
||||||
return response.status(500).send({
|
return response.status(500).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: 'Benchmark execution failed',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +171,12 @@ export default class BenchmarkController {
|
||||||
percentile: submitResult.percentile,
|
percentile: submitResult.percentile,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Pass through the status code from the service if available, otherwise default to 400
|
const detail = error instanceof Error ? error.message : String(error)
|
||||||
|
logger.error(`[BenchmarkController] Submit failed: ${detail}`)
|
||||||
const statusCode = (error as any).statusCode || 400
|
const statusCode = (error as any).statusCode || 400
|
||||||
return response.status(statusCode).send({
|
return response.status(statusCode).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: 'Failed to submit benchmark results',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { createSessionSchema, updateSessionSchema, addMessageSchema } from '#val
|
||||||
import KVStore from '#models/kv_store'
|
import KVStore from '#models/kv_store'
|
||||||
import { SystemService } from '#services/system_service'
|
import { SystemService } from '#services/system_service'
|
||||||
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
||||||
|
import { apiSuccess, apiError } from '../helpers/api_response.js'
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class ChatsController {
|
export default class ChatsController {
|
||||||
|
|
@ -13,7 +14,7 @@ export default class ChatsController {
|
||||||
async inertia({ inertia, response }: HttpContext) {
|
async inertia({ inertia, response }: HttpContext) {
|
||||||
const aiAssistantInstalled = await this.systemService.checkServiceInstalled(SERVICE_NAMES.OLLAMA)
|
const aiAssistantInstalled = await this.systemService.checkServiceInstalled(SERVICE_NAMES.OLLAMA)
|
||||||
if (!aiAssistantInstalled) {
|
if (!aiAssistantInstalled) {
|
||||||
return response.status(404).json({ error: 'AI Assistant service not installed' })
|
return response.status(404).json({ success: false, error: 'AI Assistant service not installed' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
|
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
|
||||||
|
|
@ -29,11 +30,11 @@ export default class ChatsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async show({ params, response }: HttpContext) {
|
async show({ params, response }: HttpContext) {
|
||||||
const sessionId = parseInt(params.id)
|
const sessionId = parseInt(params.id, 10)
|
||||||
const session = await this.chatService.getSession(sessionId)
|
const session = await this.chatService.getSession(sessionId)
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return response.status(404).json({ error: 'Session not found' })
|
return response.status(404).json({ success: false, error: 'Session not found' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
@ -45,58 +46,48 @@ export default class ChatsController {
|
||||||
const session = await this.chatService.createSession(data.title, data.model)
|
const session = await this.chatService.createSession(data.title, data.model)
|
||||||
return response.status(201).json(session)
|
return response.status(201).json(session)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({
|
return apiError(response, 500, 'Failed to create session', error)
|
||||||
error: error instanceof Error ? error.message : 'Failed to create session',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async suggestions({ response }: HttpContext) {
|
async suggestions({ response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const suggestions = await this.chatService.getChatSuggestions()
|
const suggestions = await this.chatService.getChatSuggestions()
|
||||||
return response.status(200).json({ suggestions })
|
return response.status(200).json({ success: true, suggestions })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({
|
return apiError(response, 500, 'Failed to get suggestions', error)
|
||||||
error: error instanceof Error ? error.message : 'Failed to get suggestions',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update({ params, request, response }: HttpContext) {
|
async update({ params, request, response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const sessionId = parseInt(params.id)
|
const sessionId = parseInt(params.id, 10)
|
||||||
const data = await request.validateUsing(updateSessionSchema)
|
const data = await request.validateUsing(updateSessionSchema)
|
||||||
const session = await this.chatService.updateSession(sessionId, data)
|
const session = await this.chatService.updateSession(sessionId, data)
|
||||||
return session
|
return session
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({
|
return apiError(response, 500, 'Failed to update session', error)
|
||||||
error: error instanceof Error ? error.message : 'Failed to update session',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy({ params, response }: HttpContext) {
|
async destroy({ params, response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const sessionId = parseInt(params.id)
|
const sessionId = parseInt(params.id, 10)
|
||||||
await this.chatService.deleteSession(sessionId)
|
await this.chatService.deleteSession(sessionId)
|
||||||
return response.status(204)
|
return response.status(204)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({
|
return apiError(response, 500, 'Failed to delete session', error)
|
||||||
error: error instanceof Error ? error.message : 'Failed to delete session',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMessage({ params, request, response }: HttpContext) {
|
async addMessage({ params, request, response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const sessionId = parseInt(params.id)
|
const sessionId = parseInt(params.id, 10)
|
||||||
const data = await request.validateUsing(addMessageSchema)
|
const data = await request.validateUsing(addMessageSchema)
|
||||||
const message = await this.chatService.addMessage(sessionId, data.role, data.content)
|
const message = await this.chatService.addMessage(sessionId, data.role, data.content)
|
||||||
return response.status(201).json(message)
|
return response.status(201).json(message)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({
|
return apiError(response, 500, 'Failed to add message', error)
|
||||||
error: error instanceof Error ? error.message : 'Failed to add message',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,9 +96,7 @@ export default class ChatsController {
|
||||||
const result = await this.chatService.deleteAllSessions()
|
const result = await this.chatService.deleteAllSessions()
|
||||||
return response.status(200).json(result)
|
return response.status(200).json(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({
|
return apiError(response, 500, 'Failed to delete all sessions', error)
|
||||||
error: error instanceof Error ? error.message : 'Failed to delete all sessions',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import app from '@adonisjs/core/services/app'
|
||||||
import { randomBytes } from 'node:crypto'
|
import { randomBytes } from 'node:crypto'
|
||||||
import { sanitizeFilename } from '../utils/fs.js'
|
import { sanitizeFilename } from '../utils/fs.js'
|
||||||
import { deleteFileSchema, getJobStatusSchema } from '#validators/rag'
|
import { deleteFileSchema, getJobStatusSchema } from '#validators/rag'
|
||||||
|
import { apiError } from '../helpers/api_response.js'
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class RagController {
|
export default class RagController {
|
||||||
|
|
@ -14,7 +15,7 @@ export default class RagController {
|
||||||
public async upload({ request, response }: HttpContext) {
|
public async upload({ request, response }: HttpContext) {
|
||||||
const uploadedFile = request.file('file', { size: '50mb' })
|
const uploadedFile = request.file('file', { size: '50mb' })
|
||||||
if (!uploadedFile) {
|
if (!uploadedFile) {
|
||||||
return response.status(400).json({ error: 'No file uploaded' })
|
return response.status(400).json({ success: false, error: 'No file uploaded' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uploadedFile.isValid) {
|
if (!uploadedFile.isValid) {
|
||||||
|
|
@ -38,6 +39,7 @@ export default class RagController {
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.status(202).json({
|
return response.status(202).json({
|
||||||
|
success: true,
|
||||||
message: result.message,
|
message: result.message,
|
||||||
jobId: result.jobId,
|
jobId: result.jobId,
|
||||||
fileName,
|
fileName,
|
||||||
|
|
@ -58,7 +60,7 @@ export default class RagController {
|
||||||
const status = await EmbedFileJob.getStatus(fullPath)
|
const status = await EmbedFileJob.getStatus(fullPath)
|
||||||
|
|
||||||
if (!status.exists) {
|
if (!status.exists) {
|
||||||
return response.status(404).json({ error: 'Job not found for this file' })
|
return response.status(404).json({ success: false, error: 'Job not found for this file' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.status(200).json(status)
|
return response.status(200).json(status)
|
||||||
|
|
@ -66,16 +68,16 @@ export default class RagController {
|
||||||
|
|
||||||
public async getStoredFiles({ response }: HttpContext) {
|
public async getStoredFiles({ response }: HttpContext) {
|
||||||
const files = await this.ragService.getStoredFiles()
|
const files = await this.ragService.getStoredFiles()
|
||||||
return response.status(200).json({ files })
|
return response.status(200).json({ success: true, files })
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteFile({ request, response }: HttpContext) {
|
public async deleteFile({ request, response }: HttpContext) {
|
||||||
const { source } = await request.validateUsing(deleteFileSchema)
|
const { source } = await request.validateUsing(deleteFileSchema)
|
||||||
const result = await this.ragService.deleteFileBySource(source)
|
const result = await this.ragService.deleteFileBySource(source)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return response.status(500).json({ error: result.message })
|
return response.status(500).json({ success: false, error: result.message })
|
||||||
}
|
}
|
||||||
return response.status(200).json({ message: result.message })
|
return response.status(200).json({ success: true, message: result.message })
|
||||||
}
|
}
|
||||||
|
|
||||||
public async scanAndSync({ response }: HttpContext) {
|
public async scanAndSync({ response }: HttpContext) {
|
||||||
|
|
@ -83,7 +85,7 @@ export default class RagController {
|
||||||
const syncResult = await this.ragService.scanAndSyncStorage()
|
const syncResult = await this.ragService.scanAndSyncStorage()
|
||||||
return response.status(200).json(syncResult)
|
return response.status(200).json(syncResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(500).json({ error: 'Error scanning and syncing storage', details: error.message })
|
return apiError(response, 500, 'Error scanning and syncing storage', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { CheckServiceUpdatesJob } from '#jobs/check_service_updates_job'
|
||||||
import { affectServiceValidator, checkLatestVersionValidator, installServiceValidator, subscribeToReleaseNotesValidator, updateServiceValidator } from '#validators/system';
|
import { affectServiceValidator, checkLatestVersionValidator, installServiceValidator, subscribeToReleaseNotesValidator, updateServiceValidator } from '#validators/system';
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import { apiError } from '../helpers/api_response.js'
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class SystemController {
|
export default class SystemController {
|
||||||
|
|
@ -35,7 +36,7 @@ export default class SystemController {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
response.send({ success: true, message: result.message });
|
response.send({ success: true, message: result.message });
|
||||||
} else {
|
} else {
|
||||||
response.status(400).send({ error: result.message });
|
response.status(400).send({ success: false, error: result.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ export default class SystemController {
|
||||||
const payload = await request.validateUsing(affectServiceValidator);
|
const payload = await request.validateUsing(affectServiceValidator);
|
||||||
const result = await this.dockerService.affectContainer(payload.service_name, payload.action);
|
const result = await this.dockerService.affectContainer(payload.service_name, payload.action);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
response.internalServerError({ error: 'Failed to affect service' });
|
response.internalServerError({ success: false, error: 'Failed to affect service' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
response.send({ success: result.success, message: result.message });
|
response.send({ success: result.success, message: result.message });
|
||||||
|
|
@ -58,7 +59,7 @@ export default class SystemController {
|
||||||
const payload = await request.validateUsing(installServiceValidator);
|
const payload = await request.validateUsing(installServiceValidator);
|
||||||
const result = await this.dockerService.forceReinstall(payload.service_name);
|
const result = await this.dockerService.forceReinstall(payload.service_name);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
response.internalServerError({ error: 'Failed to force reinstall service' });
|
response.internalServerError({ success: false, error: 'Failed to force reinstall service' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
response.send({ success: result.success, message: result.message });
|
response.send({ success: result.success, message: result.message });
|
||||||
|
|
@ -94,6 +95,7 @@ export default class SystemController {
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
response.status(500).send({
|
response.status(500).send({
|
||||||
|
success: false,
|
||||||
error: 'Failed to retrieve update status',
|
error: 'Failed to retrieve update status',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -132,7 +134,7 @@ export default class SystemController {
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
if (!service) {
|
if (!service) {
|
||||||
return response.status(404).send({ error: `Service ${serviceName} not found or not installed` })
|
return response.status(404).send({ success: false, error: `Service ${serviceName} not found or not installed` })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -142,9 +144,9 @@ export default class SystemController {
|
||||||
hostArch,
|
hostArch,
|
||||||
service.source_repo
|
service.source_repo
|
||||||
)
|
)
|
||||||
response.send({ versions: updates })
|
response.send({ success: true, versions: updates })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
response.status(500).send({ error: `Failed to fetch versions: ${error.message}` })
|
return apiError(response, 500, 'Failed to fetch available versions', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +160,7 @@ export default class SystemController {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
response.send({ success: true, message: result.message })
|
response.send({ success: true, message: result.message })
|
||||||
} else {
|
} else {
|
||||||
response.status(400).send({ error: result.message })
|
response.status(400).send({ success: false, error: result.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
31
admin/app/helpers/api_response.ts
Normal file
31
admin/app/helpers/api_response.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import logger from '@adonisjs/core/services/logger'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardized API response helpers.
|
||||||
|
*
|
||||||
|
* Success responses follow the shape `{ success: true, data?: ..., message?: ... }`.
|
||||||
|
* Error responses follow the shape `{ success: false, error: '...' }`.
|
||||||
|
*
|
||||||
|
* Internal error details are logged server-side and never leaked to the client.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function apiSuccess(data?: Record<string, unknown> | unknown[] | string) {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return { success: true, message: data }
|
||||||
|
}
|
||||||
|
return { success: true, ...(data && typeof data === 'object' ? (Array.isArray(data) ? { data } : data) : {}) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiError(
|
||||||
|
response: HttpContext['response'],
|
||||||
|
status: number,
|
||||||
|
clientMessage: string,
|
||||||
|
internalError?: unknown
|
||||||
|
) {
|
||||||
|
if (internalError) {
|
||||||
|
const detail = internalError instanceof Error ? internalError.message : String(internalError)
|
||||||
|
logger.error(`[API] ${clientMessage}: ${detail}`)
|
||||||
|
}
|
||||||
|
return response.status(status).send({ success: false, error: clientMessage })
|
||||||
|
}
|
||||||
|
|
@ -623,7 +623,7 @@ export class BenchmarkService {
|
||||||
return {
|
return {
|
||||||
events_per_second: eventsMatch ? parseFloat(eventsMatch[1]) : 0,
|
events_per_second: eventsMatch ? parseFloat(eventsMatch[1]) : 0,
|
||||||
total_time: totalTimeMatch ? parseFloat(totalTimeMatch[1]) : 30,
|
total_time: totalTimeMatch ? parseFloat(totalTimeMatch[1]) : 30,
|
||||||
total_events: totalEventsMatch ? parseInt(totalEventsMatch[1]) : 0,
|
total_events: totalEventsMatch ? parseInt(totalEventsMatch[1], 10) : 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,16 +194,19 @@ export class DockerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if installation is already in progress (database-level)
|
// Atomic in-memory guard: check and set in a single operation to prevent
|
||||||
if (service.installation_status === 'installing') {
|
// race conditions where two requests pass the check before either sets the flag.
|
||||||
|
if (this.activeInstallations.has(serviceName)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Service ${serviceName} installation is already in progress`,
|
message: `Service ${serviceName} installation is already in progress`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.activeInstallations.add(serviceName)
|
||||||
|
|
||||||
// Double-check with in-memory tracking (race condition protection)
|
// Now check database-level status (safe because the in-memory flag blocks concurrent callers)
|
||||||
if (this.activeInstallations.has(serviceName)) {
|
if (service.installation_status === 'installing') {
|
||||||
|
this.activeInstallations.delete(serviceName)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Service ${serviceName} installation is already in progress`,
|
message: `Service ${serviceName} installation is already in progress`,
|
||||||
|
|
@ -211,7 +214,6 @@ export class DockerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark installation as in progress
|
// Mark installation as in progress
|
||||||
this.activeInstallations.add(serviceName)
|
|
||||||
service.installation_status = 'installing'
|
service.installation_status = 'installing'
|
||||||
await service.save()
|
await service.save()
|
||||||
|
|
||||||
|
|
@ -513,7 +515,7 @@ export class DockerService {
|
||||||
...(containerConfig?.WorkingDir && { WorkingDir: containerConfig.WorkingDir }),
|
...(containerConfig?.WorkingDir && { WorkingDir: containerConfig.WorkingDir }),
|
||||||
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
|
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
|
||||||
...(containerConfig?.Env && { Env: containerConfig.Env }),
|
...(containerConfig?.Env && { Env: containerConfig.Env }),
|
||||||
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
|
...(service.container_command ? { Cmd: service.container_command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((arg: string) => arg.replace(/^["']|["']$/g, '')) ?? [] } : {}),
|
||||||
// Ensure container is attached to the Nomad docker network in production
|
// Ensure container is attached to the Nomad docker network in production
|
||||||
...(process.env.NODE_ENV === 'production' && {
|
...(process.env.NODE_ENV === 'production' && {
|
||||||
NetworkingConfig: {
|
NetworkingConfig: {
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ export class ZIMExtractionService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Start new section
|
// Start new section
|
||||||
const level = parseInt(tagName.substring(1)); // Extract number from h2, h3, h4
|
const level = parseInt(tagName.substring(1), 10); // Extract number from h2, h3, h4
|
||||||
currentSection = {
|
currentSection = {
|
||||||
heading: $el.text().replace(/\[edit\]/gi, '').trim(),
|
heading: $el.text().replace(/\[edit\]/gi, '').trim(),
|
||||||
content: [],
|
content: [],
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export async function doResumableDownload({
|
||||||
})
|
})
|
||||||
|
|
||||||
const contentType = headResponse.headers['content-type'] || ''
|
const contentType = headResponse.headers['content-type'] || ''
|
||||||
const totalBytes = parseInt(headResponse.headers['content-length'] || '0')
|
const totalBytes = parseInt(headResponse.headers['content-length'] || '0', 10)
|
||||||
const supportsRangeRequests = headResponse.headers['accept-ranges'] === 'bytes'
|
const supportsRangeRequests = headResponse.headers['accept-ranges'] === 'bytes'
|
||||||
|
|
||||||
// If allowedMimeTypes is provided, check content type
|
// If allowedMimeTypes is provided, check content type
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user