2

Estimadas(os),

Les escribo después de bastante tiempo de buscar y probar alternativas de solución, pero tosdas sin éxito... no soy un programador experto, pero con muchas ganas de aprender. Intento tener la posibilidad de subir más de un archivo a mi base en MongoDb, el código que adjunto permite seleccionar un sólo archivo por proyecto/documento. Agradezco la ayuda que me puedan dar orientándome con qué debería hacer.

Gracias.

Lado del BackEnd

Modelo:

    'use strict'
    var mongoose = require('mongoose');
    var Schema = mongoose.Schema;
    var ProjectSchema = Schema({
        name:String,
        description:String,
        category: String,
        year: Number,
        langs:String,
        image: [String]
        //image:[{ img: String }]
    });

    module.exports = mongoose.model('Project', ProjectSchema);

Mi archivo “app.js” del Backend

'use strict'

var express = require('express');
var bodyParser = require('body-parser');

var app = express();

// carga de los archivos de rutas
var project_routes = require('./routes/project');

// middlewares
app.use(bodyParser.urlencoded({extended:false}));
app.use(bodyParser.json());

// CORS
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Authorization, X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Allow-Request-Method');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
    res.header('Allow', 'GET, POST, OPTIONS, PUT, DELETE');
    next();
});
// rutas
app.use('/api', project_routes);
module.exports = app;

Rutas

'use strict'

var express = require('express');
var ProjectController = require('../controllers/project');
var router = express.Router();

var multipart = require('connect-multiparty');
var multipartMiddleware = multipart({uploadDir: './uploads'});

router.post('/upload-image/:id', multipartMiddleware, ProjectController.uploadImage); 
router.get('/get-image/:image', ProjectController.getImageFile);
module.exports = router;

El Controlador

'use strict'
var Project = require('../models/project');

var fs = require('fs');
var path = require('path');

var controller = {

    uploadImage:function(req, res){
        var projectId = req.params.id;
        var fileName = 'Imagen no subida...';

        if (req.files){

                      var filePath = req.files.image.path; 
                      var fileSplit = filePath.split('\\');
                      var fileName = fileSplit[1];
                      var extSplit = fileName.split('\.');
                      var fileExt = extSplit[1];

                    if (fileExt == 'png' || fileExt == 'jpg' || fileExt == 'jpeg' || fileExt == 'gif'  || fileExt == 'pdf'  || fileExt == 'docx'){

                            Project.findByIdAndUpdate(projectId, {image: fileName}, {new:true}, (err, projectUpdate) => {

                            if (err) return res.status(500).send({message: 'La imagen no se ha subido'});

                            if(!projectUpdate) return res.status(404).send({message:'El proyecto no existe y no se ha podido agregar el archivo'});

                            return res.status(200).send({
                                project:projectUpdate
                            });
                       });

                    }else{
                        fs.unlink(filePath, (err) => {
                            return res.status(200).send({message:'La extensión no es válida'});
                        });
                    }  
        }else{
            return res.status(200).send({
                message: fileName
            }); 


        }

     },

                 getImageFile: function(req, res){

                        var file= req.params.image;

                         var path_file = './uploads/' + file;

                        fs.exists(path_file, (exists) => {
                            if(exists){
                                return res.sendFile(path.resolve(path_file));
                            }else{
                                return res.status(200).send({
                                    message: 'No existe la imagen...'
                                });
                            }
                        }); 


    }

};

module.exports = controller;

En el FrontEnd (Angular) Modelo

export class Project{
    constructor(
        public _id: string,
        public name : string,
        public description: string,
        public category: string,
        public year: number,
        public langs: string,
        public image: [ string ]
    ){}
}

CreateComponent

import { Component, OnInit } from '@angular/core';
import { Project } from '../../models/project';
import { ProjectService } from '../../services/project.service';
import { UploadService } from '../../services/upload.service';
import { Global } from '../../services/global';

@Component({
  selector: 'app-create',
  templateUrl: './create.component.html',
  styleUrls: ['./create.component.css'],
  providers: [ProjectService, UploadService]
})
export class CreateComponent implements OnInit {

  public title: string;
  public project: Project;

  public save_project: any;
  public status: string;
  public filesToUpload: Array<File>;

 constructor(
   private _projectService: ProjectService,
   private _uploadService: UploadService
  ) {
    this.title = 'Crear proyecto';
    // Creo una nueva instancia de mi modelo de datos ("Project")
    this.project = new Project('', '', '', '', 2020, '', [''] );
  }

  ngOnInit() { 
  }

  onSubmit(form){

    console.log('Esto se va en el project')
    console.log(this.project);
    // Guardar datos básicos
    this._projectService.saveProject(this.project).subscribe(

        response => {
          if(response.project){

              // Subir la imagen... solo cuando la deba subir!
              if(this.filesToUpload){
                console.log( "El filesToUpload, en mi 'create.component.ts' tiene:  " );
                console.log( this.filesToUpload);

                this._uploadService.makeFileRequest(Global.url + 'upload-image/' + response.project._id, [], this.filesToUpload, 'image').
                        then((result:any) => {
                  this.save_project = result.project;
                  this.status = 'success';
                  console.log('El result me entrega:   ');
                  console.log(result);

                  // Con esto vacío el formulario una vez que ya se ha guardado
                  form.reset();
                });

              }else{
                this.save_project = response.project;
                this.status = 'success';
                form.reset();

              }

            }else{
              this.status = 'failed';
            }
        },
        error => {
          console.log(<any>error); 
        }

    );
  }

  fileChangeEvent(fileinput:any){
    console.log(' My fileinput entrega:  ' );
    console.log( fileinput );
    this.filesToUpload = fileinput.target.files as Array<File>;
  }
}

El Html

        <p>
            <label for="image">Imagen del proyecto</label>
             <span class = "image" *ngIf="project.name" style="float: none;">
                    <img src="{{url + 'get-image/' + project.image}}" style="width: 100px;"/>
             </span><br>

            <script type="text/javascript">
                alert("Hola Sebas");
            </script>
            <!-- La variable $event lleva todos las datos del input -->
            <input type="file" multiple="true" name="image" placeholder="subir imagen" (change) = "fileChangeEvent($event)"/>
        </p>
        <input type = "submit" value="Enviar" [disabled] = "!projectForm.form.valid" />
Jesús
  • 1,543
  • 3
  • 10
  • 25
Sebastián
  • 73
  • 9

2 Answers2

1

PROBLEMA

El problema está en tu backend. Ya te dieron una respuesta para resolver el problema del lado del front: para subir múltiples archivos en un elemento input de tipo file, debes establecer la propiedad multiple en el mismo:

<input type="file" name="uploads" id="uploads" multiple>

Sin embargo aunque has establecido esta propiedad en tu elemento input, la subida de archivos múltiples falla. (según tu posterior pregunta relacionada con esta).

El problema se encuentra ciertamente en la implementación del lado del servidor. El módulo que usas para trabajar con los datos recibidos, (aparte de no ser recomendado por el autor del mismo), maneja de forma diferente el caso para recibir un archivo y múltiples archivos.

Implementas connect-multiparty (desaconsejado por su autor), el cual se basa directamente en multiparty (del mismo autor de connect-multiparty).

Dado que tu aplicación es de Express y no de Connect, mi recomendación es que uses multiparty directamente para manejar los archivos que se envían desde el cliente.

SOLUCIÓN

La solución está en cambiar la forma en la que analizas los datos recibidos usando el módulo connect-multiparty, además te daré una sugerencia en cuanto a las rutas (endpoints) que actualmente manejas en tu aplicación.

Rutas

Actualmente tienes la siguiente ruta:

router.post('/upload-image/:id', multipartMiddleware, ProjectController.uploadImage);

Y en tu controlador haces lo siguiente:

uploadImage:function(req, res){
  var projectId = req.params.id;
  //...

Claramente el parámetro id que usas en tu ruta se refiere a una entidad que catalogas como proyecto.

Una buena práctica sobre rutas o endpoints, es mantener una coherencia sobre el tipo de entidad o dato que estoy modificando / consumiendo.

Si los archivos que voy a subir pertenecen a un proyecto X, entonces lo ideal sería usar una ruta que refleje la entidad proyecto, identificada por el id.

Por ejemplo:

router.post('/projects/:id/files', multipartMiddleware, ProjectController.uploadImage);

De esta forma es muy claro que estoy enviando (POST) información de archivos (files) sobre el proyecto identificado con id. Aunque tu aplicación es para subir imágenes, no haría falta escribir otra ruta si en algún momento se desea subir otro tipo de archivo, por ejemplo vídeos o documentos. Sin embargo, esto último dependerá de tu necesidad. También podemos usar el identificador uploads en sustitución de files, pero esto ya es a gusto del programador.

Uso de var en NodeJS

A menos que estés utilizando una versión muy antigua de NodeJS (inferior a 6.4.0), desaconsejo el uso de var para declarar variables. Es preferible usar siempre let y const. Toda versión de NodeJS igual o superior a 6.4.0 dan soporte completo a las sentencias let y const. Más información en el siguiente enlace: var, let, const… o nada en Javascript.

El controlador

Aclarados ciertos asuntos en los puntos anteriores, veamos lo que podemos hacer para solucionar el problema de la subida de múltiples archivos desde el cliente.

La documentación de connect-multiparty es bastante escasa, por no decir nula, pero es lo lógico al pensar que todo es simplemente una implementación de multiparty para connect. Así que vamos a mirar y a basarnos en la documentación de multiparty.

En tu controlador tienes actualmente lo siguiente:

uploadImage: (req, res, next) => {
  var projectId = req.params.id;
  var fileName = 'Imagen no subida...';
  if (req.files){
    var filePath = req.files.image.path; 
    var fileSplit = filePath.split('\\');
    var fileName = fileSplit[1];
    var extSplit = fileName.split('\.');
    var fileExt = extSplit[1];
    if (fileExt == 'png' || fileExt == 'jpg' || fileExt == 'jpeg' || fileExt == 'gif'  || fileExt == 'pdf'  || fileExt == 'docx') {
      Project.findByIdAndUpdate(projectId, {image: fileName}, {new:true}, (err, projectUpdate) => {
        if (err) return res.status(500).send({message: 'La imagen no se ha subido'});
        if(!projectUpdate) return res.status(404).send({message:'El proyecto no existe y no se ha podido agregar el archivo'});
        return res.status(200).send({
          project:projectUpdate
        });
      });
    } else {
      fs.unlink(filePath, (err) => {
        return res.status(200).send({message:'La extensión no es válida'});
      });
    }  
  } else {
    return res.status(200).send({
      message: fileName
    }); 
  }
}

Analicemos lo que haces contra lo que dice la documentación.

Primero verificas si la solicitud (req) contiene un campo llamado files. Si no lo tiene devuelves al cliente el mensaje: 'Imagen no subida...'

Si la solicitud contiene el campo files lo desglosas, y aquí es donde la cosa funciona para 1 simple archivo y falla para múltiples archivos.

Resulta que la función middleware proporcionada por connect-multiparty, define el objeto files con la siguiente estructura, cuando se sube un solo archivo:

//objeto files
{
  <input_fieldname>: { // <- es un objeto con info de un único archivo
    fieldName: <String>,
    originalFileName: <String>,
    // ...
  }
}

Y cuando se reciben múltiples archivos, el formato del objeto files es diferente:

//objeto files
{
  <input_fieldname>: [ // <- es una lista de objetos
    {
      fieldName: <String>,
      originalFileName: <String>,
      // ...
    },
    //...
    {
      fieldName: <String>,
      originalFileName: <String>,
      // ...
    }
  ],
  //...
}

¡Bingo!

Hemos encontrado el problema, debemos hacer el cambio en el controlador para determinar si se trata de un solo archivo o de múltiples archivos.

Por ejemplo:

uploadImage: (req, res, nex) => {
  //...
  if(req.files && req.files.uploads && Array.isArray(req.files.uploads)) { // suponiendo que espero el input llamado 'uploads'
    let images = req.files.uploads.map(file => {
      // operaciones con cada elemento file para obtener el nombre del archivo y guardarlo en una lista
      return fileName;
    });
    Project.findByIdAndUpdate(id, { $push: {images: {$each: images} } }, callback);
    // ...
  } else if(req.files && req.files.uploads) { // caso de un único archivo
    // lógica para obtener el nombre del archivo
    // let imageName = ...
    Project.findByIdAndUpdate(id, { $push: {images: imageName} }, callback);
    //..
  } else {
    // caso de no recibir imágenes
  }
}

Esto implica un cambio en el esquema de datos, ya que ahora debemos usar una lista para guardar el nombre de la imagen o posibles imágenes subidas desde el cliente. Es por ello que utilizo el campo images que debo declararlo como un tipo Array en el esquema.

Además, utilizo el operador de actualización $push, junto con el modificador $each cuando son varias imágenes.

Espero que esto te ayude a resolver el problema.

Mauricio Contreras
  • 13,660
  • 3
  • 18
  • 40
  • Gracias Mauricio, Te tomaste un buen tiempo para documentar al máximo tu respuesta que, ciertamente ma ha ayudado a resolver el problema que había planteado. Gracias de nuevo por tu tiempo, dedicación y ayuda. – Sebastián Sep 14 '20 at 14:48
  • Hola – Sebastián, disculpa crees que puedas subir de nuevo el código de como lo solucionaste tengo el mismo problema. – Darklawz Dec 11 '20 at 01:47
0

En el html en el input=file hay que agregar la palabra multiple:

<h4>Welcome to !</h4>
<mat-card>
      <form [formGroup]="uploadForm" (ngSubmit)="uploadSubmit()">
        <mat-card-content>
          <mat-form-field class="form-field">
            <mat-label>Select Document Type</mat-label>
            <mat-select formControlName="type" required>
              <mat-option value="Passport">Passport</mat-option>
              <mat-option value="Driving_license">Driving License</mat-option>
              <mat-option value="PAN">PAN</mat-option>
            </mat-select>
          </mat-form-field>
          <br>
          <input formControlName="document" type="file" ng2FileSelect accept=".png" [uploader]="uploader" multiple/><br/>
          <br>
          <div class="drop-zone">
          <div ng2FileDrop [uploader]="uploader" class="drop-zone">
             Drag and drop files to upload
          </div>
          </div>
          <table>
            <thead>
            <tr>
              <th width="90%">
                File Name
              </th>
              <th width="10%">
                Remove
              </th>
            </tr>
            </thead>
            <tbody>
            <tr *ngFor="let item of uploader.queue">
              <th width="90%">
                (NaN MB)
              </th>
              <th class="text-center" width="10%">
                <mat-icon (click)="item.remove()">delete</mat-icon>
              </th>
            </tr>
            </tbody>
          </table>
          <br>
          <button mat-raised-button color="accent" [disabled]="!uploadForm.valid" type="submit">Upload Data</button>
        </mat-card-content>
      </form>
</mat-card>

Y en el controlador importas FileUploader y creas un uploader que tiene una lista queue donde vienen los archivos. Para enviar varios archivos al backend usas un FormData

    import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {FileUploader} from "ng2-file-upload";
import {Observable} from "rxjs";
import {HttpClient} from "@angular/common/http";

@Component({
  selector: 'app-file-upload',
  templateUrl: './file-upload.component.html',
  styleUrls: ['./file-upload.component.css']
})
export class FileUploadComponent implements OnInit {

  uploadForm: FormGroup;

  public uploader:FileUploader = new FileUploader({
    isHTML5: true
  });
  title: string = 'Angular File Upload';
  constructor(private fb: FormBuilder, private http: HttpClient ) { }

  uploadSubmit(){
        for (let i = 0; i < this.uploader.queue.length; i++) {
          let fileItem = this.uploader.queue[i]._file;
          if(fileItem.size > 10000000){
            alert("Each File should be less than 10 MB of size.");
            return;
          }
        }
        for (let j = 0; j < this.uploader.queue.length; j++) {
          let data = new FormData();
          let fileItem = this.uploader.queue[j]._file;
          console.log(fileItem.name);
          data.append('file', fileItem);
          data.append('fileSeq', 'seq'+j);
          data.append( 'dataType', this.uploadForm.controls.type.value);
          this.uploadFile(data).subscribe(data => alert(data.message));
        }
        this.uploader.clearQueue();
  }

  uploadFile(data: FormData): Observable {
    return this.http.post('http://localhost:8080/upload', data);
  }

  ngOnInit() {
    this.uploadForm = this.fb.group({
      document: [null, null],
      type:  [null, Validators.compose([Validators.required])]
    });
  }

}

Y solo tengo hasta ahí porque el backend lo tengo en java.

abrahamhs
  • 3,376
  • 1
  • 16
  • 39