Maker of open-source software and hardware.

Using MTP on macOS with Node.js: Part 3

Feature image

Today I started on wrapping the libmtp C library in a Node.js module. So far, I've managed to load the list of files on the device and successfully retrieve a file.

Let's start with the bindings.gyp file:

{
  "targets": [{
      "target_name": "module",
      "sources": [ "./src/module.c" ],
      "libraries": [
          "<!@(pkg-config --libs libmtp)"
      ],
      "cflags": [
          "<!@(pkg-config --cflags libmtp)"
      ]
  }],
}

Here I'm specifying that the file containing the C code is in module.c, and that we're using the libmtp shared library. On macOS, use brew install libmtp. On Linux, use sudo apt-get install libmtp-dev. Oh, you'll also need pkg-config if it's not installed on your system yet[1].

Here is the NAPI function to run the code in the C library and return the result to JS:

napi_value getFile(napi_env env, napi_callback_info info) {
  napi_status status;

  size_t argc = 2;
  napi_value argv[2];
  status = napi_get_cb_info(env, info, &argc;, argv, NULL, NULL);
  if (status != napi_ok) {
      napi_throw_error(env, NULL, "Failed to parse arguments");
  }

  int id = 0;
  char path[20];
  napi_get_value_int32(env, argv[0], &id;);
  napi_get_value_string_utf8(env, argv[1], path, 20, NULL);

  int ret = LIBMTP_Get_File_To_File(device, id, path, NULL, NULL);

  napi_value retVal;
  status = napi_create_int32(env, ret, &retVal;);
  if (status != napi_ok) {
      napi_throw_error(env, NULL, "Unable to create return value");
  }

  return retVal;
}

For details on how we get the arguments converted from JS to C, and the return value back to JS, see this tutorial. The line to focus on here is the one with LIBMTP_Get_File_To_File, where we pass the file ID to get from the device, and a path to save that file to.

As we need to connect to the device before we read the file, I added the following code to the NAPI Init function:

napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_value fn;

  LIBMTP_Init();

  fprintf(stdout, "libmtp version: " LIBMTP_VERSION_STRING "\n\n");

  device = LIBMTP_Get_First_Device();
  if (device == NULL) {
    printf("No devices.\n");
    return 0;
  }

  status = napi_create_function(env, NULL, 0, getFile, NULL, &fn;);
  if (status != napi_ok) {
      napi_throw_error(env, NULL, "Unable to wrap native function");
  }

  status = napi_set_named_property(env, exports, "get_file_to_file", fn);
  if (status != napi_ok) {
      napi_throw_error(env, NULL, "Unable to populate exports");
  }

  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

The line with napi_set_named_property specifies what the function name is called in JS, while napi_create_function creates a JS function from the C function called getFile.

Now it's as easy as just calling the function from JS:

const binding = require('node-gyp-build')(__dirname);
    
console.log(`Status:`, binding.get_file_to_file(1693, 'test.jpg'));

We pass the file ID (1693 in this example) and the path to save file into (test.jpg in the same folder). And that's it, we can read files from an Android phone over MTP on macOS! I'll be posting all the code on GitHub soon once I've cleaned it up a bit.


  1. brew install pkg-config on macOS. ↩︎

#nodemtp