32 · File Upload & Media Types

Use @Core.MediaType for binary attachments. Integrate with SAP Object Store for scalable file storage.

Episode 32 — File Upload & Media Types

What you'll learn

Why this matters

Every business app needs file attachments — product images, invoice PDFs, contract documents. CAP's OData media entity pattern gives you a standardised upload/download API that Fiori Attachments works with out of the box.

CDS media entity declaration

// db/schema.cds
entity Attachments {
  key ID       : UUID;
  order_ID     : UUID;
  filename     : String;
  mediaType    : String  @Core.IsMediaType;       // stores MIME type
  content      : LargeBinary @Core.MediaType: mediaType;  // the binary blob
  size         : Integer;
  createdAt    : Timestamp @cds.on.insert: $now;
}
// srv/orders-service.cds
service OrdersService {
  entity Attachments as projection on my.Attachments;
}

Upload handler

// srv/orders-service.js
const fs = require('fs')
const path = require('path')

this.on('PUT', 'Attachments', async (req) => {
  const { ID } = req.params[0]         // key from URL: PUT /Attachments(uuid)/$value
  const mediaType = req.headers['content-type']
  const filename  = req.headers['slug'] || 'upload'

  // For small files: buffer and store in DB
  const chunks = []
  for await (const chunk of req.data) chunks.push(chunk)
  const content = Buffer.concat(chunks)

  await UPSERT.into('Attachments').entries({
    ID,
    order_ID:  req.headers['x-order-id'],
    filename,
    mediaType,
    content,
    size: content.length
  })

  return { ID, filename, size: content.length }
})

Download handler

this.on('READ', 'Attachments', async (req) => {
  // When URL is /Attachments(uuid)/$value — streaming the binary
  if (req.params?.[0] && req._.req?.url?.endsWith('/$value')) {
    const { ID } = req.params[0]
    const row = await SELECT.one.from('Attachments').where({ ID })
    if (!row) return req.reject(404, 'Attachment not found')

    req._.res.setHeader('Content-Type',   row.mediaType)
    req._.res.setHeader('Content-Length', row.size)
    req._.res.setHeader('Content-Disposition', `inline; filename="${row.filename}"`)
    req._.res.end(row.content)
    return   // prevent CAP from sending its own response
  }

  // Normal metadata read
  return SELECT.from('Attachments').columns('ID','filename','mediaType','size','createdAt')
})

SAP Object Store integration (large files)

// For large files, stream to SAP Object Store instead of DB
const objectStore = await cds.connect.to('object-store')

this.on('PUT', 'Attachments', async (req) => {
  const { ID } = req.params[0]
  const key = `attachments/${ID}`

  // Stream directly to Object Store
  await objectStore.upload({ key, body: req.data, contentType: req.headers['content-type'] })

  await UPSERT.into('Attachments').entries({
    ID,
    storageKey: key,
    mediaType:  req.headers['content-type'],
    filename:   req.headers['slug']
  })
})

Fiori V4 attachment upload

// UI5: upload via OData V4 model
const oListBinding = oModel.bindList('/Attachments')
const oContext = oListBinding.create({ ID: uuid() })

const oBinding = oModel.bindProperty(`${oContext.getPath()}/$value`)
await oBinding.setValue(file)   // file is a File object from <input type="file">

HTTP REST upload

### Create metadata first
POST http://localhost:4004/orders/Attachments
Content-Type: application/json

{ "ID": "att-001", "order_ID": "ord-001", "filename": "invoice.pdf" }

### Then upload content
PUT http://localhost:4004/orders/Attachments('att-001')/$value
Content-Type: application/pdf
Slug: invoice.pdf

< ./invoice.pdf

Common mistakes

Mistake Symptom Fix
@Core.MediaType on wrong field Download returns wrong MIME type Annotate the mediaType string field
Buffering large files in memory OOM error Use streaming or Object Store
Missing $value segment in URL Gets metadata, not file Append /$value to download binary
Returning from download handler Double response error Return undefined after res.end()

Hands-on exercise

  1. Add an Attachments entity to your bookshop schema
  2. Implement PUT (upload) and GET (download) handlers
  3. Test upload with curl -X PUT .../Attachments('test')/$value --data-binary @file.pdf
  4. Verify Content-Type and Content-Disposition headers in the response

Checkpoint ✓

Sign in for progress tracking & Q&A →