feat(scan): support iX1600 duplex scanning on Linux
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { FastifyInstance, FastifyRequest } from "fastify"
|
||||
import multipart from "@fastify/multipart"
|
||||
import { createHash } from "node:crypto"
|
||||
import { and, asc, eq, sql } from "drizzle-orm"
|
||||
import { basename } from "node:path"
|
||||
import { and, asc, eq, inArray, sql } from "drizzle-orm"
|
||||
import { instanceAgentScanJobs, instanceAgents } from "../../db/schema"
|
||||
import { saveFile } from "../utils/files"
|
||||
|
||||
@@ -178,11 +179,12 @@ export default async function instanceAgentGatewayRoutes(server: FastifyInstance
|
||||
})
|
||||
.where(and(
|
||||
eq(instanceAgentScanJobs.id, req.params.id),
|
||||
eq(instanceAgentScanJobs.agentId, agent.id)
|
||||
eq(instanceAgentScanJobs.agentId, agent.id),
|
||||
inArray(instanceAgentScanJobs.status, ["pending", "running"])
|
||||
))
|
||||
.returning()
|
||||
|
||||
if (!job) return reply.code(404).send({ error: "Scan job not found" })
|
||||
if (!job) return reply.code(409).send({ error: "Scan job is no longer active or does not exist" })
|
||||
|
||||
return { job }
|
||||
})
|
||||
@@ -209,7 +211,8 @@ export default async function instanceAgentGatewayRoutes(server: FastifyInstance
|
||||
if (!data?.file) return reply.code(400).send({ error: "No file uploaded" })
|
||||
|
||||
const fileBuffer = await data.toBuffer()
|
||||
const filename = job.requestedFilename || data.filename || `${job.id}.pdf`
|
||||
// The agent normalizes the extension to the actual output format.
|
||||
const filename = basename(data.filename || job.requestedFilename || `${job.id}.pdf`)
|
||||
|
||||
const createdFile = await saveFile(
|
||||
server,
|
||||
|
||||
89
backend/tests/instanceAgentGateway.test.ts
Normal file
89
backend/tests/instanceAgentGateway.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { createHash } from "node:crypto"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { createRequire } from "node:module"
|
||||
import path from "node:path"
|
||||
import vm from "node:vm"
|
||||
import ts from "typescript"
|
||||
import Fastify from "fastify"
|
||||
import { getTableColumns, getTableName } from "drizzle-orm"
|
||||
import { PgDialect } from "drizzle-orm/pg-core"
|
||||
|
||||
// Load the real route with only file storage replaced; tests never contact S3 or a live database.
|
||||
const routePath = path.resolve(__dirname, "../src/routes/instanceAgentGateway.ts")
|
||||
const routeRequire = createRequire(routePath)
|
||||
const compiled = ts.transpileModule(readFileSync(routePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true },
|
||||
}).outputText
|
||||
|
||||
const fixture = async (status = "running") => {
|
||||
const token = "test-agent-token"
|
||||
const agent = { id: "agent", tokenHash: createHash("sha256").update(token).digest("hex"), active: true }
|
||||
const job: any = { id: "job", agentId: "agent", tenantId: 7, requestedBy: "user", requestedFilename: "scan.png", target: { folder: "folder" }, status }
|
||||
const saved: any[] = []
|
||||
const rows: Record<string, any[]> = { instance_agents: [agent], instance_agent_scan_jobs: [job] }
|
||||
const dialect = new PgDialect()
|
||||
const matching = (table: any, condition: any) => {
|
||||
const query = dialect.sqlToQuery(condition)
|
||||
const columns = getTableColumns(table) as Record<string, any>
|
||||
const clauses = [...query.sql.matchAll(/"\w+"\."(\w+)" (?:= \$(\d+)|in \(([^)]+)\))/g)]
|
||||
assert.ok(clauses.length, query.sql)
|
||||
return rows[getTableName(table)].filter(row => clauses.every(match => {
|
||||
const key = Object.keys(columns).find(key => columns[key].name === match[1])!
|
||||
if (match[2]) return row[key] === query.params[Number(match[2]) - 1]
|
||||
return [...match[3].matchAll(/\$(\d+)/g)].some(parameter => row[key] === query.params[Number(parameter[1]) - 1])
|
||||
}))
|
||||
}
|
||||
const db = {
|
||||
select: () => ({ from: (table: any) => ({ where: (where: any) => ({ limit: async () => matching(table, where) }) }) }),
|
||||
update: (table: any) => ({ set: (data: any) => ({ where: (where: any) => ({ returning: async () => matching(table, where).map(row => Object.assign(row, data)) }) }) }),
|
||||
}
|
||||
const exports: any = {}
|
||||
vm.runInNewContext(compiled, {
|
||||
exports,
|
||||
require: (name: string) => name === "../utils/files"
|
||||
? { saveFile: async (...args: any[]) => { saved.push(args); return { id: "saved-file" } } }
|
||||
: routeRequire(name),
|
||||
})
|
||||
const app = Fastify()
|
||||
app.decorate("db", db)
|
||||
await app.register(exports.default)
|
||||
return { app, job, saved, headers: { "x-agent-token": token } }
|
||||
}
|
||||
|
||||
test("late failure status cannot overwrite an already completed scan", async (t) => {
|
||||
const f = await fixture("completed")
|
||||
t.after(() => f.app.close())
|
||||
const response = await f.app.inject({ method: "POST", url: "/scan-jobs/job/status", headers: f.headers, payload: { status: "failed", message: "upload timeout" } })
|
||||
assert.equal(response.statusCode, 409)
|
||||
assert.equal(f.job.status, "completed")
|
||||
})
|
||||
|
||||
test("running jobs accept progress and scanner failures", async (t) => {
|
||||
const f = await fixture()
|
||||
t.after(() => f.app.close())
|
||||
for (const status of ["running", "failed"]) {
|
||||
const response = await f.app.inject({ method: "POST", url: "/scan-jobs/job/status", headers: f.headers, payload: { status, message: "4 Seiten" } })
|
||||
assert.equal(response.statusCode, 200)
|
||||
assert.equal(f.job.status, status)
|
||||
}
|
||||
})
|
||||
|
||||
test("upload preserves the actual PDF filename, tenant and target folder", async (t) => {
|
||||
const f = await fixture()
|
||||
t.after(() => f.app.close())
|
||||
const boundary = "fedeo-test-boundary"
|
||||
const response = await f.app.inject({
|
||||
method: "POST", url: "/scan-jobs/job/upload",
|
||||
headers: { ...f.headers, "content-type": `multipart/form-data; boundary=${boundary}` },
|
||||
payload: `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="scan.pdf"\r\nContent-Type: application/pdf\r\n\r\n%PDF-test\r\n--${boundary}--\r\n`,
|
||||
})
|
||||
assert.equal(response.statusCode, 200, response.body)
|
||||
assert.equal(f.saved[0][1], 7)
|
||||
assert.equal(f.saved[0][3].filename, "scan.pdf")
|
||||
assert.equal(f.saved[0][3].contentType, "application/pdf")
|
||||
assert.equal(f.saved[0][4], "folder")
|
||||
assert.equal(f.job.status, "completed")
|
||||
assert.equal(f.job.fileId, "saved-file")
|
||||
})
|
||||
Reference in New Issue
Block a user