Search code examples
loopbackjsstrongloop

How to store files with meta data in LoopBack?


What I want to do: Have an html form, with a file input inside. When a file is chosen, the file input should upload the file, and get a file id, so when the form is submitted, the file id is posted with the form and written in the database.

Shorter version: I want to store meta data (id for example) with my files.

Sounds simple, yet I struggle to do that in LoopBack.

There has been a couple conversations ( 1, 2 ) about this topic, and neither seemed to lead to a solution, so I thought this might be a good place to find one once and for all.

The simplest solution would be to use model relations, but LoopBack doesn't support relations with the file storage service. Bump. So we have to go with a persistedmodel named File for example, and override default create, delete so it saves and deletes from the file store model I have - named Storage.

My setup so far:

  • I have a model /api/Storage which is connected to a loopback storage service and is saving file successfully to the local filesystem.
  • I have a PersistedModel connected to Mongo with file meta data: name,size, url and objectId
  • I have a remote hook set up beforecreate so the file can be saved first and then it's url can be injected into File.create()

I'm there, and according to this LoopBack page, I have the ctx which should have the file inside:

File.beforeRemote('create', function(ctx, affectedModelInstance, next) {})`

What's ctx?

ctx.req: Express Request object.
ctx.result: Express Response object.

Ok, so now I'm at the Express page, pretty lost, and it sais something about a 'body-parsing middleware' which I have no idea what it might be.

I feel like I'm close to the solution, any help would be appreciated. Is this approach right?


Solution

  • Here's the full solution for storing meta data with files in loopback.

    You need a container model

    common/models/container.json

    {
      "name": "container",
      "base": "Model",
      "idInjection": true,
      "options": {
        "validateUpsert": true
      },
      "properties": {},
      "validations": [],
      "relations": {},
      "acls": [],
      "methods": []
    }
    

    Create the data source for your container in server/datasources.json. For example:

    ...
    "storage": {
        "name": "storage",
        "connector": "loopback-component-storage",
        "provider": "filesystem", 
        "root": "/var/www/storage",
        "maxFileSize": "52428800"
    }
    ...
    

    You'll need to set the data source of this model in server/model-config.json to the loopback-component-storage you have:

    ...
    "container": {
        "dataSource": "storage",
        "public": true
    }
    ...
    

    You'll also need a file model to store the meta data and handle container calls:

    common/models/files.json

    {
      "name": "files",
      "base": "PersistedModel",
      "idInjection": true,
      "options": {
        "validateUpsert": true
      },
      "properties": {
        "name": {
          "type": "string"
        },
        "type": {
          "type": "string"
        },
        "url": {
          "type": "string",
          "required": true
        }
      },
      "validations": [],
      "relations": {},
      "acls": [],
      "methods": []
    }
    

    And now connect files with container:

    common/models/files.js

    var CONTAINERS_URL = '/api/containers/';
    module.exports = function(Files) {
    
        Files.upload = function (ctx,options,cb) {
            if(!options) options = {};
            ctx.req.params.container = 'common';
            Files.app.models.container.upload(ctx.req,ctx.result,options,function (err,fileObj) {
                if(err) {
                    cb(err);
                } else {
                    var fileInfo = fileObj.files.file[0];
                    Files.create({
                        name: fileInfo.name,
                        type: fileInfo.type,
                        container: fileInfo.container,
                        url: CONTAINERS_URL+fileInfo.container+'/download/'+fileInfo.name
                    },function (err,obj) {
                        if (err !== null) {
                            cb(err);
                        } else {
                            cb(null, obj);
                        }
                    });
                }
            });
        };
    
        Files.remoteMethod(
            'upload',
            {
                description: 'Uploads a file',
                accepts: [
                    { arg: 'ctx', type: 'object', http: { source:'context' } },
                    { arg: 'options', type: 'object', http:{ source: 'query'} }
                ],
                returns: {
                    arg: 'fileObject', type: 'object', root: true
                },
                http: {verb: 'post'}
            }
        );
    
    };
    

    For expose the files api add to the model-config.json file the files model, remember select your correct datasources:

    ...
    "files": {
        "dataSource": "db",
        "public": true
    }
    ...
    

    Done! You can now call POST /api/files/upload with a file binary data in file form field. You'll get back id, name, type, and url in return.