web-dev-qa-db-fra.com

Comment stocker des fichiers avec métadonnées dans LoopBack?

Ce que je veux faire: Avoir un formulaire HTML, avec une entrée de fichier à l'intérieur. Lorsqu'un fichier est choisi, l'entrée de fichier doit télécharger le fichier et obtenir un identifiant de fichier. Ainsi, lorsque le formulaire est soumis, l'identifiant de fichier est publié avec le formulaire et écrit dans la base de données. 

Version plus courte: Je veux stocker des métadonnées (id par exemple) avec mes fichiers.

Cela semble simple, mais j'ai du mal à le faire dans LoopBack. 

Il y a eu quelques conversations ( 1 , 2 ) sur ce sujet, et aucune ne semblait aboutir à une solution. J'ai donc pensé que ce serait un bon endroit pour en trouver une une fois pour toutes.

La solution la plus simple consisterait à utiliser des relations de modèle, mais LoopBack ne prend pas en charge les relations avec le service de stockage de fichiers. Bosse. Nous devons donc utiliser un modèle persistant nommé File, par exemple, et remplacer la création par défaut, la supprimer, afin qu’elle enregistre et supprime du modèle de magasin de fichiers que j’ai nommé Storage.

Ma configuration jusqu'ici:

  • J'ai un modèle/api/Storage qui est connecté à un service de stockage en boucle et enregistre le fichier avec succès sur le système de fichiers local. 
  • J'ai un PersistedModel connecté à Mongo avec des métadonnées de fichier: name, size, url et objectId
  • J'ai un hook distant configuré avantcreate afin que le fichier puisse être sauvegardé en premier, puis url peut être injecté dans File.create()

Je suis là, et selon cette page LoopBack , j'ai le ctx qui devrait avoir le fichier à l'intérieur:

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

Qu'est-ce que ctx

ctx.req: objet Express Request.
ctx.result: objet Réponse Express.

Ok, alors maintenant, je suis à la page Express, plutôt perdue, et elle en dit long sur un "middleware d'analyse corporelle" dont je n'ai aucune idée de ce que cela pourrait être.

Je sens que je suis proche de la solution, toute aide serait appréciée. Cette approche est-elle correcte? 

47
Mihaly KR

Voici la solution full pour stocker des métadonnées avec des fichiers en boucle. 

Vous avez besoin d'un modèle de conteneur

common/models/container.json

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

Créez la source de données pour votre conteneur dans server/datasources.json. Par exemple:

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

Vous devez définir la source de données de ce modèle dans server/model-config.json sur le loopback-component-storage que vous avez:

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

Vous aurez également besoin d'un modèle de fichier pour stocker les métadonnées et gérer les appels de conteneur:

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": []
}

Et maintenant, connectez files avec 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'}
        }
    );

};

Pour exposer les fichiers api ajouter au modèle model-config.json le modèle files, n'oubliez pas de sélectionner vos sources de données correctes:

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

Terminé! Vous pouvez maintenant appeler POST /api/files/upload avec un fichier de données binaires dans le champ de formulaire file. Vous obtiendrez en retour l'identifiant, le nom, le type et l'URL. 

54
Mihaly KR

J'ai eu le même problème. Je l'ai résolu en créant mes propres modèles pour stocker des métadonnées et mes propres méthodes de téléchargement.

  1. J'ai créé un modèle File qui stockera des informations telles que le nom, le type, l'URL, l'ID utilisateur (idem que le vôtre)

  2. J'ai créé ma propre méthode de téléchargement à distance car je ne pouvais pas le faire avec les points d'ancrage. Le modèle de conteneur est le modèle créé par loopback-composant-storage .

  3. var fileInfo = fileObj.files.myFile[0]; Ici myFile est le nom de champ pour le téléchargement de fichier, vous devrez donc le changer en conséquence. Si vous ne spécifiez aucun champ, alors il se présentera sous la forme fileObj.file.null[0].Ce code manque le contrôle d'erreur approprié, faites-le avant de le déployer en production.

     File.uploadFile = function (ctx,options,cb) {
      File.app.models.container.upload(ctx.req,ctx.result,options,function (err,fileObj) {
        if(err) cb(err);
        else{
                // Here myFile is the field name associated with upload. You should change it to something else if you
                var fileInfo = fileObj.files.myFile[0];
                File.create({
                  name: fileInfo.name,
                  type: fileInfo.type,
                  container: fileInfo.container,
                  userId: ctx.req.accessToken.userId,
                  url: CONTAINERS_URL+fileInfo.container+'/download/'+fileInfo.name // This is a hack for creating links
                },function (err,obj) {
                  if(err){
                    console.log('Error in uploading' + err);
                    cb(err);
                  }
                  else{
                    cb(null,obj);
                  }
                });
              }
            });
    };
    
    File.remoteMethod(
      'uploadFile',
      {
        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'}
      }
    
    );
    
10
Harshil Lodhi

Pour ceux qui recherchent une réponse à la question "Comment vérifier le format de fichier avant de télécharger un fichier".

En réalité, dans ce cas, nous pouvons utiliser le paramètre facultatif allowedContentTypes .

Dans le répertoire boot utilisez un exemple de code:

module.exports = function(server) {
    server.dataSources.filestorage.connector.allowedContentTypes = ["image/jpg", "image/jpeg", "image/png"];
}

J'espère que ça va aider quelqu'un.

8
av-k

En fonction de votre scénario, il peut être intéressant d'utiliser des signatures ou similaires permettant des téléchargements directs vers Amazon S3, TransloadIT (pour le traitement des images) ou des services similaires.

Notre première décision avec ce concept était que, comme nous utilisions GraphQL, nous voulions éviter les envois multipartes de formulaires via GraphQL qui, à leur tour, devraient être transférés à nos services Loopback. De plus, nous voulions que ces serveurs restent efficaces sans que les ressources ne soient affectées par des téléchargements (volumineux) et la validation et le traitement des fichiers associés.

Votre flux de travail pourrait ressembler à ceci:

  1. Créer un enregistrement de base de données
  2. Renvoie l'ID d'enregistrement et les données de signature de téléchargement de fichier (inclut le seau S3 ou le point de terminaison TransloadIT, ainsi que tous les jetons d'authentification)
  3. Le client télécharge sur le noeud final 

Dans les cas où vous effectuez des tâches telles que le téléchargement de bannières ou d'avatars, l'étape 1 existe déjà et nous la sautons.

De plus, vous pouvez ensuite ajouter des notifications SNS ou SQS à vos compartiments S3 pour confirmer dans votre base de données que l’objet concerné est maintenant associé à un fichier - de manière efficace, étape 4.

Il s’agit d’un processus en plusieurs étapes, mais il peut très bien fonctionner sans avoir à gérer le téléchargement de fichiers au sein de votre API principale. Jusqu’à présent, cela fonctionne bien depuis notre mise en œuvre initiale (premiers jours de ce projet) pour des choses telles que les avatars des utilisateurs et l’ajout de fichiers PDF à un enregistrement.

Exemple de références:

http://docs.aws.Amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html

https://transloadit.com/docs/#authentication

1
Mark Johnson

Pour les autres personnes ayant ce problème avec loopback 3 et Postman qui sous POST, la connexion est suspendue (ou renvoie ERR_EMPTY_RESPONSE) (vue dans certains commentaires ici) ... Tapez "application/x-www-form-urlencoded"! 

Veuillez supprimer cet en-tête et ajouter "Accept" = "multipart/form-data". J'ai déjà déposé un bogue à la boucle pour ce comportement

1
PArt

Pour les utilisateurs du SDK AngularJS ... Si vous souhaitez utiliser des méthodes générées comme Container.upload (), vous pouvez ajouter une ligne pour configurer la méthode dans lb-services.js afin de définir les en-têtes Content-Type sur undefined. Cela permettrait au client de définir des en-têtes Content-Type et d'ajouter automatiquement une valeur de limite. Ressemblerait à quelque chose comme ceci: 

 "upload": {
    url: urlBase + "/containers/:container/upload",
    method: "POST",
    headers: {"Content-Type": undefined}
 }
0
blewherself

Il suffit de transmettre les données en tant qu’objet "params" et de les obtenir sur le serveur en tant que ctx.req.query 

Par exemple

Côté client

Upload.upload(
{
    url: '/api/containers/container_name/upload',
    file: file,
    //Additional data with file
    params:{
     orderId: 1, 
     customerId: 1,
     otherImageInfo:[]
    }
});

Côté serveur

Supposons que votre nom de modèle de stockage est container

Container.beforeRemote('upload', function(ctx,  modelInstance, next) {
    //OUPTUTS: {orderId:1, customerId:1, otherImageInfo:[]}
    console.log(ctx.req.query); 
    next();
})
0
Robins Gupta