如何在LoopBack中存储带有元数据的文件?

50
我想做的事情是:在一个html表单中,放置一个文件上传组件。当选择文件时,文件输入应该上传文件并获取文件id,所以当表单提交时,文件id将与表单一起发布并写入数据库。
简短版:我想与我的文件存储元数据(例如id)一起存储。
听起来很简单,但我在LoopBack中遇到了困难。
关于这个主题已经有过几次讨论(12),但似乎都没有解决方案,所以我认为这可能是找到一劳永逸的解决方案的好地方。
最简单的解决方案是使用模型关系,但LoopBack不支持与文件存储服务的关系。所以我们必须使用一个名为“File”的persistedmodel,并覆盖默认的创建和删除功能,以便它可以从我所拥有的文件存储模型(命名为“Storage”)中保存和删除。
我目前的设置:
  • 我有一个与Loopback存储服务连接的模型/api/Storage,并且可以成功将文件保存到本地文件系统中。
  • 我有一个与Mongo连接的PersistedModel,其中包含文件元数据:namesizeurlobjectId
  • 我设置了一个远程钩子,在create之前保存文件,然后将其url注入到File.create()

根据这个LoopBack页面,我已经拥有了ctx,应该里面有文件:

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

什么是ctx

ctx.req:Express请求对象。
ctx.result:Express响应对象。

好的,现在我来到了 Express 页面,感到有些迷茫,并且看到一些关于“body-parsing middleware”的内容,但我不知道它具体是什么。

我感觉我离解决方案很近了,任何帮助将不胜感激。这种方法正确吗?


我可以获取 File.beforeRemote('upload', function(ctx, modelInstance, next){ console.log(ctx.req); next(); }); 的数据,但是在 ctx 对象中看不到任何与文件相关的信息,而且 modelInstance 也是未定义的... 值得注意的是,这里的 File 是具有存储服务数据源的模型。 - RYFN
感谢RYFN查看此内容。为了保持一致性,我将坚持使用“文件”来表示文件元数据和存储ID,“存储”表示绑定到存储服务的文件模型。 - Mihaly KR
1
我可以轻松地对Storage.upload进行远程挂钩,并获取文件元数据,例如名称、大小等,并从挂钩中调用File.create(),但这不是最佳解决方案。由于File是一个persistentModel,因此可以将其设置为与User.profileimage相关联,例如,如果用户在表单中发布带有图像的内容,则Loopback会很好地处理它。因此,我仍在寻找一个关于File模型而不是Storage模型的解决方案。 - Mihaly KR
你如何从.upload钩子中获取文件元数据?你能展示一个例子吗? - RYFN
1
Storage.afterRemote('upload',function(ctx, modelInstance, next){ console.log('create file',modelInstance.result.files.file); next(); }); - Mihaly KR
7个回答

60

以下是使用loopback存储文件元数据的完整解决方案。

你需要一个容器模型。

common/models/container.json

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

server/datasources.json 中创建您的容器数据源。例如:
...
"storage": {
    "name": "storage",
    "connector": "loopback-component-storage",
    "provider": "filesystem", 
    "root": "/var/www/storage",
    "maxFileSize": "52428800"
}
...

你需要在 server/model-config.json 中设置该模型的数据源为你所拥有的 loopback-component-storage

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

您还需要一个文件模型来存储元数据并处理容器调用:
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": []
}

现在将文件容器连接起来:

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'}
        }
    );

};

为了公开文件API,请在model-config.json文件中添加files模型,并记得选择正确的数据源:

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

完成! 现在您可以调用POST /api/files/upload,在file表单字段中传递文件二进制数据。您将收到id、name、type和url的返回值。


File.app未定义 :( - Mallen
请确保在您的钩子中将“File”作为属性,如下所示:module.exports = function(File) {...} - Mihaly KR
1
嘿,我收到了一个错误响应,显示[Error: Request aborted]。我已经按照您的步骤进行操作了。请问有什么线索可以提供吗?谢谢。 - Vaibhav Magon
1
@NileshG,当然可以在File.create({name:fileInfo.name,type:fileInfo.type,container:fileInfo.container,url:CONTAINERS_URL + fileInfo.container +'/download/'+ fileInfo.name},.....中将名称设置为任何内容。 - Mihaly KR
2
以上配置给我返回了 ('Cannot override built-in "{{file}}" type.')); - Nauman Ahmad
显示剩余12条评论

11

我曾经遇到过同样的问题,我通过创建自己的模型来存储元数据并创建自己的上传方法来解决它。

  1. 我创建了一个名为 File 的模型,用于存储名称、类型、URL 和用户ID等信息(与您相同)。

  2. 我创建了自己的上传远程方法,因为我无法使用钩子完成。容器模型是由 loopback-component-storage 创建的模型。

  3. var fileInfo = fileObj.files.myFile[0]; 这里的 myFile 是文件上传的字段名称,所以您需要根据实际情况进行更改。如果您没有指定任何字段,那么它将作为 fileObj.file.null[0] 来处理。 在部署到生产环境之前,请务必添加适当的错误检查代码。

 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'}
  }

);

1
太棒了,你的答案指引了我寻找的解决方案。我会接受你的答案,并上传完整的解决方案和模型定义以备记录。 - Mihaly KR
太棒了!对于像我这样遇到“无法找到未定义的上传”问题的人来说,请确保您的容器模型名称为小写字母的“container”!或者更改它! - mehari

9

对于那些正在寻找如何在上传文件之前检查文件格式的答案的人。

实际上,在这种情况下,我们可以使用可选参数allowedContentTypes

在目录boot中使用示例代码:

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

我希望这能帮到某些人。

1
根据您的情况,值得考虑利用签名或类似的服务直接上传到Amazon S3、TransloadIT(用于图像处理)或类似的服务。
我们在这个概念上的第一个决定是,由于我们使用GraphQL,我们想避免通过GraphQL进行多部分表单上传,这反过来又需要传输到我们后面的Loopback服务。此外,我们希望保持这些服务器的高效性,而不会因为(大型)上传和相关文件验证和处理而可能占用资源。
您的工作流程可能如下:
1. 创建数据库记录 2. 返回记录ID和文件上传签名数据(包括S3存储桶或TransloadIT端点以及任何授权令牌) 3. 客户端上传到端点 对于像横幅或头像上传之类的情况,步骤1已经存在,因此我们跳过该步骤。
此外,您还可以向S3存储桶添加SNS或SQS通知,以确认相关对象现在已附加文件-有效地完成第4步。
这是一个多步骤的过程,但可以很好地工作,从而无需在核心API中处理文件上传。到目前为止,在我们最初的实现(在这个项目的早期阶段)中,对于像用户头像和将PDF附加到记录等事情,这个过程运行良好。
示例参考:

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

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


1
对于其他遇到loopback 3和Postman的问题,在POST时,连接挂起(或返回ERR_EMPTY_RESPONSE)(在这里的某些评论中看到)...在这种情况下的问题是,Postman使用Content-Type "application/x-www-form-urlencoded"!请删除该标头并添加“Accept”=“multipart/form-data”。我已经在loopback上报告了这个行为的bug。

1
当然可以,在这里找到它:https://github.com/strongloop/loopback-component-storage/issues/196 - PArt
问题的解决方案是显式地将Content-Type设置为未定义,这样您使用的浏览器或任何客户端都可以设置它并为您添加边界值。令人失望但却是真实的。 - ralixyle

0

只需将数据作为"params"对象传递,服务器端可以通过ctx.req.query获取它

例如:

在客户端:

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

在服务器端
假设你的存储模型名称是"container"。
Container.beforeRemote('upload', function(ctx,  modelInstance, next) {
    //OUPTUTS: {orderId:1, customerId:1, otherImageInfo:[]}
    console.log(ctx.req.query); 
    next();
})

1
感谢 Robins 抽出时间回复。提出的观点很好,但您提出的解决方案并没有解决主要问题:如何将此数据与文件 URL 一起存储和返回到同一 API(在您的情况下为 /api/containers/container_name/file)。Harshil 的解决方案更接近我所寻找的。感谢您的贡献。 - Mihaly KR
@MihalyKR 我认为这种方法可能有效。在您将文件上传到容器模型时,您会在响应中收到obj **providerResponse: {... ,"location": ".."}**,我正在考虑在beforeCreate钩子中使用此位置,并将其设置为File的URL。 因此,在一个方法中,您可以定义存储中的二进制数据和持久化模型的元数据。 - Joaquin Diaz

0
对于AngularJS SDK用户...如果您想使用生成的方法,如Container.upload(),您可能需要在lb-services.js中添加一行来配置该方法,以将Content-Type标头设置为undefined。这将允许客户端设置Content-Type标头并自动添加边界值。看起来会像这样:
 "upload": {
    url: urlBase + "/containers/:container/upload",
    method: "POST",
    headers: {"Content-Type": undefined}
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接