使用Mongoose、Express和AngularJS上传图片

18

我知道这个问题已经被问了很多次,我几乎阅读了所有与此主题相关的内容,包括:

https://dev59.com/qIHba4cB1Zd3GeqPYfs3#25022437

使用Node.js、Express和Mongoose上传图片

这些是我迄今为止找到的最好的资源。但我的问题在于它们仍然不太清晰,在线文档中几乎没有关于此的资料,讨论似乎针对的是比我更高级的人。

因此,如果有人能够帮助我详细介绍如何使用Mongoose、Express和AngularJS上传图片,我将非常感激。实际上,我正在使用MEAN全栈。(具体来说是这个生成器 - https://github.com/DaftMonk/generator-angular-fullstack

AddController:

'use strict';

angular.module('lumicaApp')
  .controller('ProjectAddCtrl', ['$scope', '$location', '$log', 'projectsModel', 'users', 'types', function ($scope, $location, $log, projectsModel, users, types) {
    $scope.dismiss = function () {
      $scope.$dismiss();
    };

        $scope.users = users;
        $scope.types = types;

    $scope.project = {
            name: null,
            type: null,
            images: {
                thumbnail: null // I want to add the uploaded images _id here to reference with mongoose populate.
            },
            users: null
        };

        $scope.save = function () {
            $log.info($scope.project);
            projectsModel.post($scope.project).then(function (project) {
        $scope.$dismiss();
            });
        }

  }]);

我希望将图片ID引用添加到project.images.thumbnail中,但我想使用以下模式将所有信息存储在一个Image对象中:

'use strict';

    var mongoose = require('mongoose'),
        Schema = mongoose.Schema;

    var ImageSchema = new Schema({
      fileName: String,
      url: String,
      contentType: String,
      size: String,
      dimensions: String
    });

    module.exports = mongoose.model('Image', ImageSchema);

我还将https://github.com/nervgh/angular-file-upload添加到我的bower包中。
就像我说的一样,我无法弄清楚如何把它们全部联系起来。而且我甚至不确定我尝试做的是否正确。
--------------------------------------------------------------------------
更新:
这是我现在拥有的,我已经添加了一些注释详细说明我想如何运作,不幸的是,我仍然没有成功,我甚至不能让图像开始上传,更别说上传到S3了。很抱歉我让你感到困扰,但我发现这特别令人困惑,这让我很惊讶。
client/app/people/add/add.controller.js
'use strict';

angular.module('lumicaApp')
    .controller('AddPersonCtrl', ['$scope', '$http', '$location', '$window', '$log', 'Auth', 'FileUploader', 'projects', 'usersModel', function ($scope, $http, $location, $window, $log, Auth, FileUploader, projects, usersModel) {
        $scope.dismiss = function () {
            $scope.$dismiss();
        };

        $scope.newResource = {};

        // Upload Profile Image
        $scope.onUploadSelect = function($files) {
            $scope.newResource.newUploadName = $files[0].name;

            $http
                .post('/api/uploads', {
                    uploadName: newResource.newUploadName,
                    upload: newResource.newUpload
                })
                .success(function(data) {
                    newResource.upload = data; // To be saved later
                });
        };

        $log.info($scope.newResource);

        //Get Projects List
        $scope.projects = projects;

        //Register New User
        $scope.user = {};
        $scope.errors = {};


        $scope.register = function(form) {
            $scope.submitted = true;

            if(form.$valid) {
                Auth.createUser({
                    firstName: $scope.user.firstName,
                    lastName: $scope.user.lastName,
                    username: $scope.user.username,
                    profileImage: $scope.user.profileImage, // I want to add the _id reference for the image here to I can populate it with 'ImageSchema' using mongoose to get the image details(Name, URL, FileSize, ContentType, ETC)
                    assigned: {
                        teams: null,
                        projects: $scope.user.assigned.projects
                    },
                    email: $scope.user.email,
                    password: $scope.user.password
                })
                    .then( function() {
                        // Account created, redirect to home
                        //$location.path('/');
                        $scope.$dismiss();
                    })
                    .catch( function(err) {
                        err = err.data;
                        $scope.errors = {};

                        // Update validity of form fields that match the mongoose errors
                        angular.forEach(err.errors, function(error, field) {
                            form[field].$setValidity('mongoose', false);
                            $scope.errors[field] = error.message;
                        });
                    });
            }
        };

        $scope.loginOauth = function(provider) {
            $window.location.href = '/auth/' + provider;
        };

    }]);

server/api/image/image.model.js 我希望将所有图片信息存储在这里,并使用它来填充人物控制器中的profileImage

'use strict';

    var mongoose = require('mongoose'),
        Schema = mongoose.Schema;

    var ImageSchema = new Schema({
      fileName: String,
      url: String, // Should store the URL of image on S3.
      contentType: String,
      size: String,
      dimensions: String
    });

    module.exports = mongoose.model('Image', ImageSchema);

client/app/people/add/add.jade

.modal-header
    h3.modal-title Add {{ title }}
.modal-body
    form(id="add-user" name='form', ng-submit='register(form)', novalidate='')
        .form-group(ng-class='{ "has-success": form.firstName.$valid && submitted,\
        "has-error": form.firstName.$invalid && submitted }')
            label First Name
            input.form-control(type='text', name='firstName', ng-model='user.firstName', required='')
            p.help-block(ng-show='form.firstName.$error.required && submitted')
                | First name is required

        .form-group(ng-class='{ "has-success": form.lastName.$valid && submitted,\
        "has-error": form.lastName.$invalid && submitted }')
            label Last Name
            input.form-control(type='text', name='lastName', ng-model='user.lastName', required='')
            p.help-block(ng-show='form.lastName.$error.required && submitted')
                | Last name is required

        .form-group(ng-class='{ "has-success": form.username.$valid && submitted,\
        "has-error": form.username.$invalid && submitted }')
            label Username
            input.form-control(type='text', name='username', ng-model='user.username', required='')
            p.help-block(ng-show='form.username.$error.required && submitted')
                | Last name is required

        // Upload Profile Picture Here
        .form-group
            label Profile Image
            input(type="file" ng-file-select="onUploadSelect($files)" ng-model="newResource.newUpload")

        .form-group(ng-class='{ "has-success": form.email.$valid && submitted,\
        "has-error": form.email.$invalid && submitted }')
            label Email
            input.form-control(type='email', name='email', ng-model='user.email', required='', mongoose-error='')
            p.help-block(ng-show='form.email.$error.email && submitted')
                | Doesn't look like a valid email.
            p.help-block(ng-show='form.email.$error.required && submitted')
                | What's your email address?
            p.help-block(ng-show='form.email.$error.mongoose')
                | {{ errors.email }}

        .form-group(ng-class='{ "has-success": form.password.$valid && submitted,\
        "has-error": form.password.$invalid && submitted }')
            label Password
            input.form-control(type='password', name='password', ng-model='user.password', ng-minlength='3', required='', mongoose-error='')
            p.help-block(ng-show='(form.password.$error.minlength || form.password.$error.required) && submitted')
                | Password must be at least 3 characters.
            p.help-block(ng-show='form.password.$error.mongoose')
                | {{ errors.password }}

        .form-group
            label Assign Project(s)
            br
            select(multiple ng-options="project._id as project.name for project in projects" ng-model="user.assigned.projects")
        button.btn.btn-primary(ng-submit='register(form)') Save

    pre(ng-bind="user | json")
.modal-footer
    button.btn.btn-primary(type="submit" form="add-user") Save
    button.btn.btn-warning(ng-click='dismiss()') Cancel

server/api/upload/index.js

'use strict';

var express = require('express');
var controller = require('./upload.controller');

var router = express.Router();

//router.get('/', controller.index);
//router.get('/:id', controller.show);
router.post('/', controller.create);
//router.put('/:id', controller.update);
//router.patch('/:id', controller.update);
//router.delete('/:id', controller.destroy);

module.exports = router;

server/api/upload/upload.controller.js

'use strict';

var _ = require('lodash');
//var Upload = require('./upload.model');
var aws = require('aws-sdk');
var config = require('../../config/environment');
var randomString = require('../../components/randomString');

// Creates a new upload in the DB.
exports.create = function(req, res) {
    var s3 = new aws.S3();
    var folder = randomString.generate(20); // I guess I do this because when the user downloads the file it will have the original file name.
    var matches = req.body.upload.match(/data:([A-Za-z-+\/].+);base64,(.+)/);

    if (matches === null || matches.length !== 3) {
        return handleError(res, 'Invalid input string');
    }

    var uploadBody = new Buffer(matches[2], 'base64');

    var params = {
        Bucket: config.aws.bucketName,
        Key: folder + '/' + req.body.uploadName,
        Body: uploadBody,
        ACL:'public-read'
    };

    s3.putObject(params, function(err, data) {
        if (err)
            console.log(err)
        else {
            console.log("Successfully uploaded data to my-uploads/" + folder + '/' + req.body.uploadName);
            return res.json({
                name: req.body.uploadName,
                bucket: config.aws.bucketName,
                key: folder
            });
        }
    });
};

function handleError(res, err) {
    return res.send(500, err);
}

server/config/environment/development.js

aws: {
        key: 'XXXXXXXXXXXX',
        secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
        region: 'sydney',
        bucketName: 'my-uploads'
    }

我使用这个生成器,回家后会回答。在文件输入上使用ng-file-upload>上传为base64>并在服务器端解码为二进制req.body.upload.match(/data:([A-Za-z-+\/].+);base64,(.+)/);>发布到cloudinary或s3,并将返回的id保存到我的mongo db中。 - Michael J. Calkins
太好了,谢谢。我正在使用S3,但实际上从未正确地使用过它,所以如果您能在这一部分上尽量清晰明了,那将是非常有帮助的,谢谢 :) - Daimz
如果您需要更详细的解释,请告诉我,这是一个令人沮丧的过程,我相信其他人也有同样的问题。 - Michael J. Calkins
@Michael 谢谢你的尝试帮助。 看起来似乎没有明确的答案可以实现这一点,这似乎是经常发生的事情,但是相关文档很少且零散。我离能够上传文件并没有比我第一次发布问题时更进一步,似乎又是个死路。 - Daimz
@Daimz 你说文件没有上传。还是一样吗?你试过按照我评论中提到的更改代码,至少尝试一下系统是否能够检测到选择文件时的输入更改吗?这样应该至少可以开始上传了。 - Alejandro Teixeira Muñoz
4个回答

12

所有这些代码都是从一个依赖于大型文件上传和图像的项目中直接提取出来的。一定要查看 https://github.com/nervgh/angular-file-upload

在我看来,某个地方:

<div class="form-group">
  <label>File Upload</label>
  <input type="file" ng-file-select="onUploadSelect($files)" ng-model="newResource.newUpload">
</div>

使用模块angularFileUpload后,在我的控制器中有:

$scope.onUploadSelect = function($files) {
  $scope.newResource.newUploadName = $files[0].name;
};

https://github.com/nervgh/angular-file-upload

当用户点击上传时,我执行此操作以将文件发送到上传位置:

$http
  .post('/api/uploads', {
    uploadName: newResource.newUploadName,
    upload: newResource.newUpload
  })
  .success(function(data) {
    newResource.upload = data; // To be saved later
  });

这个请求被发送到一个类似于以下的控制器:

'use strict';

var _ = require('lodash');
var aws = require('aws-sdk');
var config = require('../../config/environment');
var randomString = require('../../components/randomString');

// Creates a new upload in the DB.
exports.create = function(req, res) {
  var s3 = new aws.S3();
  var folder = randomString.generate(20); // I guess I do this because when the user downloads the file it will have the original file name.
  var matches = req.body.upload.match(/data:([A-Za-z-+\/].+);base64,(.+)/);

  if (matches === null || matches.length !== 3) {
    return handleError(res, 'Invalid input string');
  }

  var uploadBody = new Buffer(matches[2], 'base64');

  var params = {
    Bucket: config.aws.bucketName,
    Key: folder + '/' + req.body.uploadName,
    Body: uploadBody,
    ACL:'public-read'
  };

  s3.putObject(params, function(err, data) {
    if (err)
      console.log(err)
    else {
      console.log("Successfully uploaded data to csk3-uploads/" + folder + '/' + req.body.uploadName);
      return res.json({
        name: req.body.uploadName,
        bucket: config.aws.bucketName,
        key: folder
      });
    }
   });
};

function handleError(res, err) {
  return res.send(500, err);
}

服务器/组件/随机字符串/index.js

'use strict';

module.exports.generate = function(textLength) {
  textLength = textLength || 10;
  var text = '';
  var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

  for(var i = 0; i < textLength; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }

  return text;
};

输入图像描述

server/config/environment/development.js

输入图像描述

server/api/upload/upload.controller.js

输入图像描述


我收到了这个错误 Error: Cannot find module '../../components/randomString' - Daimz
由于我正在使用Yeomans Angular-fullstack生成器,所以文件夹结构有些不同。server/api/uploads是上传控制器等的位置,但文件被分解了。 index.js, uploads.controller.js, uploads.module.js, uploads.spec.js 我有点困惑,不知道应该在哪里添加您的代码以及在文件结构中的位置。 - Daimz
1
@Daimz,我刚刚添加了一堆截图,希望能更好地解释,并且还有randomString - Michael J. Calkins
谢谢,现在清晰多了。 我已经包含了所有内容,但是现在出现了这个错误:ReferenceError: newResource is not defined - Daimz
bucketName: 'my-uploads' }``` 这是我应该添加密钥等内容的地方吗?还是在其他地方处理? - Daimz
显示剩余6条评论

2
据我猜测,您正在将FileReader.onload()方法绑定到saveUserImage函数中,然后由于该函数从未被绑定,因此不会调用onload方法。在编辑图像之前,用户调用saveUserImage方法后,不会选择任何图像,因为onload()方法不会执行。
尝试以这种方式编写客户端控制器
//This goes outside your method and will handle the file selection.This must be executed when your `input(type=file)` is created. Then we will use ng-init to bind it.

  $scope.onAttachmentSelect = function(){
        var logo = new FileReader();  // FileReader.
        logo.onload = function (event) {
        console.log("THE IMAGE HAS LOADED");
        var file = event.currentTarget.files[0]
        console.log("FILENAME:"+file.name);
        $scope.image = file; 
        $scope.logoName = file.name; // Assigns the file name.
           $scope.$apply();
           //Call save from here
           $scope.saveuserImage();
        };
        logo.readAsDataURL(file[0]);
        $scope.file = file[0];
       $scope.getFileData = file[0].name
            reader.readAsDataURL(file);
    };


//The save method is called from the onload function (when you add a new file)
$scope.saveuserImage =  function(){
    console.log("STARGING UPLOAD");
    $scope.upload = $upload.upload({  // Using $upload
        url: '/user/'+$stateParams.id+'/userImage',  
        method:'put'
        data:,   $scope.image
        file: $scope.file
    }).progress(function (evt) {})
        .success(function () {
            location.reload();
            $scope.file = "";
            $scope.hideUpload = 'true'
        });
    $scope.getFileData = '';
 //        location.reload()
};

HTML是一种标记语言。
//There is the ng-init call to binding function onAttachmentSelect
<div class="form-group">
  <label>File Upload</label>
  <input type="file" ng-init="onAttachmentSelect" ng-model="newResource.newUpload">
</div>

希望这个线索能够帮到您。

编辑*

尝试向您解释一下检查代码所需遵循的不同步骤:

1.- 您的input[type=file]是否显示?如果显示,请选择一张图片

2.- 当所选图片更改时,您的输入是否调用了onload函数?(应该会打印出我的代码版本)

3.- 如果已经调用。在onload方法内部进行需要发送的操作(如果可能)

4.- 当此方法完成所需的更改时。使用ng-model或其他方式,通知您准备上传的对象中的变量,并将生成的base64字符串放入onload方法中。

到达此点时,请记住检查以下内容:

由于非常大的图像可能通过json和base64发送,因此重要的是记住更改Express.js中的最小json大小以防止拒绝。例如,在您的服务器/app.js中执行此操作:

var bodyParser = require('body-parser');
app.use(bodyParser.json({limit: '50mb'}));
app.use(bodyParser.urlencoded({limit: '50mb'}));

请记住,方法reader.readAsDataURL(file)将为您提供一个base64字符串,该字符串可以作为图像的src。您不需要更多内容。这个base64是您可以保存在mongoose中的内容。然后,您可以使用ng-model在“提交”按钮所在的表单中发送包含base64的变量。
然后,在处理表单的Express.js端点中,您可以将base64字符串解码为文件,或者直接将base64保存在mongoose中(如果要加载大量图像或希望加载大型图像,则不建议在数据库中存储图像,因为mongoDB查询速度会非常慢)。
希望这些指示可以帮助您解决问题。如果您仍有疑问,请留言,我会尽力帮助。

2
这是我使用MEAN.JS进行文件上传的方法。 模型
var UserSchema = new mongoose.Schema({
name:{type:String,required:true},
photo:Buffer  // Image
});

服务器控制器

var userPicture = function(req,res){             // Stores Picture for a user matching the ID.
user.findById(req.param('id'), function (err, user) {
    console.log(req.files) // File from Client
    if(req.files.file){   // If the Image exists
        var fs = require('node-fs');
        fs.readFile(req.files.file.path, function (dataErr, data) {
            if(data) {
                user.photo ='';
                user.photo = data;  // Assigns the image to the path.
                user.save(function (saveerr, saveuser) {
                    if (saveerr) {
                        throw saveerr;
                    }
                    res.json(HttpStatus.OK, saveuser);                        
                });
            }
        });
        return
    }
    res.json(HttpStatus.BAD_REQUEST,{error:"Error in file upload"});
});
};

客户端控制器

$scope.saveuserImage =  function(){
    $scope.upload = $upload.upload({  // Using $upload
        url: '/user/'+$stateParams.id+'/userImage',  // Direct Server Call.
        method:'put',
        data:'',  // Where the image is going to be set.
        file: $scope.file
    }).progress(function (evt) {})
        .success(function () {
            var logo = new FileReader();  // FileReader.

            $scope.onAttachmentSelect = function(file){
                logo.onload = function (e) {
                    $scope.image = e.target.result;  // Assigns the image on the $scope variable.
                    $scope.logoName = file[0].name; // Assigns the file name.
                    $scope.$apply();
                };
                logo.readAsDataURL(file[0]);
                $scope.file = file[0];
                $scope.getFileData = file[0].name
            };
            location.reload();
            $scope.file = "";
            $scope.hideUpload = 'true'
        });
    $scope.getFileData = '';
 //        location.reload()
};

Html

ng-file-select用于从客户端获取文件。

这对我来说很有效。希望这能帮到你。

注意:我使用了HTML标签而不是jade。在使用jade时适用相应的更改。


我认为你可以通过将请求体更改为form-data并使用req.body来避免使用node-fs - Michael J. Calkins

0

我也是一个使用MEANJS的新手,这是我如何使用ng-flow + FileReader让它工作的方法:

HTML输入:

<div flow-init 
        flow-files-added="processFiles($files)"
        flow-files-submitted="$flow.upload()" 
        test-chunks="false">
        <!-- flow-file-error="someHandlerMethod( $file, $message, $flow )"     ! need to implement-->
        <div class="drop" flow-drop ng-class="dropClass">
            <span class="btn btn-default" flow-btn>Upload File</span>
            <span class="btn btn-default" flow-btn flow-directory ng-show="$flow.supportDirectory">Upload Folder</span>
            <b>OR</b>
            Drag And Drop your file here
        </div>

控制器:

    $scope.uploadedImage = 0;

    // PREPARE FILE FOR UPLOAD
    $scope.processFiles = function(flow){
        var reader = new FileReader();
        reader.onload = function(event) {
            $scope.uploadedImage = event.target.result;
        };
        reader.onerror = function(event) {
            console.error('File could not be read! Code ' + event.target.error.code);
        };
        reader.readAsDataURL(flow[0].file);
    };

在服务器端,接收上传图像的模型上的变量只是字符串类型。 从服务器检索它不需要任何转换:
<img src={{result.picture}} class="pic-image" alt="Pic"/>

现在只需要找出如何处理大文件...


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