Usando HTML5 / JavaScript para generar y guardar un archivo


317

Últimamente he estado jugando con WebGL y he conseguido que funcione un lector Collada. El problema es que es bastante lento (Collada es un formato muy detallado), así que voy a comenzar a convertir archivos a un formato más fácil de usar (probablemente JSON). Ya tengo el código para analizar el archivo en JavaScript, ¡así que también puedo usarlo como mi exportador! El problema es el ahorro.

Ahora, sé que puedo analizar el archivo, enviar el resultado al servidor y hacer que el navegador solicite el archivo del servidor como descarga. Pero en realidad el servidor no tiene nada que ver con este proceso en particular, entonces, ¿por qué involucrarse? Ya tengo el contenido del archivo deseado en la memoria. ¿Hay alguna forma de presentarle al usuario una descarga usando JavaScript puro? (Lo dudo, pero bien podría preguntar ...)

Y para ser claros: ¡no estoy tratando de acceder al sistema de archivos sin el conocimiento de los usuarios! El usuario proporcionará un archivo (probablemente mediante arrastrar y soltar), el script transformará el archivo en la memoria y se le pedirá al usuario que descargue el resultado. Todo lo cual debería ser actividades "seguras" en lo que respecta al navegador.

[EDITAR]: No lo mencioné por adelantado, por lo que los carteles que respondieron "Flash" son lo suficientemente válidos, pero parte de lo que estoy haciendo es un intento de resaltar lo que se puede hacer con HTML5 puro ... así que Flash es justo en mi caso. (Aunque es una respuesta perfectamente válida para cualquiera que esté haciendo una aplicación web "real"). En ese caso, parece que no tengo suerte a menos que quiera involucrar al servidor. ¡Gracias de cualquier manera!


8
Podría considerar cambiar la respuesta aceptada, parece que hay una forma puramente HTML ahora
Pekka

Respuestas:


256

OK, crear un dato: URI definitivamente hace el truco para mí, ¡gracias a Matthew y Dennkster señalando esa opción! Aquí es básicamente cómo lo hago:

1) obtenga todo el contenido en una cadena llamada "contenido" (por ejemplo, al crearlo allí inicialmente o al leer innerHTML de la etiqueta de una página ya creada).

2) Construir el URI de datos:

uriContent = "data:application/octet-stream," + encodeURIComponent(content);

Habrá limitaciones de longitud según el tipo de navegador, etc., pero, por ejemplo, Firefox 3.6.12 funciona hasta al menos 256k. En cambio, codificar en Base64 usando encodeURIComponent podría hacer que las cosas sean más eficientes, pero para mí eso estuvo bien.

3) abra una nueva ventana y "rediríjalo" a este URI solicita una ubicación de descarga de mi página generada en JavaScript:

newWindow = window.open(uriContent, 'neuesDokument');

Eso es.


34
Si desea evitar el uso de una ventana emergente, que puede bloquearse, puede configurar location.hrefel contenido. location.href = uriContent.
Alex Turpin el

12
Hola, lo intenté pero descarga el archivo como archivo .part. ¿Cómo puedo configurar el tipo de archivo?
Sedat Başar

66
@ Los URI de datos de SedatBaşar no admiten la configuración de un nombre de archivo, debe confiar en que el navegador configure una extensión adecuada utilizando el tipo mime. Pero si el navegador puede representar el tipo MIME, no lo descargará, sino que lo mostrará. Hay otras formas de hacer esto, pero ninguna funciona en IE <10.
panzi

77
IE realmente no admite URI de datos y Firefox parece guardar los archivos con un nombre aleatorio.
nylund

25
Creo que estamos haciendo esto más difícil de lo que debería ser. Abra su consola JS en esta página y póngala location.href = "data:application/octet-stream," + encodeURIComponent(jQuery('#answer-4551467 .post-text').first().text());y guardará el contenido de la respuesta de @ Nøk en un archivo. No tengo IE para probarlo, pero funciona para webkit.
Bruno Bronosky

278

Solución simple para navegadores listos para HTML5 ...

function download(filename, text) {
    var pom = document.createElement('a');
    pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
    pom.setAttribute('download', filename);

    if (document.createEvent) {
        var event = document.createEvent('MouseEvents');
        event.initEvent('click', true, true);
        pom.dispatchEvent(event);
    }
    else {
        pom.click();
    }
}

Uso

download('test.txt', 'Hello world!');

2
Si conoce la URL de origen que desea descargar, ¡esta es la mejor solución!
sidonaldson

31
La capacidad de establecer el nombre del archivo lo convierte en un ganador.
Omn

99
Este método funcionó en Chrome hasta la última actualización que recibí hace unos días (35.0.1916.114 m). Ahora funciona parcialmente porque el nombre de archivo y la extensión ya no funcionan (siempre nombra el archivo download.txt independientemente de lo que se haya pasado)
Sevin7

66
Tengo Chrom 42.0.2311.90, y esto funciona para mí con el nombre de archivo esperado.
Saurabh Kumar

44
Si hay un límite en la cantidad de datos que se pueden incluir?
Kaspar Lee

80

HTML5 definió un window.saveAs(blob, filename)método. No es compatible con ningún navegador en este momento. Pero hay una biblioteca de compatibilidad llamada FileSaver.js que agrega esta función a la mayoría de los navegadores modernos (incluido Internet Explorer 10+). Internet Explorer 10 admite un navigator.msSaveBlob(blob, filename)método ( MSDN ), que se utiliza en FileSaver.js para la compatibilidad con Internet Explorer.

Escribí una publicación de blog con más detalles sobre este problema.


1
¿Qué pasa con el bloqueo de ventanas emergentes? El comportamiento de esta biblioteca es similar a la solución de @ Nøk. El texto sin formato en Firefox se abre en una nueva. Solo Chrome intenta guardarlo, pero cambia la extensión (necesito un archivo de puntos sin extensión).
ciembor

1
@ciembor la variante de atributo de descarga (object url +) (que uso con Chrome) le permite establecer un nombre de archivo. A mí me funciona en cromo.
panzi

1
@ciembor aha y una ventana emergente no se bloquea si un clic lo causó directamente.
panzi

66
FileSaver.js ahora es compatible con IE
Eli Gray

15
El W3C dice: El trabajo en este documento ha sido descontinuado y no debe ser referenciado o utilizado como base para la implementación.
WaiKit Kung

48

Guardar archivos grandes

Los URI de datos largos pueden dar problemas de rendimiento en los navegadores. Otra opción para guardar archivos generados del lado del cliente es colocar su contenido en un objeto Blob (o Archivo) y crear un enlace de descarga usando URL.createObjectURL(blob). Esto devuelve una URL que puede usarse para recuperar el contenido del blob. El blob se almacena dentro del navegador hasta que URL.revokeObjectURL()se llame en la URL o se cierre el documento que lo creó. La mayoría de los navegadores web admiten URL de objetos , Opera Mini es el único que no los admite.

Forzando una descarga

Si los datos son texto o una imagen, el navegador puede abrir el archivo, en lugar de guardarlo en el disco. Para hacer que el archivo se descargue al hacer clic en el enlace, puede usar el downloadatributo. Sin embargo, no todos los navegadores web admiten el atributo de descarga . Otra opción es usarlo application/octet-streamcomo el tipo mime del archivo, pero esto hace que el archivo se presente como un blob binario que es especialmente amigable para el usuario si no puede o no puede especificar un nombre de archivo. Consulte también ' Forzar para abrir la ventana emergente "Guardar como ..." abierta en el enlace de texto, haga clic para acceder al pdf en HTML "

Especificando un nombre de archivo

Si el blob se crea con el constructor de archivos, también puede establecer un nombre de archivo, pero solo unos pocos navegadores web (incluidos Chrome y Firefox) tienen soporte para el constructor de archivos . El nombre de archivo también se puede especificar como argumento paradownload atributo, pero esto está sujeto a un montón de consideraciones de seguridad . Internet Explorer 10 y 11 proporciona su propio método, msSaveBlob , para especificar un nombre de archivo.

Código de ejemplo

var file;
var data = [];
data.push("This is a test\n");
data.push("Of creating a file\n");
data.push("In a browser\n");
var properties = {type: 'text/plain'}; // Specify the file's mime-type.
try {
  // Specify the filename using the File constructor, but ...
  file = new File(data, "file.txt", properties);
} catch (e) {
  // ... fall back to the Blob constructor if that isn't supported.
  file = new Blob(data, properties);
}
var url = URL.createObjectURL(file);
document.getElementById('link').href = url;
<a id="link" target="_blank" download="file.txt">Download</a>


1
¿Puedo mostrar un cuadro de diálogo (ventana emergente) para especificar una carpeta (directorio) para guardar el archivo?
Calvin

@ Calvin: He actualizado la respuesta para explicar cómo forzar una descarga y proporcionar un nombre de archivo. Para IE, creo que puede usar msSaveBlobpara abrir el cuadro de diálogo "Guardar como". Para otros navegadores, su única opción es elegir manualmente "Guardar como" en el menú contextual del enlace de descarga.
bcmpinc

1
@ Jek-fdrv: solo las Blob-urls funcionan en Safari. Safari no admite el atributo de descarga y el constructor de archivos, por lo que no puede forzar una descarga, lo que significa que el blob probablemente se abrirá en el navegador y no puede especificar un nombre de archivo. Para el ejemplo dado, aún debería poder descargar el archivo con Safari usando el menú contextual del enlace.
bcmpinc


Esta es una respuesta muy útil e informativa. Una cosa más que puede ayudar a personas como yo: después de configurar document.getElementById('link').href = url;, su código puede seguir adelante y disparar el enlace utilizando el .click()método del elemento .
LarsH

36
function download(content, filename, contentType)
{
    if(!contentType) contentType = 'application/octet-stream';
        var a = document.createElement('a');
        var blob = new Blob([content], {'type':contentType});
        a.href = window.URL.createObjectURL(blob);
        a.download = filename;
        a.click();
}

3
¿Cuál es el efecto de contentType? ¿Para qué se usa esto?
Uri

3
Este funciona bien incluso en el último Chrome, a diferencia de la respuesta de @ Matěj Pokorný. Gracias.
Alexander Amelkin el

2
Esto no funciona para mí en FF36 o IE11. Si lo reemplazo a.clickcon código usando document.createEvent()como Matěj Pokorný sugirió, funciona en FF pero no en IE. No he probado Chrome.
Peter Hull

25

Echar un vistazo a Doug Neiner Downloadify que es una interfaz basada en Flash JavaScript para hacer esto.

Downloadify es una pequeña biblioteca JavaScript + Flash que permite generar y guardar archivos sobre la marcha, en el navegador, sin interacción del servidor.


66
Para la mayoría de las personas, esta es probablemente la respuesta que necesitarán. Entonces, aunque no cumple con mis requisitos específicos (como se explicó anteriormente), lo marco como la respuesta aceptada.
Toji

2
@Toji ah, ya veo. ¿Quizás volver a preguntar y reformular debajo del HTML 5banner y etiquetar en consecuencia? Es probable que eso atraiga a los usuarios que conocen ese campo específico (supongo que todavía es una multitud relativamente pequeña en este momento). Estoy bastante seguro de que se puede hacer en HTML 5, pero no tengo idea de cómo.
Pekka

1
¿Se ha comprado / transferido el dominio downloadify.info Downloadify y, en caso afirmativo, hay una nueva ubicación? El presente sitio parece completamente ajeno a la respuesta dada.
Christos Hayward

17
Esto no responde a la pregunta titulada Usando HTML5 ...
Ixx

55
@Ixx bien para ser justos, eso se agregó después de que se publicó la respuesta. Aún así, tienes razón. La respuesta a continuación debe ser aceptada
Pekka

17

¡Solución simple!

<a download="My-FileName.txt" href="data:application/octet-stream,HELLO-WORLDDDDDDDD">Click here</a>

Funciona en todos los navegadores modernos.


Hola, ¿sabes cómo especificar el comportamiento del atributo "descargar" usando window.open u otra función de JavaScript?
yucer

2
@yucer No hay ningún atributo de descarga (ni ningún atributo para el caso) con window.open(). Es irrelevante Podrías usar este método y forzarlo .click(), pero mira el tiempo ya que a Firefox no le gusta si llamas .click()antes de dejar que el elemento se una al cuerpo.
Brad

Pero lamentablemente todos los espacios se eliminan. En mi caso, eso es realmente malo ya que quiero descargar el código fuente de un archivo SVG.
frankenapps

los espacios se conservan si usa encodeURIComponent (contenido)
Mike Redrobe

no puede elegir el nombre en Firefox, pero Chrome funciona
Bhikkhu Subhuti

10

Puede generar un URI de datos . Sin embargo, hay limitaciones específicas del navegador.


1
Esto es interesante. Lo investigaré más cuando tenga la oportunidad. ¡Gracias!
Toji

@quantumpotato, generar la URL es un poco complicado de explicar. Todo lo esencial está en RFC 2397 . Puede usar herramientas como esta para realizar pruebas. Luego, para su aplicación real, puede buscar un URI de datos o una biblioteca base 64 para su idioma. Si no lo encuentra, no dude en hacer una pregunta de seguimiento. Algunas de las limitaciones específicas del navegador se dan en el artículo de Wikipedia . Por ejemplo, IE limita la longitud y el tipo (por ejemplo, no text / html).
Matthew Flaschen el

Generar URL de datos no es tan complicado: "data:text/plain;charset=UTF-8,"+encodeURIComponent(text)pero sí, IE limita el tamaño de las URL de datos a una cantidad inutilizable y no las admite para cosas como window.open(...)o iframes (creo).
panzi

@panzi, tampoco es tan fácil como eso. Por un lado, ese no es el tipo MIME correcto para Collada o JSON.
Matthew Flaschen

Respuesta demasiado no informativa. por favor, agregue las especificaciones para los navegadores, si los menciona.
T.Todua

10

He usado FileSaver ( https://github.com/eligrey/FileSaver.js ) y funciona bien.
Por ejemplo, hice esta función para exportar los registros que se muestran en una página.
Tienes que pasar una matriz para la instanciación del Blob, así que quizás no escribí esto de la manera correcta, pero funciona para mí.
Por si acaso, tenga cuidado con el reemplazo: esta es la sintaxis para hacer esto global, de lo contrario solo reemplazará el primero que encuentre.

exportLogs : function(){
    var array = new Array();

    var str = $('#logs').html();
    array[0] = str.replace(/<br>/g, '\n\t');

    var blob = new Blob(array, {type: "text/plain;charset=utf-8"});
    saveAs(blob, "example.log");
}

2
FileSaver es excelente, aquí hay una cuña IE para la función pre-IE10 preIE10SaveAs: (nombre de archivo, contenido de archivo, tipo mime) {var w = window.open (); var doc = w.document; doc.open (mimetype, 'replace'); doc.charset = "utf-8"; doc.write (contenido del archivo); doc.close (); doc.execCommand ("SaveAs", nulo, nombre de archivo); }
aqm

Una advertencia sobre la cuña de @ aqm: ejecuta etiquetas de script.
GameFreak

También, puede ser deseable poner w.close();después de laexecCommand
GameFreak

8

Encontré dos enfoques simples que funcionan para mí. Primero, usando un aelemento ya hecho clic e inyectando los datos de descarga. Y segundo, generar un aelemento con los datos de descarga, ejecutarlos a.click()y eliminarlos nuevamente. Pero el segundo enfoque solo funciona si es invocado por una acción de clic del usuario también. (Algunos) El navegador se bloquea click()desde otros contextos, como al cargar o se activa después de un tiempo de espera (setTimeout).

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
    <script type="text/javascript">
      function linkDownload(a, filename, content) {
        contentType =  'data:application/octet-stream,';
        uriContent = contentType + encodeURIComponent(content);
        a.setAttribute('href', uriContent);
        a.setAttribute('download', filename);
      }
      function download(filename, content) {
        var a = document.createElement('a');
        linkDownload(a, filename, content);
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }
    </script>
   </head>
  <body>
    <a href="#" onclick="linkDownload(this, 'test.txt', 'Hello World!');">download</a>
    <button onclick="download('test.txt', 'Hello World!');">download</button>
  </body>
</html>

2
También puede insertar el adocumento (posiblemente con "display:none"), hacer clic en él y luego eliminarlo.
Teepeemm

1
¿esto funcionaría en navegadores donde el atributo de descarga no es compatible como incluso moderno, es decir, y safari .. caniuse.com/#feat=download
Muhammad Umer

1
Acabo de probar Safari 5.0 con vino. La primera versión funciona, la segunda no. También probé IE 8 (vino) y no funciona.
maikel

6

Aquí hay un enlace al método URI de datos que Mathew sugirió, funcionó en un safari, pero no bien porque no pude establecer el tipo de archivo, se guarda como "desconocido" y luego tengo que ir allí nuevamente más tarde y cambiarlo en orden para ver el archivo ...

http://www.nihilogic.dk/labs/canvas2image/


4

Puedes usar localStorage. Este es el equivalente Html5 de las cookies. Parece que funciona en Chrome y Firefox PERO en Firefox, necesitaba subirlo a un servidor. Es decir, las pruebas directamente en la computadora de mi casa no funcionaron.

Estoy trabajando en ejemplos HTML5. Vaya a http://faculty.purchase.edu/jeanine.meyer/html5/html5explain.html y desplácese hasta el laberinto. La información para reconstruir el laberinto se almacena utilizando localStorage.

Llegué a este artículo buscando JavaScript HTML5 para cargar y trabajar con archivos xml. ¿Es lo mismo que html y JavaScript anteriores?


4

tratar

let a = document.createElement('a');
a.href = "data:application/octet-stream,"+encodeURIComponent('"My DATA"');
a.download = 'myFile.json';
a.click(); // we not add 'a' to DOM so no need to remove

Si quieres descargar datos binarios mira aquí

Actualizar

2020.06.14 Actualizo Chrome a 83.0 y superior SO snippet stop funciona (debido a restricciones de seguridad de sandbox ) - pero la versión JSFiddle funciona - aquí


3

Como se mencionó anteriormente, la API de archivos , junto con las API FileWriter y FileSystem, se pueden usar para almacenar archivos en la máquina de un cliente desde el contexto de una pestaña / ventana del navegador.

Sin embargo, hay varias cosas relacionadas con las dos últimas API que debe tener en cuenta:

  • Las implementaciones de las API actualmente solo existen en los navegadores basados ​​en Chromium (Chrome & Opera)
  • Ambas API fueron retiradas de la pista de estándares del W3C el 24 de abril de 2014, y a partir de ahora son propietarias
  • La eliminación de las API (ahora propietarias) de la implementación de navegadores en el futuro es una posibilidad
  • Se utiliza una caja de arena (una ubicación en el disco fuera de la cual los archivos no pueden producir ningún efecto) para almacenar los archivos creados con las API
  • Se utiliza un sistema de archivos virtual (una estructura de directorio que no existe necesariamente en el disco en la misma forma que cuando se accede desde el navegador) representa los archivos creados con las API

Aquí hay ejemplos simples de cómo se usan las API, directa e indirectamente, en conjunto para hacer esto:

Productos horneados *

bakedGoods.get({
        data: ["testFile"],
        storageTypes: ["fileSystem"],
        options: {fileSystem:{storageType: Window.PERSISTENT}},
        complete: function(resultDataObj, byStorageTypeErrorObj){}
});

Uso de las API de File, FileWriter y FileSystem sin formato

function onQuotaRequestSuccess(grantedQuota)
{

    function saveFile(directoryEntry)
    {

        function createFileWriter(fileEntry)
        {

            function write(fileWriter)
            {
                var dataBlob = new Blob(["Hello world!"], {type: "text/plain"});
                fileWriter.write(dataBlob);              
            }

            fileEntry.createWriter(write);
        }

        directoryEntry.getFile(
            "testFile", 
            {create: true, exclusive: true},
            createFileWriter
        );
    }

    requestFileSystem(Window.PERSISTENT, grantedQuota, saveFile);
}

var desiredQuota = 1024 * 1024 * 1024;
var quotaManagementObj = navigator.webkitPersistentStorage;
quotaManagementObj.requestQuota(desiredQuota, onQuotaRequestSuccess);

Aunque las API FileSystem y FileWriter ya no están en el camino de los estándares, su uso puede justificarse en algunos casos, en mi opinión, porque:

  • El renovado interés de los proveedores de navegadores que no se implementan puede colocarlos nuevamente en él.
  • La penetración en el mercado de la implementación de navegadores (basados ​​en cromo) es alta
  • Google (el principal contribuyente a Chromium) no ha dado una fecha de finalización de vida a las API

Sin embargo, si "algunos casos" abarca los suyos, es decisión suya.

* BakedGoods es mantenido por nada menos que este tipo aquí :)


2

Este hilo fue invaluable para descubrir cómo generar un archivo binario y solicitar la descarga del archivo nombrado, todo en código de cliente sin servidor.

El primer paso para mí fue generar el blob binario a partir de los datos que estaba guardando. Hay muchas muestras para hacer esto para un solo tipo binario, en mi caso tengo un formato binario con múltiples tipos que puede pasar como una matriz para crear el blob.

saveAnimation: function() {

    var device = this.Device;
    var maxRow = ChromaAnimation.getMaxRow(device);
    var maxColumn = ChromaAnimation.getMaxColumn(device);
    var frames = this.Frames;
    var frameCount = frames.length;

    var writeArrays = [];


    var writeArray = new Uint32Array(1);
    var version = 1;
    writeArray[0] = version;
    writeArrays.push(writeArray.buffer);
    //console.log('version:', version);


    var writeArray = new Uint8Array(1);
    var deviceType = this.DeviceType;
    writeArray[0] = deviceType;
    writeArrays.push(writeArray.buffer);
    //console.log('deviceType:', deviceType);


    var writeArray = new Uint8Array(1);
    writeArray[0] = device;
    writeArrays.push(writeArray.buffer);
    //console.log('device:', device);


    var writeArray = new Uint32Array(1);
    writeArray[0] = frameCount;
    writeArrays.push(writeArray.buffer);
    //console.log('frameCount:', frameCount);

    for (var index = 0; index < frameCount; ++index) {

      var frame = frames[index];

      var writeArray = new Float32Array(1);
      var duration = frame.Duration;
      if (duration < 0.033) {
        duration = 0.033;
      }
      writeArray[0] = duration;
      writeArrays.push(writeArray.buffer);

      //console.log('Frame', index, 'duration', duration);

      var writeArray = new Uint32Array(maxRow * maxColumn);
      for (var i = 0; i < maxRow; ++i) {
        for (var j = 0; j < maxColumn; ++j) {
          var color = frame.Colors[i][j];
          writeArray[i * maxColumn + j] = color;
        }
      }
      writeArrays.push(writeArray.buffer);
    }

    var blob = new Blob(writeArrays, {type: 'application/octet-stream'});

    return blob;
}

El siguiente paso es hacer que el navegador solicite al usuario que descargue este blob con un nombre predefinido.

Todo lo que necesitaba era un enlace con nombre que agregué en HTML5 que podía reutilizar para cambiar el nombre del nombre de archivo inicial. Lo mantuve oculto ya que el enlace no necesita visualización.

<a id="lnkDownload" style="display: none" download="client.chroma" href="" target="_blank"></a>

El último paso es solicitar al usuario que descargue el archivo.

var data = animation.saveAnimation();
var uriContent = URL.createObjectURL(data);
var lnkDownload = document.getElementById('lnkDownload');
lnkDownload.download = 'theDefaultFileName.extension';
lnkDownload.href = uriContent;
lnkDownload.click();

1

Aquí hay un tutorial para exportar archivos como ZIP:

Antes de comenzar, hay una biblioteca para guardar archivos, el nombre de la biblioteca es fileSaver.js. Puede encontrar esta biblioteca aquí. Comencemos, ahora, incluya las bibliotecas requeridas:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js"  type="text/javascript"></script>
<script type="text/javascript" src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.js" ></script>

Ahora copie este código y este código descargará un archivo zip con un archivo hello.txt con contenido Hello World. Si todo funciona bien, esto descargará un archivo.

<script type="text/javascript">
    var zip = new JSZip();
    zip.file("Hello.txt", "Hello World\n");
    zip.generateAsync({type:"blob"})
    .then(function(content) {
        // see FileSaver.js
        saveAs(content, "file.zip");
    });
</script>

Esto descargará un archivo llamado file.zip. Puede leer más aquí: http://www.wapgee.com/story/248/guide-to-create-zip-files-using-javascript-by-using-jszip-library

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.