nocin.eu

Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[nodejs] Parsing multipart/mixed response (containing a file stream)

Recently I had to consume an API which returned multipart/mixed data. A response looked like this:

--Boundary_0000000000001
Content-Type: application/octet-stream
Content-Disposition: attachment; filename"test.pdf"

%PDF-1.7
%�������
1 0 obj
...
%%EOF

--Boundary_0000000000001
Content-Type: application/json

{"data":[]}
--Boundary_0000000000001--

There are some node packages for parsing multipart responses, but most can only handle multipart/formData and not multipart/mixed. The most recommended package for multipart/mixed is Dicer, but to be honest, I wasn’t sure how to use it properly. Therefore, I built my own parser. Luckily the user idbehold provided a function to parse a response string into a json object here. To get it working, I just had to change the regex expressions in the split function. The most important step is to convert the data from the arrayBuffer to a String in binary encoding before parsing.

Also, I wrote two helper functions. The first one to parse the boundary string from the Content-Type and the second one to parse the filename from the Content-Dispositon Header of your response.

module.exports = new class multipartMixedParser {

    parse(boundary, buffer) {
        const body = buffer.toString('binary') //toString encodes to utf-8 as default, this would lead to corrupted pdf's     
        return body.split(boundary).reduce((parts, part) => {
            if (part && part !== '--\r\n') {
                const [head, body] = part.trim().split(/\r\n\r\n/g)
                console.log({ body })
                parts.push({
                    body: body,
                    headers: head.split(/\r\n/g).reduce((headers, header) => {
                        const [key, value] = header.split(/:\s+/)
                        headers[key.toLowerCase()] = value
                        return headers
                    }, {})
                })
            }
            return parts
        }, [])
    }

    getBoundaryFromResponseHeaders(headers) {
        //example: multipart/mixed;boundary=Boundary_0000000000001 -> --Boundary_0000000000001
        const contentType = headers.get('content-type')
        return '--' + contentType.split("=")[1].split(";")[0]
    }

    getFileNameFromContentDisposition(cd) {
        //example: 'attachment; filename="example.pdf"' -> example.pdf
        return cd.slice(
            cd.indexOf('"') + 1,
            cd.lastIndexOf('"')
        )
    }

}

And that’s how I’m calling the API and using the multipartMixedParser Class to parse the response. The API I was using is expecting a file as formData and is also returning a file (as part of the multipart/mixed response).
It’s important to get the buffer from the response. If you would use response.getText() it would convert the data to an utf-8 encoded string which will lead to corrupted files.

Please note, I’m using node-fetch. When using Axios, the response object will look different.

const btoa = require('btoa')
const FormData = require('form-data')
const fetch = require('node-fetch')
const multipartMixedParser = require('./multipartMixedParser') 

function callAPI(file) {

        const form = new FormData()
        form.append('file', file.content, {
            contentType: file.mediaType,
            filename: file.fileName
        })

        const headers = {
            'Authorization': 'Basic ' + btoa(username + ':' + password),
            ...form.getHeaders()
        }

        const url = /my/api/path

        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: headers,
                body: form
            })
            if (!response.ok) throw new Error(response.statusText)

            //parse the response
            const buffer = await response.buffer() 
            const boundary = multipartMixedParser.getBoundaryFromResponseHeaders(response.headers)

            const result = multipartMixedParser.parse(boundary, buffer)

            // in my case I only returned the file content as buffer and filename 
            return {
                fileContent: Buffer.from(result[0].body, 'binary'),
                fileName: multipartMixedParser.getFileNameFromContentDisposition(result[0].headers["content-disposition"])
            }
        } catch (err) {
            console.log("Error message: " + err.message)
        }

}

[SAPUI5] Get i18n texts

To simply get access to i18n texts, I useally add this helper function to my BaseController.js

// helper for direct access to the ResourceBundle getText() function
getText : function (sTextKey, aParamter) {
    return this.getOwnerComponent().getModel("i18n").getResourceBundle().getText(sTextKey, aParamter)
}

Texts can then be read in every controller with

// i18n: objects=Amount of objects: {0}
this.getText("objects", [iLength])

[Home Assistant] Open window reminder

This automation is triggered when a window stays open for >10 minutes. It will then send a reminder every 10 minutes (max 6 times).

This post helped me to create this script.

alias: Open window reminder
description: ''
trigger:
  - platform: state
    entity_id: binary_sensor.lumi_lumi_sensor_magnet_aq2_xxx_on_off
    from: 'off'
    to: 'on'
    for:
      hours: 0
      minutes: 10
      seconds: 0
condition: []
action:
  - repeat:
      while:
        - condition: state
          entity_id: binary_sensor.lumi_lumi_sensor_magnet_aq2_xxx_on_off
          state: 'on'
        - condition: template
          value_template: '{{ repeat.index <= 6 }}'
      sequence:
        - variables:
            counter: '{{ repeat.index * 10 }}'
        - service: telegram_bot.send_message
          data:
            message: Window is open for {{ counter }} minutes
        - delay: '00:10:00'
mode: single

[Home Assistant] Control lights with multiple motion sensors

Create group of motion sensors in groups.yaml
https://www.home-assistant.io/integrations/group/

cellar_motion:
  name: Cellar Presence
  icon: mdi:motion-sensor
  entities:
    - binary_sensor.bewegungsmelder_xxx_ias_zone
    - binary_sensor.bewegungsmelder_xxx_ias_zone

Next, if you have more than one light you’d like to control, create a group of lights in your configuration.yaml
https://www.home-assistant.io/integrations/light.group/

# Light Groups      
light:
  - platform: group
    name: Cellar Lights
    entities:
      - light.ikea_of_sweden_tradfri_bulb_e27_ww_806lm_xxx_level_on_off
      - light.ikea_of_sweden_tradfri_bulb_e27_ww_806lm_xxx_level_on_off   

And finally use both in an automation

alias: My motion activated lights
description: Turn on a light when motion is detected.
trigger:
  - platform: state
    entity_id: group.cellar_motion
    from: 'off'
    to: 'on'
condition: []
action:
  - service: light.turn_on
    target:
      entity_id: light.cellar_lights
    data: {}
  - wait_for_trigger:
      platform: state
      entity_id: group.cellar_motion
      from: 'on'
      to: 'off'
  - delay: 120
  - service: light.turn_off
    target:
      entity_id: light.cellar_lights
    data: {}
mode: restart
max_exceeded: silent # https://www.home-assistant.io/docs/automation/modes/