Upload: APIv3 Attachment.create with base64 content (not multipart)
The previous multipart attempt (f74fd07) was rejected by Civi with
"Mandatory key(s) missing: id or content or options.move-file" — the
/civicrm/ajax/rest endpoint on this install doesn't expose $_FILES to
v3 actions; only params['content'] is consulted. Our multipart `file`
part was ignored.
APIv3 Attachment.create has historically been the file-upload entry
point used by Civi's own form widgets and auto-decodes the `content`
field from base64, unlike v4 File.create which stores it verbatim.
Send the same shape we tried first (entity_table, entity_id, name,
mime_type, content=base64) but to v3 Attachment instead of v4 File.
The civi3Upload helper in lib/civicrm.ts is kept in place — it's not
useful for this endpoint but the multipart-POST shape may be needed
later for other Civi entities that do read $_FILES.
This commit is contained in:
+29
-33
@@ -25,7 +25,7 @@
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { civi3Upload, verifyChecksum } from "@/lib/civicrm";
|
||||
import { civi3, verifyChecksum } from "@/lib/civicrm";
|
||||
import { allFields } from "@/config/form";
|
||||
import { rateLimit, clientIp } from "@/lib/rate-limit";
|
||||
|
||||
@@ -205,43 +205,39 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// APIv3 Attachment.create with a multipart `file` part. The earlier
|
||||
// approach used APIv4 File.create with `content: base64String` and that
|
||||
// Civi on this install stores the literal base64 *text* on disk — every
|
||||
// file came back corrupted (hex 6956... = "iVBO..." = base64 PNG header).
|
||||
// APIv3 Attachment.create reads $_FILES['file'] from the multipart body
|
||||
// and writes the bytes through unchanged.
|
||||
// APIv3 Attachment.create with `content` as base64. Path history:
|
||||
//
|
||||
// Attachment.create requires (entity_table, entity_id). Our custom-field
|
||||
// flow stores the file id directly in the custom column (no
|
||||
// civicrm_entity_file linkage needed for prefill/download), but the API
|
||||
// still mandates an entity context for the upload call itself, so we
|
||||
// anchor to the form-filler's contact id. The resulting civicrm_entity_file
|
||||
// row is metadata-only — Civi's custom-field joins ignore it.
|
||||
// 1. APIv4 File.create + content:base64 → stored base64 *text* on disk
|
||||
// (Civi v4 doesn't decode). Every file came back corrupt.
|
||||
// 2. APIv3 Attachment.create + multipart file part → Civi rejected with
|
||||
// "Mandatory key(s) missing: id or content or options.move-file".
|
||||
// The /civicrm/ajax/rest endpoint on this install doesn't expose
|
||||
// $_FILES to the v3 action; only params['content'] is consulted.
|
||||
// 3. APIv3 Attachment.create + content:base64 (this code) → Civi's v3
|
||||
// Attachment.create has historically been the file-upload entry
|
||||
// point used by Civi's own form widgets, and it auto-decodes the
|
||||
// content field.
|
||||
//
|
||||
// The returned id is what the frontend stores in RHF state and ultimately
|
||||
// sends as the field value on /api/submit. /api/submit then writes that
|
||||
// id to the activity custom field (stage-N) or to the org contact custom
|
||||
// field (Food_Co_op_Organizing.*).
|
||||
// Attachment.create requires (entity_table, entity_id); our custom-field
|
||||
// flow stores the file id directly in the custom column (no entity_file
|
||||
// linkage needed for prefill/download), but the API still mandates an
|
||||
// entity context, so we anchor to the form-filler's contact id. The
|
||||
// resulting civicrm_entity_file row is metadata-only — Civi's custom-
|
||||
// field joins ignore it.
|
||||
//
|
||||
// Orphan files: if the user uploads and then abandons the form, the
|
||||
// civicrm_file + civicrm_entity_file rows persist with no business
|
||||
// reference. Cleanup is handled by a CiviCRM scheduled job (configured
|
||||
// separately by the Civi admin) that prunes orphans older than ~24h.
|
||||
// Orphan files: civicrm_file + civicrm_entity_file rows persist if the
|
||||
// user uploads and then abandons the form. Cleanup is handled by a
|
||||
// CiviCRM scheduled job configured separately by the Civi admin.
|
||||
let fileId: number;
|
||||
try {
|
||||
const res = await civi3Upload<{ id: string | number }>(
|
||||
"Attachment",
|
||||
"create",
|
||||
{
|
||||
entity_table: "civicrm_contact",
|
||||
entity_id: Number(cid),
|
||||
name: safeName,
|
||||
mime_type: clientMime,
|
||||
sequential: 1,
|
||||
},
|
||||
{ bytes, filename: safeName, mime: clientMime },
|
||||
);
|
||||
const res = await civi3<{ id: string | number }>("Attachment", "create", {
|
||||
entity_table: "civicrm_contact",
|
||||
entity_id: Number(cid),
|
||||
name: safeName,
|
||||
mime_type: clientMime,
|
||||
content: Buffer.from(bytes).toString("base64"),
|
||||
sequential: 1,
|
||||
});
|
||||
const idRaw = res.values?.[0]?.id;
|
||||
const id = typeof idRaw === "string" ? Number(idRaw) : idRaw;
|
||||
if (!id || !Number.isFinite(id)) {
|
||||
|
||||
Reference in New Issue
Block a user