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:
+24
-28
@@ -25,7 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { civi3Upload, verifyChecksum } from "@/lib/civicrm";
|
import { civi3, verifyChecksum } from "@/lib/civicrm";
|
||||||
import { allFields } from "@/config/form";
|
import { allFields } from "@/config/form";
|
||||||
import { rateLimit, clientIp } from "@/lib/rate-limit";
|
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
|
// APIv3 Attachment.create with `content` as base64. Path history:
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Attachment.create requires (entity_table, entity_id). Our custom-field
|
// 1. APIv4 File.create + content:base64 → stored base64 *text* on disk
|
||||||
// flow stores the file id directly in the custom column (no
|
// (Civi v4 doesn't decode). Every file came back corrupt.
|
||||||
// civicrm_entity_file linkage needed for prefill/download), but the API
|
// 2. APIv3 Attachment.create + multipart file part → Civi rejected with
|
||||||
// still mandates an entity context for the upload call itself, so we
|
// "Mandatory key(s) missing: id or content or options.move-file".
|
||||||
// anchor to the form-filler's contact id. The resulting civicrm_entity_file
|
// The /civicrm/ajax/rest endpoint on this install doesn't expose
|
||||||
// row is metadata-only — Civi's custom-field joins ignore it.
|
// $_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
|
// Attachment.create requires (entity_table, entity_id); our custom-field
|
||||||
// sends as the field value on /api/submit. /api/submit then writes that
|
// flow stores the file id directly in the custom column (no entity_file
|
||||||
// id to the activity custom field (stage-N) or to the org contact custom
|
// linkage needed for prefill/download), but the API still mandates an
|
||||||
// field (Food_Co_op_Organizing.*).
|
// 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
|
// Orphan files: civicrm_file + civicrm_entity_file rows persist if the
|
||||||
// civicrm_file + civicrm_entity_file rows persist with no business
|
// user uploads and then abandons the form. Cleanup is handled by a
|
||||||
// reference. Cleanup is handled by a CiviCRM scheduled job (configured
|
// CiviCRM scheduled job configured separately by the Civi admin.
|
||||||
// separately by the Civi admin) that prunes orphans older than ~24h.
|
|
||||||
let fileId: number;
|
let fileId: number;
|
||||||
try {
|
try {
|
||||||
const res = await civi3Upload<{ id: string | number }>(
|
const res = await civi3<{ id: string | number }>("Attachment", "create", {
|
||||||
"Attachment",
|
|
||||||
"create",
|
|
||||||
{
|
|
||||||
entity_table: "civicrm_contact",
|
entity_table: "civicrm_contact",
|
||||||
entity_id: Number(cid),
|
entity_id: Number(cid),
|
||||||
name: safeName,
|
name: safeName,
|
||||||
mime_type: clientMime,
|
mime_type: clientMime,
|
||||||
|
content: Buffer.from(bytes).toString("base64"),
|
||||||
sequential: 1,
|
sequential: 1,
|
||||||
},
|
});
|
||||||
{ bytes, filename: safeName, mime: clientMime },
|
|
||||||
);
|
|
||||||
const idRaw = res.values?.[0]?.id;
|
const idRaw = res.values?.[0]?.id;
|
||||||
const id = typeof idRaw === "string" ? Number(idRaw) : idRaw;
|
const id = typeof idRaw === "string" ? Number(idRaw) : idRaw;
|
||||||
if (!id || !Number.isFinite(id)) {
|
if (!id || !Number.isFinite(id)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user