Search code examples
angularjsvalidationexpressng-file-upload

How to manage validation in AngularJS of a file that has already been uploaded inside a form widget?


My AngularJS model has two properties, a file object, vm.profile.file, and a text value that contains a reference to the uploaded file on the server, vm.profile.resume. The vm.profile.file is validated as required with limited size. However, when the form is being updated, the reference which is acceptable and passed to the server is fine but there is not a need for the file object to be set to the server again. How do I validate the form if the reference to the file exists while the unneeded file object is empty?

I'm using ng-file-upload.

Here is what the form looks like: enter image description here

Here is some of the code I'm using:

    <div class="sj-section-content" flex="60">
        <md-card>
            <md-input-container>
                <div layout="row" layout-align="start center">
                    <md-button class="md-primary md-raised"
                               style="max-width: 150px; color: white;"
                               ngf-select
                               required
                               name="resume"
                               ngf-min-size="0MB"
                               ngf-max-size="1MB"
                               ng-model="vm.profile.file">
                        <span>{{!vm.profile.resume ? 'Select file' : 'Change'}}</span>
                    </md-button>
                    <md-button ng-if="vm.profile.resume" ng-click="vm.profile.file = {}" class="md-icon-button">
                        <md-icon md-font-icon="clear">clear</md-icon>
                    </md-button>
                </div>
                <div ng-if="vm.profile.file.name" layout="row" layout-align="start center">
                    <md-button class="md-icon-button" md-no-ink>
                        <md-icon md-font-icon="attachment">attachment</md-icon>
                    </md-button>
                    <div>{{vm.profile.file.name}}</div>
                </div>
                <div ng-messages="profileForm.resume.$error"
                     ng-if="profileForm.resume.$error && (profileForm.$submitted || profileForm.resume.$dirty)"
                     role="alert">
                    <div ng-if="profileForm.resume.$error.maxSize"
                         ng-message="maxSize">Max file size is 10MB</div>
                    <div ng-if="profileForm.resume.$error.required"
                         ng-message="required">Resume is a required field</div>
                </div>
            </md-input-container>
        </md-card>
    </div>

The API service:

    ProfileApi.prototype.update = function update(model) {
        var deferred = $q.defer();

        Upload.upload({
            url: endpoint,
            method: 'PUT',
            data: model
        }).then(function success(response) {
            deferred.resolve(response.data);
        }, function error(response) {
            deferred.reject(response.data);
        });

        return deferred.promise;
    };

And on the server:

exports.fileHandler = function(req, res, next) {
    var MAX_FILE_SIZE = 10 * 1000000;
    var FILE_FIELD = 'file';
    var PARENT_DIRECTORY = 'files/resumes/';

    // Validate here
    var fileFilter = function fileFilter (req, file, callback) {
        // allowed extensions .doc .docx .odt .pdf .txt
        var allowedMimeTypes = [
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.oasis.opendocument.text',
            'application/pdf',
            'text/plain'
        ];
        var validMimetype = allowedMimeTypes.some(function(mimetype) {
            return file.mimetype == mimetype;
        });

        if (!validMimetype) return callback(new Error('Resume is not a valid file format'));

        callback(null, true)
    };

    var fileOptions = {
        fileFilter: fileFilter,
        dest: 'tmp/',
        limits: {
            fileSize: MAX_FILE_SIZE
        }
    };
    var upload = multer(fileOptions).single(FILE_FIELD);

    upload(req, res, function(err) {
        if (err) return res.status(400).send({message: err.message});

        // We don't need a new file if req.body.resume has a value
        console.log(typeof req.body.resume);
        if (_.isEmpty(req.body.resume) && typeof req.file === 'undefined') {
            return res.status(400).send({message: 'Resume file is required'});
        }

        // Make sure that if there is a resume file but no new file, the resume exists in the file system.
        if (req.body.resume && typeof req.file === 'undefined') {
            fs.stat('client/' + req.body.resume, function(err, stats) {
                if (err) {
                    return res.status(400).send({message: 'Resume doesn\'t exist on file server'});
                } else {
                    return next();
                }
            })
        } else {
            // Let's add the file path at req.body.resume.
            // The idea is that if the save method on the model returns and error. Delete the file
            // in the tmp folder. Then return an error. If the model validates and is saved. Move the
            // file into the proper folder

            req.body.resume = PARENT_DIRECTORY + req.user._id + '/' + req.file.originalname;
            return next();
        }
    });
};

The update controller method:

exports.update = function(req, res) {
    var crewListing = req.app.locals.crewListing;

    // Protect information
    delete req.body.author;
    delete req.body.__v;
    delete req.body._id;

    // Merge objects
    _.merge(crewListing, req.body);

    crewListing.save({runValidators: true}, function(err, result) {
        if (req.file) {
            if (err) {
                // Delete the req file
                fs.unlink(req.file.path, function() {
                    return res.status(400).send(validationErrorHandler(err,true));
                });
            } else {
                // Move the req file
                console.log(req.file);
                fs.move(req.file.path, 'client/' + crewListing.resume, {clobber: true}, function(err) {
                    if (err) return res.status(400).send({message: 'Unexpected error has occured'})
                    return res.json(result);
                });
            }
        } else {
            if (err) return res.status(400).send(validationErrorHandler(err, true));
            return res.json(result);
        }
    })
};

Solution

  • ng-required directive on the file field can be conditional. If the resume text field has a value then the file field doesn't need to be required.

    ng-required="!vm.profile.resume"