La descarga simple de archivos de Angularjs hace que el enrutador redirija

78

HTML:

<a href="mysite.com/uploads/asd4a4d5a.pdf" download="foo.pdf">

Las cargas obtienen un nombre de archivo único, mientras que el nombre real se mantiene en la base de datos. Quiero realizar una simple descarga de archivos. Pero el código anterior redirige a / debido a:

$routeProvider.otherwise({
    redirectTo: '/', 
    controller: MainController
});

Lo intenté con

$scope.download = function(resource){
    window.open(resource);
}

pero esto solo abre el archivo en una nueva ventana.

¿Alguna idea de cómo habilitar una descarga real para cualquier tipo de archivo?

Voto a favor
fuente
11
lo intentaste target="_blank"o target="_self"? Ver: docs.angularjs.org/guide/…
Moritz Petersen
2
@MoritzPetersen target = "_ self" funciona muy bien, haz que esto sea una respuesta, por favor
Votación a
6
Acepte la respuesta de jessegavins, ya que no podría haberla escrito mejor.
Moritz Petersen
Moritz, el enlace ahora está roto; debería ser docs.angularjs.org/guide/$location#html-link-rewriting
Ricky Clarkson

Respuestas:

114

https://docs.angularjs.org/guide/$location#html-link-rewriting

En casos como el siguiente, los enlaces no se reescriben; en su lugar, el navegador realizará una recarga de página completa al enlace original.

  • Enlaces que contienen el elemento de destino Ejemplo:
    <a href="https://stackoverflow.com/ext/link?a=b" target="_self">link</a>

  • Enlaces absolutos que van a un dominio diferente Ejemplo:
    <a href="http://angularjs.org/">link</a>

  • Enlaces que comienzan con '/' que conducen a una ruta base diferente cuando se define la base Ejemplo:
    <a href="https://stackoverflow.com/not-my-base/link">link</a>

Entonces, en su caso, debe agregar un atributo de destino como este ...

<a target="_self" href="example.com/uploads/asd4a4d5a.pdf" download="foo.pdf">
jessegavin
fuente
La URL absoluta no funcionará si el enlace apunta al mismo sitio.
Jan Święcki
1
Esta es una respuesta genial. Ahora solo tengo que averiguar cómo hacer esto con un botón y un POST 8- /
Snekse
1
@Snekse Si necesita descargar un archivo con un botón y un POST, simplemente cree una etiqueta <form> normal y una <button type = "submit"> como lo hicieron en 1996
jessegavin
1
:-) Tenía miedo de que dijeras eso. Estaba tratando de evitar un formulario ya que todos los datos que publico se generaron y no la entrada del usuario.
Snekse
1
Un punto a tener en cuenta, downloadno es compatible con IE o Safari.
Ashish Gaur
32

También tuvimos que desarrollar una solución que incluso funcionara con API que requieran autenticación (consulte este artículo )

Usando AngularJS en pocas palabras, así es como lo hicimos:

Paso 1: cree una directiva dedicada

// jQuery needed, uses Bootstrap classes, adjust the path of templateUrl
app.directive('pdfDownload', function() {
return {
    restrict: 'E',
    templateUrl: '/path/to/pdfDownload.tpl.html',
    scope: true,
    link: function(scope, element, attr) {
        var anchor = element.children()[0];

        // When the download starts, disable the link
        scope.$on('download-start', function() {
            $(anchor).attr('disabled', 'disabled');
        });

        // When the download finishes, attach the data to the link. Enable the link and change its appearance.
        scope.$on('downloaded', function(event, data) {
            $(anchor).attr({
                href: 'data:application/pdf;base64,' + data,
                download: attr.filename
            })
                .removeAttr('disabled')
                .text('Save')
                .removeClass('btn-primary')
                .addClass('btn-success');

            // Also overwrite the download pdf function to do nothing.
            scope.downloadPdf = function() {
            };
        });
    },
    controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) {
        $scope.downloadPdf = function() {
            $scope.$emit('download-start');
            $http.get($attrs.url).then(function(response) {
                $scope.$emit('downloaded', response.data);
            });
        };
    }] 
});

Paso 2: crea una plantilla

<a href="" class="btn btn-primary" ng-click="downloadPdf()">Download</a>

Paso 3: Úselo

<pdf-download url="/some/path/to/a.pdf" filename="my-awesome-pdf"></pdf-download>

Esto generará un botón azul. Al hacer clic, se descargará un PDF (Precaución: ¡el backend tiene que entregar el PDF en codificación Base64!) Y se colocará en el archivo href. El botón se vuelve verde y cambia el texto a Guardar . El usuario puede hacer clic de nuevo y se le presentará un diálogo de archivo de descarga estándar para el archivo my-awesome.pdf .

Nuestro ejemplo usa archivos PDF, pero aparentemente podría proporcionar cualquier formato binario dado que está codificado correctamente.

aix
fuente
2
Buena solución, pero hay dos limitaciones: 1. El usuario tiene que hacer clic en el botón dos veces, 2. IE 11 no admite el atributo de descarga, por lo que no puede establecer un nombre de archivo.
Louis Haußknecht
4
¿Qué pasa con los archivos grandes? 1GB? 10GB?
ecdeveloper
8

Si necesitas una directiva más avanzada, te recomiendo la solución que implementé, correctamente probada en Internet Explorer 11, Chrome y FireFox.

Espero que te sea de ayuda.

HTML:

<a href="#" class="btn btn-default" file-name="'fileName.extension'"  ng-click="getFile()" file-download="myBlobObject"><i class="fa fa-file-excel-o"></i></a>

DIRECTIVA:

directive('fileDownload',function(){
    return{
        restrict:'A',
        scope:{
            fileDownload:'=',
            fileName:'=',
        },

        link:function(scope,elem,atrs){


            scope.$watch('fileDownload',function(newValue, oldValue){

                if(newValue!=undefined && newValue!=null){
                    console.debug('Downloading a new file'); 
                    var isFirefox = typeof InstallTrigger !== 'undefined';
                    var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
                    var isIE = /*@cc_on!@*/false || !!document.documentMode;
                    var isEdge = !isIE && !!window.StyleMedia;
                    var isChrome = !!window.chrome && !!window.chrome.webstore;
                    var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
                    var isBlink = (isChrome || isOpera) && !!window.CSS;

                    if(isFirefox || isIE || isChrome){
                        if(isChrome){
                            console.log('Manage Google Chrome download');
                            var url = window.URL || window.webkitURL;
                            var fileURL = url.createObjectURL(scope.fileDownload);
                            var downloadLink = angular.element('<a></a>');//create a new  <a> tag element
                            downloadLink.attr('href',fileURL);
                            downloadLink.attr('download',scope.fileName);
                            downloadLink.attr('target','_self');
                            downloadLink[0].click();//call click function
                            url.revokeObjectURL(fileURL);//revoke the object from URL
                        }
                        if(isIE){
                            console.log('Manage IE download>10');
                            window.navigator.msSaveOrOpenBlob(scope.fileDownload,scope.fileName); 
                        }
                        if(isFirefox){
                            console.log('Manage Mozilla Firefox download');
                            var url = window.URL || window.webkitURL;
                            var fileURL = url.createObjectURL(scope.fileDownload);
                            var a=elem[0];//recover the <a> tag from directive
                            a.href=fileURL;
                            a.download=scope.fileName;
                            a.target='_self';
                            a.click();//we call click function
                        }


                    }else{
                        alert('SORRY YOUR BROWSER IS NOT COMPATIBLE');
                    }
                }
            });

        }
    }
})

EN CONTROLADOR:

$scope.myBlobObject=undefined;
$scope.getFile=function(){
        console.log('download started, you can show a wating animation');
        serviceAsPromise.getStream({param1:'data1',param1:'data2', ...})
        .then(function(data){//is important that the data was returned as Aray Buffer
                console.log('Stream download complete, stop animation!');
                $scope.myBlobObject=new Blob([data],{ type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
        },function(fail){
                console.log('Download Error, stop animation and show error message');
                                    $scope.myBlobObject=[];
                                });
                            }; 

EN SERVICIO:

function getStream(params){
                 console.log("RUNNING");
                 var deferred = $q.defer();

                 $http({
                     url:'../downloadURL/',
                     method:"PUT",//you can use also GET or POST
                     data:params,
                     headers:{'Content-type': 'application/json'},
                     responseType : 'arraybuffer',//THIS IS IMPORTANT
                    })
                    .success(function (data) {
                        console.debug("SUCCESS");
                        deferred.resolve(data);
                    }).error(function (data) {
                         console.error("ERROR");
                         deferred.reject(data);
                    });

                 return deferred.promise;
                };

BACKEND (en PRIMAVERA):

@RequestMapping(value = "/downloadURL/", method = RequestMethod.PUT)
public void downloadExcel(HttpServletResponse response,
        @RequestBody Map<String,String> spParams
        ) throws IOException {
        OutputStream outStream=null;
outStream = response.getOutputStream();//is important manage the exceptions here
ObjectThatWritesOnOutputStream myWriter= new ObjectThatWritesOnOutputStream();// note that this object doesn exist on JAVA,
ObjectThatWritesOnOutputStream.write(outStream);//you can configure more things here
outStream.flush();
return;
}
havelino
fuente
1
¿Entiendo correctamente: el archivo completo para descargar se lee en el espacio de datos de Javascript y luego se pasa al navegador para escribir en el archivo local? Imagine que los datos son de 1 GB o más; Creo que el navegador transmitirá el uso de la etiqueta <a> simple anterior de forma incremental. No estoy seguro de que reunir todos los datos en una sola cadena de matrices sea práctico en mi caso.
Mark Laff
Sí, eso es correcto, se podría usar una etiqueta <a> simple, pero en mi caso implementé esto por dos razones, la primera razón es, en mi caso, necesito construir un Excel dinámicamente con datos de la base de datos; y la segunda razón es que una etiqueta <a> simple no funciona en IE.
havelino
0

en plantilla

<md-button class="md-fab md-mini md-warn md-ink-ripple" ng-click="export()" aria-label="Export">
<md-icon class="material-icons" alt="Export" title="Export" aria-label="Export">
    system_update_alt
</md-icon></md-button>

en controlador

     $scope.export = function(){ $window.location.href = $scope.export; };
nat_jea
fuente