Newer
Older
client_dwatch / app / api / ds-agent / commands / [command_id] / chunks / [seq] / route.ts
/**
 * POST /ds-agent/commands/{command_id}/chunks/{seq} — zip のチャンク(指示書AY 段階7-3。D1・D20・D25 ①・D-179)
 *
 * 仕様の正は `d-watch-api.yaml` の同パス。★URL の command_id は export の指示の id★。
 * ★1チャンク 4MB 以下★(proxy が本体を10MBまでしかバッファせず、超えた分を黙って切り捨てるため)。長さとハッシュを必ず検証する。
 * ★204 はブラウザ側に空きができてから(流量制御。最大45秒)★。チャンクはメモリで中継するだけで、サーバーには保存しない。
 */
import { NextResponse } from "next/server";
import { agentCommandPreamble, commandNotFound, jsonError, noContent } from "@/lib/ds-agent/agent-request";
import { getRelay } from "@/lib/ds-agent/relay";
import { ensureDsAgentLogging } from "@/lib/ds-agent/relay-log";
import { parsePathSeq } from "@/lib/ds-logs/validate";
import { withRequestId } from "@/lib/logger";

const SHA256_HEX = /^[0-9a-f]{64}$/;

export async function POST(
  request: Request,
  { params }: { params: Promise<{ command_id: string; seq: string }> }
) {
  return withRequestId(request, async () => {
    ensureDsAgentLogging();
    const { command_id, seq: seqRaw } = await params;
    const pre = await agentCommandPreamble(request, command_id);
    if (!pre.ok) return pre.response;

    const seq = parsePathSeq(seqRaw);
    if (!seq.ok) return NextResponse.json(seq.error, { status: seq.status });

    const sha256 = request.headers.get("x-chunk-sha256");
    if (sha256 === null || !SHA256_HEX.test(sha256)) {
      return jsonError(400, "chunk_invalid", "X-Chunk-Sha256 の形式が不正です");
    }

    const bytes = new Uint8Array(await request.arrayBuffer());
    switch (await getRelay().pushChunk(pre.serial, pre.commandId, seq.query, bytes, sha256)) {
      case "ok":
        return noContent();
      case "invalid":
        return jsonError(400, "chunk_invalid", "チャンクの長さかハッシュが不正です");
      case "out_of_order":
        return jsonError(409, "chunk_out_of_order", "チャンクの順番が正しくありません");
      case "cancelled":
      case "gone":
        return jsonError(410, "export_cancelled", "エクスポートは取り消されました");
      case "not_found":
        return commandNotFound();
    }
  });
}