Weblogs Código

Blog Bitix

Las formas de guardar relaciones jerárquicas en bases de datos relacionales

noviembre 27, 2020 02:00

Para implementar relaciones jerárquicas en base de datos relacionales hay varias soluciones conocidas. En este artículo comento las más conocidas con sus desventajas y cual elegir en función de si la base de datos soporta consultas recursivas o en caso de que no las soporte.

PostgreSQL

Las bases de datos relacionales guardan los datos en tablas, filas y columnas. Los datos de unas tablas se relacionan con los de otras a través de las claves primarias y claves foráneas. Este modelo relacional permite guardar datos y es suficiente para muchos casos, habitualmente utilizando la tercera forma normal de las 6+2 formas normales de las bases de datos relacionales.

Sin embargo, el modelo relacional en principio no se ajusta a guardar relaciones jerárquicas, hay varias soluciones para implementar este tipo de relaciones en los datos en una base de datos relacional. Algunas soluciones aunque válidas en la teoría tiene importantes limitaciones en su uso práctico. Algunas solución depende de que la base de datos ofrezcan el soporte para implementarla en caso contrario hay que elegir otra opción.

En el libro SQL Antipatterns se trata con detalle las diferentes las formas de guardar relaciones jerárquicas en bases de datos relacionales entre otros diversos aspectos y buenas prácticas.

Contenido del artículo

Ejemplo de relación jerárquica

Las relaciones jerárquicas establecen unas normas en las relaciones de los elementos, los elementos se organizan en niveles en el que los elementos se relacionan como nivel por encima, por debajo o en el mismo nivel que otro. El número de niveles que tiene una estructura jerárquica es indefinido pudiendo tener cualquier profundidad.

Esta es una estructura pequeña jerárquica de productos con dos niveles de profundidad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Componentes (id: 1)
├── Placas base (id: 2)
│   ├── ATX (id: 3)
│   └── ITX (id: 4)
├── Procesadores (id: 5)
│   ├── Intel (id: 6)
│   └── AMD (id: 7)
├── Memoria RAM (id: 8)
├── Almacenamiento SSD (id: 9)
└── Tarjetas gráficas (id: 10)
    ├── NVIDIA (id: 11)
    └── AMD (id: 12)
Periféricos
├── Monitores
│   ├── FHD
│   ├── QHD
│   └── UHD
├── Teclados
│   ├── Mecánicos
│   └── Membrana
├── Ratones
└── Altavoces
hierarchy.txt

Formas de guardar relaciones jerárquicas

Adjacency List

La solución de listas adyacentes o adjacency list se basa en añadir un campo adicional a la tabla en el que cada fila guarda el identificador del elemento por encima en la estructura jerárquica.

Esta solución utiliza un modelo de datos sencillo, es fácil de añadir nuevos elementos a la estructura jerárquica además de mantener la integridad referencial.

1
2
select * from products_categories;

adjacency-list-data.sql
1
2
select * from products_categories;

adjacency-list-data.sql

Sus desventajas son que las consultas para obtener los ascendientes o descendientes de un elemento son complicadas e ineficientes además de no soportar cualesquiera niveles de profundidad.

Las consultas para obtener los ascendientes, los descendientes y de inserción de un nodo son las siguientes.

1
2
3
4
5
SELECT p1.*, p2.*, p3.*, p4.* FROM products_categories p1
    LEFT OUTER JOIN products_categories p2 ON p2.category_id = p1.parent_id
    LEFT OUTER JOIN products_categories p3 ON p3.category_id = p2.parent_id
    LEFT OUTER JOIN products_categories p4 ON p4.category_id = p3.parent_id
    WHERE p1.category_id = 7;
adjacency-list-ancestors.sql
1
2
3
4
 category_id | name | parent_id | category_id |     name     | parent_id | category_id |    name     | parent_id | category_id | name | parent_id 
-------------+------+-----------+-------------+--------------+-----------+-------------+-------------+-----------+-------------+------+-----------
           7 | AMD  |         5 |           5 | Procesadores |         1 |           1 | Componentes |           |             |      |          
(1 row)
adjacency-list-ancestors.out
1
2
3
4
5
SELECT p1.*, p2.*, p3.*, p4.* FROM products_categories p1
    LEFT OUTER JOIN products_categories p2 ON p2.parent_id = p1.category_id
    LEFT OUTER JOIN products_categories p3 ON p3.parent_id = p2.category_id
    LEFT OUTER JOIN products_categories p4 ON p4.parent_id = p3.category_id
    WHERE p1.category_id = 5;
adjacency-list-descendants.sql
1
2
3
4
 category_id | name | parent_id | category_id |     name     | parent_id | category_id |    name     | parent_id | category_id | name | parent_id 
-------------+------+-----------+-------------+--------------+-----------+-------------+-------------+-----------+-------------+------+-----------
           7 | AMD  |         5 |           5 | Procesadores |         1 |           1 | Componentes |           |             |      |          
(1 row)
adjacency-list-ancestors.out

Para este caso y con consultas recursivas el ejemplo se puede probar con Docker y la base de datos PostgreSQL.

1
2
3
4
5
6
7
$ docker run --name postgres -e POSTGRES_PASSWORD=password postgres
$ docker exec -it postgres /bin/bash
root@9e6463f25eaf:/# psql --username=postgres
psql (13.1 (Debian 13.1-1.pgdg100+1))
Type "help" for help.

postgres=#
docker-run.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE products_categories (
    category_id integer NOT NULL,
    name character varying(255) NOT NULL,
    parent_id integer
);

INSERT INTO products_categories (category_id, name, parent_id) VALUES
    (1, 'Componentes', null),
    (2, 'Placas base', 1),
    (3, 'ATX', 2),
    (4, 'ITX', 2),
    (5, 'Procesadores', 1),
    (6, 'Intel', 5),
    (7, 'AMD', 5),
    (8, 'Memoria RAM', 1),
    (9, 'Almacenamiento SSD', 1),
    (10, 'Tarjetas gráficas', 1),
    (11, 'NVIDIA', 10),
    (12, 'AMD', 10);
insert-data.sql

Consultas recursivas

Las otras implementaciones están originadas a la limitación del modelo relacional y del lenguaje SQL. En las últimas versiones de las bases de datos el lenguaje SQL soporta consultas recursivas o recursive queries con las que implementar la sencilla solución de adjacency list sin sus desventajas. MySQL soporta consultas recursivas desde la versión 8 y PostgreSQL ya era posible en la versión 9.

Las consultas para obtener los ascendientes, los descendientes y de inserción de un nodo son las siguientes.

1
2
3
4
5
6
7
WITH RECURSIVE ancestors AS (
    SELECT c.* FROM products_categories c WHERE c.category_id = 7
    UNION ALL
    SELECT c.* AS depth FROM ancestors a 
        INNER JOIN products_categories c ON a.parent_id = c.category_id
)
SELECT * FROM ancestors ORDER BY category_id;
recursive-queries-ancestors.sql
1
2
3
4
5
6
 category_id |     name     | parent_id
-------------+--------------+----------
           1 | Componentes  |          
           5 | Procesadores |         1
           7 | AMD          |         5
(3 rows)
recursive-queries-ancestors.out
1
2
3
4
5
6
WITH RECURSIVE descendants AS (
    SELECT p.* FROM products_categories p WHERE p.category_id = 5
    UNION ALL
    SELECT p.*FROM descendants d INNER JOIN products_categories p ON d.category_id = p.parent_id
) 
SELECT * FROM descendants p ORDER BY category_id;
recursive-queries-descendants.sql
1
2
3
4
5
6
 category_id |     name     | parent_id
-------------+--------------+----------
           5 | Procesadores |         1
           6 | Intel        |         5
           7 | AMD          |         5
(3 rows)
recursive-queries-descendants.out

Closure Table

Cuando la base de datos no soporta consultas recursivas una solución alternativa es la de closure table, esta se basa en guardar las relaciones entre los nodos en una tabla adicional, se guardan la relación de un nodo con todos sus descendientes o las de un nodo con todos sus ascendientes además de consigo mismo.

1
2
select * from products_categories;

closure-table-data-1.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
category_id | name
1           | Componentes
2           | Placas base
3           | ATX
4           | ITX
5           | Procesadores
6           | Intel 
7           | AMD
8           | Memoria RAM
9           | Almacenamiento SSD
10          | Tarjetas gráficas
11          | NVIDIA
12          | AMD
closure-table-data-1.out
1
2
select * from products_categories_hierachy;

closure-table-data-2.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ancestor_id | descendant_id
1           | 1
1           | 2
2           | 2
2           | 3
2           | 3
3           | 3
1           | 4
2           | 4
4           | 4
1           | 5
5           | 5
1           | 6
5           | 6
6           | 6
1           | 7
5           | 7
7           | 7
1           | 8
8           | 8
1           | 9
9           | 9
1           | 10
10          | 10
1           | 11
10          | 11
11          | 11
1           | 12
10          | 12
12          | 12
closure-table-data-2.out

Esquema de las relaciones entre los nodos

Esquema de las relaciones entre los nodos

En este caso las consultas de búsqueda son eficientes, las de inserción son sencillas y hay integridad referencial. Esta solución permite a la misma fila formar parte de varias estructuras jerárquicas al mismo tiempo. Su desventaja es que requiere una tabla adicional.

Las consultas para obtener los ascendientes, los descendientes son las siguientes.

1
2
3
SELECT p.* FROM products_categories AS p
    INNER JOIN products_categories_hierachy AS h ON p.product_id = h.ancestor_id
    WHERE h.descendant_id = 7;
closure-table-ancestors.sql
1
2
3
SELECT c.* FROM products_categories AS p
    INNER JOIN products_categories_hierachy AS h ON p.product_id = h.descendant_od
    WHERE h.ancestor_id = 1;
closure-table-descendants.sql

La de inserción.

1
2
3
4
5
6
7
INSERT INTO products_categories (category_id, name, parent_id) VALUES
    (1, 'ARM', null);
INSERT INTO products_categories_hierachy (ancestor, descendant)
    SELECT h.ancestor_id, 13 FROM products_categories_hierachy AS h
        WHERE h.descendant_id = 5
    UNION ALL
        SELECT 13, 13;
closure-table-insert.sql

Y para eliminar una rama del árbol.

1
2
3
4
5
DELETE FROM products_categories_hierachy
    WHERE descendant_id IN (SELECT descendant_id
        FROM products_categories_hierachy
        WHERE ancestor_id = 2
    );
closure-table-delete-subtree.sql

Otras implementaciones

Estas otras soluciones son válidas para implementar estructuras jerárquicas en bases de datos relacionales. aunque en algunos aspectos con desventajas sobre las recomendadas de consultas recursivas si están soportadas por la base de datos o la solución de closure table.

Path Enumeration

La solución path enumeration se basa en añadir una columna que contiene en una cadena la colección de identificadores de sus ascendientes incluido el propio nodo.

1
2
select * from products_categories;

path-enumeration-data.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
category_id | name               | path_enumeration
1           | Componentes        | 1,
2           | Placas base        | 1,2,
3           | ATX                | 1,2,3,
4           | ITX                | 1,2,4,
5           | Procesadores       | 1,5,
6           | Intel              | 1,5,6,
7           | AMD                | 1,5,7,
8           | Memoria RAM        | 1,8,
9           | Almacenamiento SSD | 1,9,
10          | Tarjetas gráficas  | 1,10,
11          | NVIDIA             | 1,10,11,
12          | AMD                | 1,10,12,
path-enumeration-data.out

Su desventaja es que al igual que la solución de nested set no ofrece integridad referencial en los identificadores incluidos en la cadena que guarda la jerarquía. Sin embargo, las consultas para obtener los ascendientes, descendientes y realizar una inserción son sencillas.

Las consultas para obtener los ascendientes y los descendientes.

1
2
SELECT * FROM products_categories AS p
    WHERE '1,5,7,' LIKE p.path || '%';
path-enumeration-ancestors.sql
1
2
SELECT * FROM products_categories AS p
    WHERE p.path LIKE '1,5,' || '%';
path-enumeration-descendants.sql

Nested Set

La solución de nested set es una solución a las limitaciones del modelo relacional para guardar relaciones jerárquicas. Se basa en añadir a la tabla dos nuevos campos con el rango de nodos contenidos en el nivel inferior.

Esta solución también tiene un modelo de datos sencillo, no requiere tablas adicionales, es fácil de añadir un nuevo nodo a la jerarquía y las consultas para buscar los ascendientes y descendientes son rápidas.

1
2
select * from products_categories;

nested-set-data.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
category_id | name               | left | right
1           | Componentes        | 1    | 25
2           | Placas base        | 2    | 7
3           | ATX                | 3    | 4
4           | ITX                | 5    | 6
5           | Procesadores       | 8    | 14
6           | Intel              | 10   | 11
7           | AMD                | 12   | 13
8           | Memoria RAM        | 15   | 16
9           | Almacenamiento SSD | 17   | 18
10          | Tarjetas gráficas  | 19   | 24
11          | NVIDIA             | 20   | 21
12          | AMD                | 22   | 23
nested-set-data.out

Sus desventajas es que no mantiene la integridad referencial. Es más adecuada cuando la estructura de datos no cambia con frecuencia ya que las inserciones y eliminaciones requieren actualizar los valores de los rangos de gran cantidad de nodos. Si se inserta un nuevo nodo en la categoría de procesadores, por ejemplo ARM, su left y right serán 14 y 15 y a todos los right mayor o igual que 14 hay que sumarle más 2.

Para hacer eficientes las consultas que busque nodos inmediatamente inferiores a un determinado nodo requiere añadir una una columna adicional para guardar el nivel del nodo.

Las consultas para obtener los ascendientes, los descendientes y de inserción de un nodo son las siguientes.

1
2
3
SELECT p2.* FROM products_categories AS p1
    INNER JOIN products_categories AS p2 ON p1.left BETWEEN p2.left AND p2.right
    WHERE p1.category_id = 7;
nested-set-ancestors.sql
1
2
3
SELECT p2.* FROM products_categories AS p1
    INNER JOIN products_categories_hierachy AS p2 ON p2.left BETWEEN p1.left AND p1.right
    WHERE p1.category_id = 5;
nested-set-descendants.sql

¿Cúal es la mejor solución para estructuras jerárquicas en bases de datos relacionales?

Si la base de datos soporta consultas recursivas la mejor solución para estructuras jerárquicas en bases de datos relacionales es la de consultas recursivas. Si la base de datos no soporta consultas recursivas la solución de closure table ofrece integridad referencial y es sencilla.

Nested set es adecuada solo sin la estructura de datos jerárquica no cambia con frecuencia y la de adjacency list sin consultas recursivas es adecuada si el rendimiento no es significativo para la aplicación y el nivel de profundidad máximo de la estructura es conocido y está limitado a unos pocos.

Esta es una pequeña comparación entre cada una de las soluciones en su dificultad de implementación y si soporta integridad referencial.

Comparación entre las diferentes soluciones

Comparación entre las diferentes soluciones

» Leer más, comentarios, etc...

info.xailer.com

Black Friday / Cyber Monday

noviembre 24, 2020 10:31

Buenos días,

Como todos los años, ya tenemos preparadas nuestras ofertas de Black Friday y Cyber Monday. Ofrecemos un 20% de descuento no acumulable con otras ofertas en todos nuestros productos. No dejéis de pasar esta oportunidad.

Un saludo

» Leer más, comentarios, etc...

Variable not found

12 novedades destacables de Blazor 5.0 (bueno, y algunas más)

noviembre 24, 2020 09:49

Blazor

Hace unos días fue lanzada la nueva versión de ASP.NET Core basada en el flamante .NET 5, que incluye un buen número de novedades para los desarrolladores Blazor, algunas de ellas bastante jugosas.

En este post vamos a ver por encima las que creo que son más destacables en esta nueva entrega:

  1. .NET 5 y mejoras de rendimiento
  2. CSS Isolation
  3. JavaScript Isolation
  4. Virtualización
  5. InputRadio & InputRadioGroup
  6. Soporte para IAsyncDisposable
  7. Soporte para upload de archivos
  8. Control del foco
  9. Parámetros de ruta catch-all
  10. Protected browser storage
  11. Prerenderización WebAssembly
  12. Lazy loading de áreas de aplicación en Web Assembly
  13. Otros cambios y mejoras
¡Vamos allá!

1. .NET 5 y mejoras de rendimiento

Mejoras de rendimiento en renderización de rejillas en Blazor WebAssemblyBlazor aprovecha ventajas introducidas en .NET 5, tanto en el servidor como en el navegador. Esto, acompañado de bastantes mejoras internas, hace que las aplicaciones construidas sobre este framework se ejecuten de forma mucho más rápida. Se habla incluso de llegar cuadruplicar el rendimiento en las renderizaciones, o duplicar o triplicar el rendimiento en Blazor WebAssembly.

Al mismo tiempo, hay mejoras en componentes como <Virtualize>, que comentaremos algo más adelante, pensados para optimizar escenarios específicos como la visualización de listas extensas.

2. CSS Isolation

Esta es una de las novedades más interesantes de esta nueva entrega, pues permite que cada componente defina sus propios estilos en archivos .css de forma aislada e independiente del resto, muy al estilo de lo que ya permiten otros frameworks SPA como Angular o React. De esta forma, conseguiremos tener archivos .css más pequeños, fáciles de leer y escribir, y centrados exclusivamente en un componente, el lugar del clásico y monstruoso site.css con los estilos de toda la aplicación.

En la práctica, lo único que tendremos que hacer para definir los estilos de un componente llamado MyComponent.razor es crear un archivo con su mismo nombre terminado en .css, como en MyComponent.razor.css. En él podremos introducir reglas de estilo que sólo se aplicarán al componente (aunque opcionalmente podemos indicar que también queremos que se apliquen a sus sub-componentes).

3. JavaScript Isolation

Se trata de otra característica interesante, que en esta ocasión nos ayudará a la hora de mejorar la organización del código JavaScript que usamos a través de los mecanismos de interoperación de Blazor.

La idea es que el código JS podremos ahora organizarlo en módulos estándar y exportar las funciones a utilizar desde Blazor, como en el siguiente ejemplo:

export function showPrompt(message, defaultValue) {
return prompt(message, defaultValue);
}

Luego, ya desde Blazor, podremos cargar de forma dinámica dicho módulo realizando una llamada a InvokeAsync() con el identificador "import" (una palabra reservada para estos efectos) y la ruta del archivo .js donde está definido. Tras ello, podremos llamar a sus funciones exportadas de la forma habitual:

@page "/test"
@inject IJSRuntime Js

<button @onclick="Click">Get name</button>
<p>Your name: @name</p>

@code {
string name = "Unknown";
IJSObjectReference module;

async Task Click()
{
name = await module.InvokeAsync<string>("prompt", "Your name?", "Peter");
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module = await Js.InvokeAsync<IJSObjectReference>(
"import", "./modules/MyPrompt.js"
);
}
}
}

4. Virtualización de listas

Como ocurre con otros frameworks similares, la renderización de listas extensas en el navegador (hablamos de miles de elementos) supone siempre un problema de rendimiento.

Con objeto de mejorar la experiencia de usuario, Blazor 5 proporciona el nuevo componente <Virtualize> que permite virtualizar una lista de elementos para que sólo sean renderizados aquellos que realmente son visibles en pantalla. Por ejemplo, si tenemos una tabla con un listado de 10.000 filas pero en el navegador sólo caben 20, sólo éstas serán renderizadas, y conforme el usuario vaya scrolleando se irán renderizando el resto bajo demanda.

Por ejemplo, sin usar virtualización, la siguiente página, que muestra 100.000 números, normalmente tardará varios segundos en renderizarse:

@page "/numbers"
<h3>Numbers</h3>
<ul>
@foreach(var n in numbers)
{
<li>@n</li>
}
</ul>

@code {
List<int> numbers = Enumerable.Range(1, 100_000).ToList();
}

Usar la virtualización es tan sencillo como sustituir el @foreach por el componente <Virtualize>, facilitándole la colección de elementos a mostrar. Este simple cambio hará que la carga de la lista sea virtualmente inmediata:

@page "/numbers"
<h3>Numbers</h3>
<ul>
<Virtualize Items="numbers">
<li>@context</li>
</Virtualize>
</ul>

@code {
List<int> numbers = Enumerable.Range(1, 100_000).ToList();
}

La virtualización va mucho más allá, y permite incluso obtener los elementos bajo demanda con objeto de mantener en memoria colecciones completas, de las cuales sólo serán mostradas algunos elementos.

5. Los componentes InputRadio y InputRadioGroup

En las versiones anteriores de Blazor, los controles de formulario tipo radio teníamos que implementarlos de forma manual, porque el framework no los incluía de serie. Con esta nueva entrega, podemos usar <InputRadioGroup> para agrupar <InputRadio> y bindear sus valores a un campo del viewmodel asociado al formulario:

<InputRadioGroup @bind-Value="Color">
<label><InputRadio Value=0 /> Red</label>
<label><InputRadio Value=1 /> Green</label>
<label><InputRadio Value=2 /> Blue</label>
</InputRadioGroup>

6. Soporte de IAsyncDisposable

Los componentes pueden ahora implementar IAsyncDisposable para liberar sus recursos de forma asíncrona.

@page "/"
@implements IAsyncDisposable
...
@code {
public ValueTask DisposeAsync()
{
// Dispose your resources here
}
}

7. Soporte para upload de archivos

Pues sí, sorprendentemente, la versión anterior de Blazor no incluía de serie mecanismos para facilitar la selección y subida de archivos al servidor, aunque era compensado por el paquete NuGet BlazorInputFile que el propio Steve Sanderson, creador de Blazor, había publicado para nosotros.

La nueva versión de Blazor ya incluye <InputFile>, un componente que actúa como wrapper del clásico <input type="file"> de HTML, pero que, además, proporciona mecanismos para acceder desde C# tanto a las propiedades como contenidos de los archivos seleccionados.

Un ejemplo de uso sencillo podría ser el siguiente:

@page "/Upload"
<InputFile OnChange="@OnInputFileChange" />
@if (file != null)
{
<p>
File: @file.Name, Size: @file.Size,
Last Modified: @file.LastModified
</p>
<button @onclick="UploadAsync">Upload</button>
}

@code {
IBrowserFile file;
Task OnInputFileChange(InputFileChangeEventArgs e)
{
file = e.File;
return Task.CompletedTask;
}
Task UploadAsync()
{
if(file == null) return;
using var stream = file.OpenReadStream();

// Hacer algo con el stream, como:
// - salvarlo a un archivo en disco (Blazor Server)
// - enviarlo a una API (Blazor WebAssembly)

file = null;
}
}

8. Control de posición del foco

La clase ElementReference, utilizada para referenciar elementos del DOM mediante la directiva @ref, dispone ahora del método FocusAsync(), que, como su nombre indica, sirve para pasar el foco al elemento indicado.

En el siguiente componente podemos ver un ejemplo de uso, en una interfaz en la que incluimos tres <input>, y un botón:

<h3>Focus Demo</h3>

<input />
<input />
<input @ref="third" />
<button @onclick='()=>third.FocusAsync()'>Set focus</button>

@code {
ElementReference third;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await third.FocusAsync();
}
}

Inicialmente, el foco se encontrará en el tercer cuadro de texto, porque así lo inicializamos en el OnAfterRenderAsync(). Aparte, la pulsación del botón hará que en cualquier momento el foco vuelva a dicho cuadro de texto.

9. Parámetros de ruta catch-all

El sistema de routing de Blazor soporta ahora el uso de parámetros de ruta catch-all, cuya misión es capturar todos los segmentos de la dirección URL desde la posición donde se encuentran hasta el final.

Por ejemplo, imaginad que tenemos una página definida con una ruta como la siguiente:

@page "/catalog/{*data}"
...
@code {
[Parameter] public string Data { get; set; }
}

Si accedemos a la página con una ruta como "/catalog/computers/laptop/lenovo", la propiedad Data contendrá "computers/laptops/lenovo". Esto puede ser interesante para casos en los que el esquema de rutas sea complejo o flexible, y queramos procesarlas nosotros de forma manual.

10. Protected browser storage

Se trata de un mecanismo exclusivo para Blazor Server, que permite guardar información de forma segura tanto en el local storage como en el session storage del navegador. De hecho, es muy similar al uso de estos mecanismos habituales del lado cliente, pero la diferencia es que la encriptación y desencriptación se realizan en el servidor, mientras el almacenamiento se realizará en el lado cliente.

Para usarlo, basta con instalar el paquete NuGet Microsoft.AspNetCore.Components.Web.Extensions, e inyectar los servicios ProtectedLocalStorage o ProtectedSessionStorate (dependiendo del almacenamiento que queramos usar, local o de sesión) y usarlos para leer o escribir valores:

@inject ProtectedLocalStorage ProtectedLocalStorage
...
private async Task IncrementCount()
{
await ProtectedLocalStorage.SetAsync("count", ++currentCount);
}

Seguro que os preguntaréis por qué no podemos utilizar esto en Blazor WebAssembly. Y la respuesta es sencilla: sería inseguro de todas formas. Al encriptarlo desde el lado servidor, las claves nunca viajan al lado cliente y por tanto podemos estar seguros de lo que está almacenado no puede ser manipulado; sin embargo, si todo el proceso se realiza en cliente, es imposible asegurar este extremo.

11. Prerenderización en Blazor WebAssembly

La prerenderización era un mecanismo interesante de Blazor Server para adelantar la llegada de contenidos al navegador gracias a una renderización inicial de los componentes Blazor en el servidor, en el momento de generar la página contenedora de la aplicación SPA, y antes de establecerse el circuito con SignalR. Además de dar a los usuarios una mayor sensación de velocidad de carga, era positivo desde el punto de vista del SEO.

La nueva versión de Blazor lo hace posible también en el hosting WebAssembly con un nuevo tipo de render-mode en el tag helper <component>:

<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

12. Lazy loading de áreas de aplicación en Web Assembly

Como sabemos, las aplicaciones Blazor WebAssembly deben descargarse al browser por completo antes de iniciar su ejecución; y no sólo hablamos del runtime de .NET y ensamblados de la BCL, sino también de los ensamblados de nuestra aplicación y sus dependencias (por ejemplo, los paquetes NuGet externos que utilicemos).

Para aliviar un poco este peso inicial, la nueva versión de Blazor permite cargar bajo demanda ensamblados con áreas de la aplicación, de forma que sólo serán descargados cuando vayan a ser utilizados. Una vez cargados, quedarán en memoria para usos posteriores y no será necesario descargarlos de nuevo.

A grandes rasgos, la idea consiste básicamente en:

  • Especificar qué ensamblados no deben descargarse inicialmente, al cargar la aplicación, introduciendo algunos elementos en el archivo .csproj.
  • Luego, tomar el control del sistema de routing en el momento en que se detecta un cambio de ruta para, si nos interesa, cargar el ensamblado que contenga la página de destino antes de mostrarla.

Podéis ver un ejemplo completo de uso en esta página de la documentación oficial.

13. Otros cambios y mejoras

Obviamente, en una major release como esta hay muchos más detalles, aunque ya son de menor impacto, como:

  • Eliminación de Internet Explorer 11 y Edge Legacy (es decir, el Edge anterior a saltar a Chromium) de la lista de browsers soportados, tanto en Blazor Server como Blazor WebAssembly.
  • Eliminación de espacios no significativos del marcado en tiempo de compilación.
  • Introducción de hot reload para agilizar el ciclo de desarrollo.
  • Posibilidad de especificar nombres de clases personalizadas para los estados de validación de controles de formulario.
  • Introducción de analizadores de compatibilidad de código para Blazor WebAssembly.
  • Introducción de IComponentActivator, para tomar el control en la activación (instanciación) de componentes.
  • Introducción del parámetro DisplayName en algunos controles de formulario.
  • Soporte para el evento ontoggle.

Y con esto, creo que hemos visto lo más destacable de esta nueva entrega, pero, ¡no dudéis en decírmelo si véis algo que consideréis importante y haya pasado por alto!

Publicado en Variable not found.

» Leer más, comentarios, etc...

Fixed Buffer

Cómo crear una imagen docker multiarquitectura desde cero

noviembre 24, 2020 09:00

Tiempo de lectura: 7 minutos
Imagen ornamental de docker

Estas últimas semanas han sido apasionantes, Kubecon, DotNetConf,… Un montón de novedades y nuevas versiones de diferentes softwares y herramientas de las que hablaremos en las próximas semanas. Hoy vengo a hablar de algo más ‘mundano’, como generar imágenes Docker. Solo que esta vez vamos a hacer que nuestra imagen sea multiarquitectura.

DISCLAIMER: La idea de esta entrada es hablar de cómo crear imágenes Docker multiarquitectura, por lo que no se entra en los detalles sobre crear imágenes Docker en si mismo, sino que se pasa por encima para dar el contexto.

¿Qué es y cómo funciona Docker?

Antes de empezar a hablar sobre imágenes multiarquitectura, conviene empezar aclarando que es Docker y como funciona. Docker es un sistema de empaquetado de contenedores que nos permite añadir aplicaciones y dependencias en un mismo ‘paquete’, de modo que corra donde corra, siempre funcionará igual. Podríamos decir que es como si cogiéramos una máquina virtual y la distribuyésemos. De este modo, podemos preparar el sistema operativo, sus dependencias y nuestra aplicación, generar un ‘paquete’ y ponerlo en marcha allá donde queramos.

Con esto mente, hay que aclarar que docker es como correr una maquina virtual, pero no es correr una máquina virtual. Esto quiere decir que, a diferencia de una máquina virtual, aquí no movemos todo el sistema operativo, sino simplemente las ‘particularidades’ que tiene, dejando la base común intacta ya que esta base común se comparte con el sistema operativo anfitrión.

La imagen muestra la composición de las aplicaciones en una VM y en Docker, donde en la VM el sistema huésped corre sobre el sistema anfitrión y sobre este huésped las aplicaciones mientras que en Docker las aplicaciones corren sobre el contenedor y este sobre el sistema anfitrión

A esta tecnología se la conoce como contenedores y no es algo nuevo, si no que lleva existiendo varios años ya. Su funcionamiento (simplificado) es que vamos a crear imágenes que con las dependencias y aplicaciones, y estas son las que vamos a distribuir. Precisamente del hecho de que la parte común se comparta con el sistema operativo anfitrión, es por lo que tenemos que hablar de imágenes Docker para una arquitectura concreta. Por poner un símil, diferentes distribuciones de Linux comparten la misma base común que es el kernel de Linux, diferentes imágenes comparten la misma arquitectura.

¿Por qué me puede interesar crear imágenes Docker multiarquitectura?

En las últimas entradas estuvimos hablando sobre módulos para IoT Edge y sobre aprovisionamiento de dispositivos con IoT Hub. Pues bien, en ese momento no entramos en detalle, aunque hicimos referencia a ello:

Actualmente, Docker soporta de forma experimental el crear manifiestos de imágenes para múltiples arquitecturas simultáneamente, siendo el runtime de Docker el que se descarga la imagen de su arquitectura especifica.

Creando módulos especializados para Azure IoT Edge

Pues efectivamente, este es un caso muy claro donde seguramente nos interese crear imágenes Docker multiarquitectura. Esto es porque es un escenario común que queramos crear módulos de IoT Edge que trabajen por ejemplo de igual manera si los desplegamos en un microprocesador amd64, arm32, arm64, …

Estos escenarios solían requerir de diferentes imágenes con una imagen base diferente, y cuyo uso requería de mantener un tag diferente para identificarlos. De este modo no es nada raro encontrarnos con que muchos repositorios de Docker Hub tienen soporte especifico mediante tags para diferentes arquitecturas.

Esto tiene un problema evidente y es que te obliga a especificar un tag concreto en el que indicamos la arquitectura, y dificulta mucho el generalizar en situaciones de despliegue masivo. ¿Realmente necesito saber la arquitectura del dispositivo IoT Edge para ejecutar el un módulo? ¿Realmente es algo que quiera tener en cuenta?

En Python sin ir más lejos, su imagen soporta hasta 10 arquitecturas distintas:

En cambio, todas ellas se sirven desde el mismo tag, es decir, están agrupadas y es el propio cliente de Docker el que se baja la imagen concreta que necesita para el caso concreto.

Creando nuestra primera imagen Docker multiarquitectura (Docker Manifest)

Vamos a partir plantear un ejemplo muy sencillo. Partimos de un escenario donde todas las arquitecturas que queremos soportar ya están soportadas por la imagen base que utilicemos.

Lo primero que vamos a necesitar, es habilitar el soporte para ‘Docker Manifest‘. Esta es una característica experimental de Docker por lo que para habilitarlas debemos (extraído y traducido de la documentación oficial):

Para habilitar las características experimentales en el Docker CLI, edite el archivo config.json y ponga experimental a habilitado.
Para habilitar las características experimentales en el menú del escritorio de la base Docker, haz clic en Configuración (Preferencias en macOS) > Línea de comandos y luego activa la opción Habilitar características experimentales. Haz clic en Aplicar y reiniciar.

Una vez teniendo eso listo, vamos a crear las diferentes imágenes que queremos que se sirvan desde nuestra ‘meta imagen’ multiarquitectura. Por ejemplo, podríamos tener un par de Dockerfiles como estos (en el repositorio de Github puedes encontrar más arquitecturas):

# Dockerfile.amd64
FROM amd64/alpine
CMD echo "Hello world desde amd64/alpine"

# Dockerfile.arm32v6
FROM arm32v6/alpine
CMD echo "Hello world desde arm32v6/alpine"

Una vez que tenemos estos dos Dockerfile, vamos a generar sus respectivas imágenes y subirlas a Docker Hub con los comandos:

# Generamos las imágenes
docker build -f .\Dockerfile.amd64 -t fixedbuffer/multi-arch:amd64  .
docker build -f .\Dockerfile.arm32v6 -t fixedbuffer/multi-arch:arm32v6  .

# Pusheamos las imágenes a Docker Hub
docker push fixedbuffer/multi-arch:amd64
docker push fixedbuffer/multi-arch:arm32v6

Una vez que hemos subido las imágenes, vamos a crear nuestro manifiesto que genere esa anhelada imagen Docker multiarquitectura. Para ello simplemente vamos a ejecutar el comando ‘docker manifest create‘ indicandole el nombre del manifiesto y su tag (que será el nombre de esa ‘meta imagen’) y las imágenes que queramos que la compongan:

docker manifest create fixedbuffer/multi-arch:latest fixedbuffer/multi-arch:amd64 fixedbuffer/multi-arch:arm32v6

En caso de que queramos añadir nuevas imágenes, basta con ejecutar de nuevo el comando con el modificador ‘–amend‘. Una vez que hemos terminado de crear nuestro manifiesto, basta con subirlo a Docker Hub ejecutando:

docker manifest push docker.io/fixedbuffer/multi-arch:latest

Si ahora vamos a Docker Hub, vamos a encontrarnos con que nuestro repositorio tiene una nueva imagen multiarquitectura son la misma etiqueta que nuestro manifiesto. Esta nueva imagen soporta varias arquitecturas:

Si ahora ejecutamos la imagen en diferentes equipos, vamos a poder comprobar como dependiendo del sistema, se utiliza de manera transparente una u otra imagen:

La imagen muestra el comando docker run desde una raspberry donde la salida es Hello world desde arm32v6/alpine
La imagen muestra el comando docker run desde un equipo Windows con Docker for Descktop donde la salida es Hello world desde amd64/alpine

Creando nuestra segunda imagen Docker multiarquitectura (Docker Buildx)

Si bien es cierto que esto funciona y funciona bien, se queda un poco a medias en el sentido de que es necesario generar las diferentes imágenes previamente a hacer el manifiesto. Esto significa que vamos a necesitar tener hardware que pueda ejecutar los Dockerfiles y subirlos a Docker Hub.

Otra opción, que personalmente me gusta más por ser muy sencilla de automatizar es 'docker buildx. Este comando nos va a permitir definir las diferentes arquitecturas que queremos que soporte nuestra imagen multiarquitectura, y será suficiente con que recibamos ese argumento en nuestro Dockerfile. Por ejemplo:

ARG ARCH=
FROM ${ARCH}debian:buster-slim

CMD echo "Hello world desde ${ARCH}debian:buster-slim"

Este Dockerfile simplemente recibe como parámetro el tipo de arquitectura que tiene que utilizar, el cual obtendrá desde la propia ejecución del comando:

docker buildx build --push --tag fixedbuffer/multi-arch:action --platform linux/amd64,linux/arm/v7,linux/arm64 -f Dockerfile.multi .

El comando ‘docker buildx build‘ soporta los mismos parámetros que soporta ‘docker build‘ de manera normal, pero además nos va a permitir utilizar algunos adicionales como –push para subir la imagen al terminar, o –platform para indicarle las diferentes plataformas que queremos soportar.

Una de las grandes ventajas que le veo respecto a Docker Manifest, es que es muy fácil de automatizar, como se puede comprobar en la entrada del propio blog de Docker (en inglés): Multi-arch build and images, the simple way – Docker Blog

Conclusión

En esta entrada hemos planteado un par de soluciones para poder generar imágenes Docker multiarquitectura, de modo que podamos utilizar la misma ‘meta imagen’ en todos nuestros dispositivos, sin tener que preocuparnos de cuál sea la arquitectura de dicho dispositivo.

Si bien es cierto que no es un escenario muy habitual, ya que normalmente las aplicaciones tienen una plataforma objetivo, no está de más conocer este tipo de posibilidades ya que como a mí, pueden sacarte de un apuro cuando generalizas las imágenes Docker.

**La entrada Cómo crear una imagen docker multiarquitectura desde cero se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Bitácora de Javier Gutiérrez Chamorro (Guti)

Simulador de Casio F-91W en Javascript (2.00)

noviembre 24, 2020 07:06



Comencé con mi Simulador de Casio F-91W en HTML y Javascript en 2013 y lo actualicé durante 2014. Como ocurre a menudo, lo dejé abandonado. Durante 2016 o 2017 apliqué algunas mejoras pero estaban sin terminar, así que lo dejé sin publicar. Hoy, en 2020, he sacado un par de horas libres para acabar de …

Simulador de Casio F-91W en Javascript (2.00) Leer más »



Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

» Leer más, comentarios, etc...

Coding Potions

Cómo formatear y parsear fechas en Javascript con dayjs

noviembre 24, 2020 12:00

Introducción

El parseo de fechas en javascript es una tarea recurrente en todo proyecto front. Da igual el proyecto, casi siempre tienes que pasear fecha. Lo primero que hay que hacer es mirar si merece la pena descargar una librería para parsear fechas. Si la respuesta es que sí, entonces seguramente hayas contemplado instalar la librería de moment.js ya que es una de las más conocidas.

Pues bien, dayjs es exactamente igual que moment.js, con la misma sintaxis y todo, solo que dayjs pesa mucho menos. A día de cuando estoy escribiendo este artículo, moment.js pesa 67,9Kb, mientras que el archivo minificado de dayjs solo ocupa 2Kb.

Como he dicho, la sintaxis de ambas librerías es la misma, incluso ambas tienen los mismos nombres de funciones. Esto es porque la gente que hace dayjs tiene una serie de tests que comprueban que la librería funciona exactamente igual que la otra. Tanto es así que la documentación de momentjs te sirve perfectamente para days ya que como he dicho todos los nombres de funciones coinciden.

Veamos cómo se usa y se instala dayjs.

Instalación

Para instalar dayjs tienes muchas posibilidades, como en cualquier librería de javascript.

Lo más rápido es utilizar el CDN para no tener que descargar la librería en el proyecto. Para ello tan solo tienes que meter esta línea en el head del HTML de tu proyecto:

<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.9.4/dayjs.min.js" integrity="sha512-XZSHSEFj4QeE0G4pwy4tToyAhF2VXoEcF9CP0t1PSZMP2XHhEEB9PjM9knsdzcEKbi6GRMazdt8tJadz0JTKIQ==" crossorigin="anonymous"></script>

Esto es recomendable para proyectos sencillos o de pruebas, lo que yo recomiendo es descargar el archivo js de la librería para guardarlo en el propio proyecto. Para ello, abre está URL:

https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.9.4/dayjs.min.js

💾 Botón derecho > “Guardar cómo” y lo guardas en el proyecto que estés desarrollando.

Luego tan solo tienes que referenciar el archivo que acabas de descargar en el head del HTML:

<script src="ruta/al/arvchivo/dayjs.min.js"></script>

La última forma de usar dayjs es usándola como una librería más de NPM. Para ello, si estás en un proyecto con dependencias NPM ejecuta:

npm install dayjs

Si lo has instalado con npm install, para usar la librería tienes que importar dayjs en los archivos javascript:

import dayjs from "dayjs";

Por cierto, si quieres una guía más exhaustiva de cómo se usa NPM te recomiendo mi artículo:

https://codingpotions.com/npm-tutorial

Primeros pasos con dayjs

Day js como hemos dicho, tiene la misma sintaxis que moment.js por lo que si antes ya habías usado esta librería no tienes que aprender nada nuevo. Tan solo tienes que cambiar moment.loquesea() por dayjs.loquesea(), eso es todo.

Si nunca has usado dayjs no te preocupes porque en este artículo voy a tratar de explicarte cómo funciona.

Lo primero que tienes que saber es que dayjs tiene una función llamada dayjs que es la base de esta librería. Esa función acepta un parámetro que será la fecha a parsear, y lo que va a hacer es devolver un objeto de tipo dayjs que nos servirá para luego poder formatear, transformar, etc las fechas. Veamos un ejemplo:

console.log(dayjs("2020-10-09"));

Si ejecutas el código de arriba verás que en la consola se pinta algo parecido a esto:

Se ve en consola un objeto javascript con muchas propiedades

Este es el objeto dayjs del que te hablaba, y lo que ves en la consola son las propiedades y funciones que puedes ejecutar sobre ese objeto. Pero antes de eso veamos primero las opciones que tenemos para crear este objeto dayjs del que hablamos.

Parseo de fechas con dayjs

A la función base de dayjs con el mismo nombre, podemos pasar strings que sean fechas en cualquier formato (incluso fechas nativas de javascript). Por ejemplo todas estas fechas son fechas válidas:

dayjs("2020-10-09");
dayjs("2020/10/09");
dayjs("30-June 2008");
dayjs(1603730600); // Formato epoch
dayjs(new Date());

Incluso podemos no pasar fecha a la función de dayjs. En ese caso dayjs creará el objeto con la fecha actual, es decir, que la última línea de los ejemplos anteriores se puede cambiar por:

dayjs();

Ojo cuidado porque las fechas se pasan en formato con el año delante, es decir esta fecha es inválida:

dayjs("02-12-1995");

Más adelante veremos cómo se pasan las fechas en formato español.

También es importante que conozcas que:

dayjs(undefined);

También devuelve la fecha actual pero:

dayjs(null);

Devuelve el objeto dayjs pero como si se tratara de una fecha inválida.

Si estás seguro del formato que tienen las fechas que pasas a dayjs lo mejor es pasar como segundo parámetro el formato de las fechas. Esto te puede servir por ejemplo si manejas fechas desde una API, para validar cuando una fecha que te pasan no tiene el formato correcto. Por ejemplo también te puede servir para validar que un usuario escribe en un campo de texto la fecha en el formato que toca:

dayjs("12-25-1995", "MM-DD-YYYY") // Fecha valida
dayjs("12-25-1995", "MM-DD-YYYY") // Fecha inválida

Por último, tienes que saber que puedes llamar a la función dayjs con este objeto:

dayjs({ year :2010, month :3, day :5, hour :15, minute :10, second :3, millisecond :123});

Ese es el objeto completo pero puedes pasar solo alguna de las propiedades, por ejemplo para crear una fecha actual con cierta hora y minutos:

dayjs({ hour:15, minute:10 });

GET y SET de parámetros de las fechas

Ahora que sabemos crear el objeto de dayjs ya podemos usarlo para conseguir partes de las fechas. Lo primero que tienes que saber es que las funciones para devolver ciertas partes de las fechas tienen el mismo nombre que las funciones para setear parámetros. Es decir, si llamas a la función para recuperar la hora que tiene una determinada fecha, por ejemplo, si no pasas parámetro te devolverá el dato y si llamas a la función y pasas un número entonces estarás seteando la fecha para que tenga esa hora.

La API de momentjs y dayjs es muy intuitiva y puedes deducir fácilmente el nombre de todas las funciones dependiendo de lo que quieras recuperar o modificar.

GET y SET para el tiempo y la hora

Empecemos por las cosas que tienen que ver con la hora y el tiempo de las fechas.

Para empezar si quieres recuperar o modificar los segundos que tiene una fecha simplemente tienes que hacer:

dayjs().second() // Devuelve los segundos que tenga la fecha
dayjs().second(1) // Modifica la fecha para que tenga los segundos puestos a 1

Para los ejemplos voy a crear el objeto dayjs sin parámetro para que sea más simple, pero puedes crear este objeto con el parseo de fechas que hemos visto antes.

Para los minutos es más de lo mismo:

dayjs().minute() // Devuelve los minutos de la fecha
dayjs().minute(59) // Setea los minutos

En este caso los minutos se pasan y se devuelven entre 0 y 59.

Y para sopresa de nadie, para las horas es igual también:

dayjs().hour() // Devuelve las horas de la fecha
dayjs().hour(8) // Setea las horas a la fecha

Las horas van en formato digital, es decir, entre 0 y 23. En el ejemplo de arriba, en la segunda línea, obtendremos la fecha actual pero a las 8 de la mañana.

GET y SET para la fecha

Para las cosas que tienen que ver con la fecha, es decir, número del día, número de mes, año, etc es más de lo mismo.

Para el día del mes, es decir, de 0 a 31 (dependiendo del mes) es así:

dayjs().date() // Devuelve el día del mes
dayjs().date(1) // Setea lel día del mes

Ten cuidado con este método porque como se llama date puedes pensar que es para algo que tiene que ver con toda la fecha, pero no es así, es solo para obtener y cambiar el día del mes.

Para el día de la semana (desde 0 a 6) es así:

dayjs().day() // Devuelve el día de la semana
dayjs().day(0) // Setea el día de la semana

Lo mismo para el número del mes (entre 0 y 11)

dayjs("2018-06-27").month() // Devuelve 05
dayjs().month(0) // Setea la fecha actual pero con el mes puesto en Enero

Por último, puedes conseguir el año de una fecha, o cambiarlo

dayjs("2018-06-27").year() // Devuelve 2018
dayjs().year(2018) // Setea la fecha actual pero con el año a 2018

Como ves toda la sintaxis es muy sencilla de recordar, eso hace que esta librería sea muy cómoda de usar porque no tienes que consultar la documentación todo el rato.

Operaciones con fechas

Bien, de momento todo lo que hemos visto tampoco ahorra mucho código. Realmente con las fechas nativas de javscript new Date ya podemos hacer este tipo de operaciones de recuperar valores de las fechas y modificarlos. A partir de aquí vienen cosas más interesantes.

En esta sección vamos a ver una cuantas operaciones que podemos hacer con el objeto de dayjs que creamos siempre.

Sumar y restar partes de la fecha

Empecemos por lo básico, añadir partes y restar partes de la fecha.

Para sumar partes de las fechas existe la función add:

dayjs().add(5, 'day')

Así de sencillo, recibe dos parámetros, el primero es el número a añadir y el segundo parámetro es la parte de la fecha que quieres sumar. En este ejemplo lo que se hace es añadir 5 días a la fecha actual. El resultado de esta operación es un objeto dayjs con ese parámetro modificado.

Los parámetros que puedes modificar son los siguientes: second, minute, hour, day, week, month, y year. Los mismos parémtros con los mismos nombres que vimos en el apartado anterior de los GET y SET.

Restar tiene la misma sintaxis con los mismos parámetros pero la función tiene el nombre de subtract. Ejemplos de sumas y restas:

dayjs().subtract(5, 'day')
dayjs().add(2, 'week')
dayjs().subtract(10, 'year')
dayjs().add(3, 'hour')
dayjs().subtract(6, 'minute')

Poner un parámetro de la fecha al inicio o al final

Hay otras dos opciones muy interesantes, startOf y endOf. Ambas funciones reciben un parémetro en el que tienes que pasar el campo de la fecha que quieres poner al inicio o al final. El parámetro a pasar tiene los mismos nombres que los que hemos visto hasta ahora: second, minute, hour, day, week, month, y year.

Imagina que tienes una fecha y quieres modifcar el mes de esa fecha para que esté al inicio del año, es decir, 1 de enero a las 00:00 de la fecha que pases. Con startOf sería así:

dayjs().startOf('year')

También puedes hacer esto con las funciones SET que hemos visto antes, pero tendrías que cambiar el mes, día, hora y minutos. Con esta función tienes todo en una línea.

Este ejemplo en concreto no es demasiado útil, pero por ejemplo si quieres poner una fecha para que el día sea el de inicio de semana es tan fácil como:

dayjs().startOf('week')

Así no tienes que calcular en qué día cae el Lunes de esa semana o cosas de esas.

La función de endOf es más de lo mismo, solo que pone la fecha al último parámetro que haya. Por ejemplo, fecha actual pero poniendo el día al último del año:

dayjs().endOf('year')

Esta función de endOf es mucho maś útil ya que te evitas tener que andar calculando el último día del mes, semana, etc.

IMPORTANTE. Se me ha olvidado comentar que como estos métodos devuelve un objeto dayjs con los parámetros modificados, una ventaja muy grande es que se pueden concatenar distintas llamadas en una sola línea, Ejemplo:

dayjs('2017-03-23').add(1, 'day').subtract(1, 'year').year(1996)

Formateo de fechas

Cómo formatear fechas con javascript

Aquí viene una de las cosas que más me gusta de este tipo de librerías, el formateo de las fechas, es decir, pasar una fecha a string con el formato que queramos.

Todos hemos creado alguna vez este tipo de funciones en javascript vanilla para formatear fechas:

function formatDate(date) {
  const parsedDate = new Date(date);
  return parsedDate.getYear() + " " + parsedDate.getMonth() + " " + parsedDate.getDate();
}

Todo esto con dayjs se resuelve con un único método llamado format al que tienes que pasar en forma de string el formato que quieres. Pongo un ejemplo y ahora lo comentamos:

dayjs('2019-01-25').format('DD/MM/YYYY'); // '25/01/2019'

Como ves, el método format se aplica también sobre el objeto dayjs con la fecha que quieres. Dentro del format se pasa en forma de string el formato que quieres, eso es todo. Veamos ahora los strings que puedes pasar.

Untitled

En el ejemplo de arriba he usado DD para poner el día del mes en dos dígitos, MM para el número del mes en dos dígitos y YYYY para el año completo, todo ello separado por barras.

Si quisiera el mismo formato pero separado por guiones tan solo tendría que hacer:

dayjs('2019-01-25').format('DD-MM-YYYY'); // '25-01-2019'

O si quiero la fecha separada por espacios:

dayjs('2019-01-25').format('DD MM YYYY'); // '25 01 2019'

O separada con espacios y el nombre del mes completo:

dayjs('2019-01-25').format('DD MMMM YYYY'); // '25 January 2019'

Más tarde en este mismo artículo te enseño a poner el nombre del mes en español.

Comprobaciones con fechas

Otra cosa muy útil que ofrece esta librería es la posibilidad de comprobar cosas con las fechas. Como pasa con todos los ejemplos que hemos visto hasta ahora, las fechas siempre se manejan con el objeto que creas con el constructor de dayjs. No me voy a parar en cada ejemplo para no hacer el artículo demasiado largo, creo que los ejemplos se entienden bien sin tener que explicarlos.

Comprobar si una fecha es anterior a otra

dayjs().isBefore(dayjs('2011-01-01'))

Comprobar si una fecha es posterior a otra

dayjs().isAfter(dayjs('2011-01-01'))

Comprobar si dos fechas son iguales

dayjs().isSame(dayjs('2011-01-01'))

Fechas en español 🇪🇸

Por defecto dayjs viene con fechas en inglés, si quieres poner las fechas en español tan solo tienes que cargar un archivo nuevo javascript, aparte del de dayjs.

Si instalaste dayjs mediante cdn, añade debajo del de dayjs la siguiente línea:

<script src="https://unpkg.com/dayjs@1.9.4/locale/es.js"></script>

Si, lo que hiciste fue bajarte el archivo javascript de dayjs en el proyecto, de igual forma descarga el archivo. Abre esta URL y dale botón derecho > guardar cómo y descárgatelo en tu proyecto. Como hiciste antes, añade en el head la ruta a este archivo:

<script src="ruta/al/arvchivo/es.js"></script>

Si por el contrario usas npm tan solo tienes que importar el arhivo con las traducciones de la propia librería de dayjs:

import es from 'dayjs/locale/es'

Activando la traducción

Por último, para activar las traducciones tan solo tienes que llamar a la función locale de dayjs con el nombre del locale, en este caso tienes que poner “es”.

dayjs.locale("es");

Con esto activado, cuando hagas format de una fecha, podrás mostrar el nombre de los meses y de los días en español. Ejemplo:

dayjs('2019-01-25').format('DD MMMM YYYY'); // '25 Enero 2019'

Plugins

Además de toda esta funcionalidad que hemos visto, dayjs viene con un set de plugins que añaden todavía más. pero hay que activar los que se quieran usar. Voy a poner una lista de los que considero más útiles y un enlace a la documentación por si lo quieres activar.

  • advancedFormat: Añade más tipos de formatos para las fechas, por ejemplo para números ordinales (1st, 2nd…), el cuatrimestre el año, etc.
  • customParseFormat: Si el parseo de fechas que viene con dayjs se te queda corto, con este plugin puedes definir tus propios parseaos para cuando tienes fechas strings que tienen un formato raro.
  • dayOfYear: Para devolver o modificar el número del día de todo el año, es decir, desde 1 a 365.
  • isBetween: Uno de los más útiles, para saber si una fecha está contenida dentro de otras dos fechas, es decir, puedes saber si una fecha está dentro de un rango de fechas.
  • isToday: Para saber si una fecha es hoy, Sin este método tendrías que comparar día, mes y año.
  • isTomorrow: Para saber si una fecha es mañana.
  • isYesterday: Para saber si una fecha es ayer.
  • quarterOfYear: Devuelve el número de cuatrimestre del año, de 1 a 4. Además añade la posibilidad de poder poner una fecha a comienzos de cuatrimestre o a final de cuatrimestre.
  • relativeTime: Añade 4 métodos, from, to, fromNow y toNow y como dice su nombre devuelve la diferencia de tiempo entre dos fechas o entre una fecha y la fecha actual. Ejemplo: hace 3 horas
  • utc: Como su nombre indica, añade soporte para fechas en formato UTC.

Conclusiones

Eso es todo respecto a dayjs. Como ves, es una librería muy completa (igual que momentjs) pero si me tengo que quedar con una me quedo con esta, más que nada porque ocupa mucho menos espacio y está más actualizada.

Yo personalmente es una de las pocas librerías que uso en todos mis proyectos front (siempre que tengo que tratar con fechas).

Te invito a que eches un vistazo a su documentación oficial ya que hay cosas que las he comentado por encima para no hacer esto más largo.

Por último decirte, que eches un vistazo a este artículo de @midudev ya que tiene un artículo muy interesante que hace lo mismo que el plugin relative pero sin dependencias, en javascript vanilla:

https://midu.dev/como-crear-un-time-ago-sin-dependencias-intl-relativeformat/

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 423

noviembre 23, 2020 07:05

Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET / Blazor

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

» Leer más, comentarios, etc...

Blog Bitix

5 formas de implementar el patrón Singleton en Java

noviembre 21, 2020 10:00

El patrón Singleton es un patrón de diseño muy utilizado, este patrón garantiza que de una clase solo haya una única instancia. En Java hay varias formas de implementar el patrón, algunas son más sencillas otras no son thread-safe o con evaluación lazy.

Java

Los lenguajes orientados a objetos modelan los programas utilizando clases, estas clases contienen tanto los datos como las operaciones que tratan esos datos que solo pueden ser modificados a través de esas operaciones. Este principio de encapsulación tiene como beneficio garantizar que los datos no sean modificados por cualquier otro código distinto a las operaciones y que los datos no queden en un estado inconsistente.

En un lenguaje orientado a objetos de cada clase se crean tantas instancias u objetos como se necesiten en el programa. En algunos casos de una clase en concreto sólo se desea que haya una única instancia, normalmente hacer que de una clase en concreto haya una única instancia se implementa con el patrón Singleton.

En Java hay varias formas de implementar el patrón Singleton, aunque todas son posibles cada una tiene diferentes propiedades, algunas tiene más beneficios sin las debilidades de otras. En este artículo incluyo 5 formas de cómo implementar el patrón Singleton con un ejemplo de código de cada una de ellas e indico cuales de ellas son las mejores opciones de entre estas.

Contenido del artículo

Qué es el patrón Singleton

El patrón Singleton es un patrón de diseño que restringe a una única instancia de objeto el número instancias de una clase que se pueden crear durante la ejecución del programa. Es útil en ciertas clases donde su única instancia es compartida en todo el código como si de una variable global se tratase. Si el método que permite obtener la referencia a la instancia es un método estático la instancia es como si fuese una variable global dado que es posible acceder a ella desde cualquier parte del programa siempre que lo permitan los modificadores de acceso.

Los casos de uso de las clases singleton son mantener un estado compartido, permitir inicialización bajo demanda o lazy, también es usado en otros patrones como el patrón abstract factory, factory method, el patrón builder o facade.

Formas de implementar el patrón Singleton en Java

En Java hay varias formas de implementar el patrón Singleton en una clase, el objetivo y resultado es el mismo pero la implementación hace que tengan algunas diferencias como que no sea thread-safe, no sea lazy, otras implementaciones igualmente sencillas son thread-safe y lazy al mismo tiempo.

Esta son las implementaciones más comunes, dependiendo de las necesidades de la aplicación todas son válidas simplemente la implementación con la clase inner y enum son las que proporcionan thread-safe y evaluación lazy que algunas de las otras no tienen.

Forma tradicional

La forma tradicional de implementar el patrón Singleton es utilizando una variable estática privada para guardar la referencia de la única instancia, hacer el constructor privado de modo que el resto de clases no tengan la posibilidad de crear más instancias y un método que crea la instancia si no ha sido creada con anterioridad con una sentencia condicional y devuelve la referencia si ya existe la instancia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...

public class Singleton {

   private static final Singleton instance;

   private Singleton() {}

   public static Singleton getInstance() {
       if (instance == null) {
           instance = new Singleton();
       }
       return instance;
   }
}
Singleton-1.java

Esta implementación es muy utilizada, aunque es lazy ya que la instancia no se crea hasta que se realiza la primera solicitud su inconveniente es que no es thread-safe si varios hilos intentan obtener una instancia cuando aún no hay ninguna creada.

Método sincronizado

Para implementar el patrón Singleton con la propiedad de que sea thread-safe en el caso anterior la forma más sencilla es hacer el método synchronized de modo que Java garantiza que si varios hilos intentan obtener la referencia de la instancia cuando aún no está creada sólo uno de ellos la cree.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...

public class Singleton {

   private static final Singleton instance;

   private Singleton() {}

   public synchronized static Singleton getInstance() {
       if (instance == null) {
           instance = new Singleton();
       }
       return instance;
   }
}
Singleton-2.java

El inconveniente de esta implementación con synchronized hace que el método para obtener la instancia sea más lento debido a la propia sincronización y a la contención que se produce si hay múltiples hilos en ejecución que hacen uso del método, si el rendimiento es una consideración importante hay otras formas de implementar el patrón Singleton no mucho más difíciles que tienen mejor rendimiento.

Variable estática final

La siguiente forma de implementar el patrón Singleton es crear la instancia en la inicialización de la clase ya sea como una asignación en la variable o con el constructor estático.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...

public class Singleton {

   private static final Singleton instance = new Singleton();

   private Singleton() {}

   public static Singleton getInstance() {
       return instance;
   }
}
Singleton-3a.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
...

public class Singleton {

   private static final Singleton instance;

   static {
       instance = new Singleton();
   }

   private Singleton() {}

   public static Singleton getInstance() {
       return instance;
   }
}
Singleton-3b.java

Esta implementación no requiere utilizar una sentencia condicional if para comprobar si la instancia ya ha sido creada y es thread-safe, sin embargo, no es lazy ya que la instancia se crea cuando se inicializa la clase antes de que se haga el primer uso.

Clase inner

Esta implementación es thread-safe y tiene evaluación lazy que algunas de las anteriores no tienen. Lo que hace es utilizar una clase anidada o inner para mantener la referencia a la instancia de la clase que la contiene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...

class Singleton {

   private static class SingletonHolder {
      public static Singleton instance = new Singleton();
   }

   private Singleton() {}

   public static Singleton getInstance() {
       return SingletonHolder.instance;
   }
}
Singleton-4.java

Este caso tampoco hace uso de la sentencia condicional if y el propio lenguaje Java por la inicialización de las clases y propiedades static garantiza que es thread-safe.

Clases enum

Las clases enum son por definición clases cuyos enumerados son singleton y thread-safe. La particularidad de los enum es que no se pueden crear nuevas instancias adicionales a las que se definan en el código en tiempo de compilación, el número de instancias es constante, por lo demás los enum al igual que las clases pueden implementar interfaces, declarar métodos y constructores de uso en el propio enum.

1
2
3
4
5
6
7
8
...

public enum Singleton {

    INSTANCE;

    ...
}
Singleton-5.java

Conclusión

Si el Singleton debe extender una clase la mejor opción de crearlo es con la clase inner, la implementación con una clase enum también es válida. Estas son las dos implementaciones aconsejadas para implementar el patrón Singleton. La forma tradicional sigue siendo muy utilizada con números ejemplos de código en artículos y es posible utilizarla, sin embargo, dadas sus desventajas es preferible utilizar alguna de las dos aconsejadas que son igual de sencillas de implementar.

» Leer más, comentarios, etc...

Blog Bitix

Autenticación con OpenID/OAuth en cualquier web con Nginx y de forma nativa con Spring Boot

noviembre 20, 2020 03:00

La autenticación permite identificar a los usuarios en una aplicación, en muchas es una necesidad para no permitir accesos no autorizados a la información que proporcionan o realizar las acciones que ofrecen. Las aplicaciones heredadas o legacy en ocasiones no es posible modificarlas y cuando una organización tiene varias aplicaciones gestionar los usuarios en cada una de ellas de forma individual se convierte en un problema. OpenID Connect proporciona la autenticación en el protocolo OAuth 2, con este protocolo las aplicaciones pueden delegar la autenticación a un proveedor de autenticación y ser este la que identifique a los usuarios y los gestione de forma forma centralizada además de proporcionar un inicio de sesión único o single-sing-on a varias aplicaciones. El servidor web Nginx tiene proxys que permiten añadir autenticación OAuth 2 a cualquier servicio web y las aplicaciones de Spring Boot pueden implementarlo de forma nativa.

OAuth

Java

La funcionalidad de autenticación y autorización es básica en la mayoría de aplicaciones web y servicios REST. La forma de implementar la autenticación y autorización es posible de varias formas. Sin embargo, con los métodos anteriores las aplicaciones gestionan sus propios usuarios y cada aplicación ha de implementar la autenticación y autorización. Otras formas comunes de autenticación son emplear autenticación básica en el servidor web, autenticación mutua entre el servidor y el cliente o con una implementación específica en la que una parte importante es guardar las contraseña de forma segura con salted-password-hashing.

Gestionar los usuarios de en cada una aplicación se convierte en un problema cuando el número de aplicaciones y servicios son varios, ya que a lo largo del tiempo hay que dar de alta a los usuarios nuevos y de baja a los usuarios que ya no deben acceder a la aplicación por ejemplo porque ya no forman parte de una organización. Por otro, lado hay aplicaciones heredadas o legacy que no se pueden modificar para añadirles la autenticación que necesitan.

Para administrar de forma centralizada los usuarios o credenciales de las aplicaciones y que el sistema de autenticación y autorización sea uno compartido para cualesquiera usuarios y aplicaciones una forma de implementarlo es usando un proveedor que implemente el estándar OpenID Connect. Para las aplicaciones heredadas la opción es añadir un proxy que proteja la aplicación con autenticación y en el caso de una aplicación Java con Spring Boot el proyecto de Spring Security permite añadir autenticación fácilmente para cualquier proveedor de OpenID.

Contenido del artículo

Qué es OpenID Connect

OpenID Connect es una capa de identidad que funciona sobre el protocolo OAuth 2.0. Permite a los clientes verificar la identidad del usuario basándose en la autenticación realizada por el servidor de autorización, así como obtener información básica del perfil del usuario. El protocolo funciona con principios similares a REST lo que lo hace interoperable con cualquier sistema.

Son varios los proveedores que ofrecen autenticación con sus cuentas, algunos de ellos son Google, GitHub, Azure Active Directory, AWS Cognito u Okta. Para implementar el servicio de autenticación y autorización OAuth gestionando sin depender de esas otras organizaciones está Keycloak.

Autenticación OpenID/OAuth con Nginx

Para añadir autenticación OpenId Connect en una aplicación web se suele configurar con el servidor web actuando de proxy y un proxy de OAuth. La función del servidor web y el proxy es requerir que el usuario esté autenticado en el proveedor de autenticación. De este modo entre el usuario y la página web están el servidor web, el intermediario de OAuth y el proveedor de autenticación, el esquema es el siguiente.

Modos de funcionamiento de oauth2-proxy (con y sin Nginx)

Modos de funcionamiento de oauth2-proxy (con y sin Nginx)

Con el servidor web Nginx dos intermediarios o proxys que proporcionan autenticación OpenID Connect son oauth2-proxy y vouch-proxy. Tanto oauth2-proxy y vouch-proxy son dos servicios que le indican a Nginx si el usuario está autenticado usando la directiva auth_request de Nginx. Estos proxys simplemente devuelven como respuesta un código de estado 202 Accepted o 401 Unauthorized para indicarle a Nginx si hay que realizar la autenticación, las otras directivas de configuración son para establecer cabeceras con las que es posible entre otras cosas indicarle a la aplicación web final cual es el usuario autenticado. En caso de que haya que autenticar al usuario el proxy de OAuth redirige al proveedor de autenticación.

En el caso de oauth2-proxy la configuración consiste en hacer de proxy para la aplicación web en la ubicación / y requerir autenticación con el endpoint /oauth2/ y /oauth2/auth que hace de proxy para oauth2-proxy. Buena parte de esa configuración de proxy son el tratamiento de las cabeceras con las que se obtiene el nombre de usuario autenticado y correo electrónico.

Este es la configuración para Nginx.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    resolver 127.0.0.1;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    sendfile on;
    keepalive_timeout 65;

    include /etc/nginx/conf.d/*.conf;
}
nginx.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
server {
    listen 80;
    server_name nginx.127.0.0.1.xip.io;
    root /var/www/html/;

    location /oauth2/ {
        proxy_pass http://oauth2:4180;

        proxy_set_header Host                    $host;
        proxy_set_header X-Real-IP               $remote_addr;
        proxy_set_header X-Scheme                $scheme;
        proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
    }
    
    location = /oauth2/auth {
        proxy_pass       http://oauth2:4180;

        proxy_pass_request_body           off;
        proxy_set_header Content-Length   "";

        proxy_set_header Host             $host;
        proxy_set_header X-Real-IP        $remote_addr;
        proxy_set_header X-Scheme         $scheme;
        proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
    }

    location / {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/start;

        auth_request_set $user   $upstream_http_x_auth_request_user;
        auth_request_set $email  $upstream_http_x_auth_request_email;
        auth_request_set $token  $upstream_http_x_auth_request_access_token;
        auth_request_set $auth_cookie $upstream_http_set_cookie;
        auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
        
        proxy_set_header X-User  $user;
        proxy_set_header X-Email $email;
        proxy_set_header X-Access-Token $token;

        add_header Set-Cookie $auth_cookie;

        if ($auth_cookie ~* "(; .*)") {
            set $auth_cookie_name_0 $auth_cookie;
            set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
        }

        if ($auth_cookie_name_upstream_1) {
            add_header Set-Cookie $auth_cookie_name_0;
            add_header Set-Cookie $auth_cookie_name_1;
        }

        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}
nginx.127.0.0.1.xip.io.conf

El servicio de proxy OAuth debe ubicarse en un subdominio de la página a autenticar ya que para esto se utilizan cookies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
server {
    listen 80;
    server_name oauth2.nginx.127.0.0.1.xip.io;

    location /oauth2/callback {
        proxy_pass http://oauth2:4180;

        proxy_set_header Host                    $host;
        proxy_set_header X-Real-IP               $remote_addr;
        proxy_set_header X-Scheme                $scheme;
        proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;        
    }
    
    location / {
        return 302 http://nginx.127.0.0.1.xip.io;
    }
}
oauth2.nginx.127.0.0.1.xip.io.conf

Tanto oauth2-proxy como vouch-proxy tienen imágenes de Docker para su fácil uso sin necesidad de instalar nada en la máquina local salvo el propio Docker (imagen docker oauth2-proxy, imagen docker vouch-proxy). Ambos proxys requieren cierta configuración indicada en la documentación del archivo de configuración, las propiedades son desde el puerto en el que escucha el servicio del proxy las peticiones desde Nginx, configuración de TLS, proveedor de autenticación, dirección de redirección después de la autenticación, configuración de logging, por supuesto el client-id y el client-secret obtenidos del proveedor de autenticación, los correos electrónicos considerados como válidos y configuración de la cookie que mantiene la autenticación. También permite guardar la información de la sesión en Redis en vez de en una cookie.

La configuración del oauth-proxy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
http_address = "0.0.0.0:4180"

redirect_url = "http://oauth2.nginx.127.0.0.1.xip.io/oauth2/callback"

skip_provider_button = true
pass_host_header = true
pass_access_token = true
pass_authorization_header = true

upstreams = ["http://nginx.127.0.0.1.xip.io"]

provider = "google"
client_id = "949347437228-c2rqkjknbfq10jak1ugccfffocaut7vp.apps.googleusercontent.com"
client_secret = "AJUHi_m8bqCWI_FY9aHGLLcu"

authenticated_emails_file = "/etc/oauth2-proxy-authenticated-emails.cfg"

cookie_name = "_oauth2_proxy"
cookie_secret = "83L36igQrdxAWRxfajQXnzj8WMo9jNKe"
cookie_samesite = "lax"
cookie_secure = false
oauth2-proxy.cfg

Y en este caso las direcciones de correos electrónicas o usuarios permitidos en la aplicación.

1
2
pico.dev@gmail.com

oauth2-proxy-authenticated-emails.cfg

Al acceder a la página en el dominio nginx.127.0.0.1.xip.io se solicita el inicio de sesión o selección de cuenta.

Autenticación con cuenta de Google Autenticación con cuenta de Google

Autenticación con cuenta de Google

Una vez autenticado el usuario se permite el acceso a la página web, en este caso la página por defecto de Nginx, también se observa la creación de la cookie que mantiene la sesión.

Página web y cookie de sesión Página web y cookie de sesión

Página web y cookie de sesión

Al implementar el ejemplo me he encontrado con dos mensajes de error, OAuth2: unable to obtain CSRF cookie y http: named cookie not present. Para resolver el primero es necesario indicar el parámetro de configuración cookie-domain que en el momento de realizar el ejemplo solo me ha sido posible indicándolo a través de la línea de comandos no en el archivo de configuración y para resolver el segundo es necesario que el host del servicio proxy OAuth esté en un subdominio del dominio de la página web.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
version: '3'
services:
  oauth2:
    image: bitnami/oauth2-proxy:latest
    volumes:
      - "./oauth2-proxy.cfg:/etc/oauth2-proxy.cfg"
      - "./oauth2-proxy-authenticated-emails.cfg:/etc/oauth2-proxy-authenticated-emails.cfg"
    ports:
      - "4180:4180"
    command: oauth2-proxy --config=/etc/oauth2-proxy.cfg --cookie-domain="nginx.127.0.0.1.xip.io"

  nginx:
    image: nginx
    volumes:
      - "./nginx.conf:/etc/nginx/nginx.conf:ro"
      - "./oauth2.nginx.127.0.0.1.xip.io.conf:/etc/nginx/conf.d/oauth2.nginx.127.0.0.1.xip.io.conf:ro"
      - "./nginx.127.0.0.1.xip.io.conf:/etc/nginx/conf.d/nginx.127.0.0.1.xip.io.conf:ro"
    ports:
      - "80:80"
docker-compose.yml

Autenticación OpenID/OAuth con Apache

En el caso del servidor web Apache HTTPD la solución que he encontrado es usar el módulo mod_auth_openidc.

Autenticación OpenID en una aplicación Spring Boot

Las aplicaciones de Java que usan Spring Boot a través de la dependencia de Spring Security que soporta OpenID Connect con el que añadir soporte a la aplicación fácilmente con un proveedor de autenticación. En este ejemplo se usa Google como proveedor de autenticación , Keycloak es otro proveedor de autenticación OAuth para autenticar un servicio REST.

El siguiente ejemplo es un servicio de Spring Boot accedido desde el navegador, en caso de que el cliente fuese otra aplicación o servicio hay que obtener un token con el que poder invocar este servicio.

Primero hay que incluir en el proyecto la dependencia del cliente OAuth2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
plugins {
    id 'java'
    id 'application'
}

group = 'io.github.picodotdev.blogbitix.springoauth'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:2.4.0"))

    implementation('org.springframework.boot:spring-boot-starter')
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.springframework.boot:spring-boot-starter-oauth2-client')

    runtimeOnly('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtimeOnly('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}

application {
    mainClass = 'io.github.picodotdev.blogbitix.springoauth.Main'
}
build.gradle

Añadir la configuración en la aplicación para incluir el identificativo del cliente y el secreto del servicio de autenticación que permite validar la autenticación.

1
2
3
4
5
6
7
8
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 949347437228-c2rqkjknbfq10jak1ugccfffocaut7vp.apps.googleusercontent.com
            client-secret: AJUHi_m8bqCWI_FY9aHGLLcu
application.yml

La información del usuario autenticado se puede obtener con la anotación @AuthenticationPrincipal o mediante la clase SecurityContextHolder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package io.github.picodotdev.blogbitix.springoauth;

import java.util.Collections;
import java.util.Map;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;

@RestController
public class MainController {

    public MainController() {
    }

    @GetMapping("/")
    public String hello(@AuthenticationPrincipal OidcUser principal) {
        return String.format("Hello %s (%s)", principal.getUserInfo().getGivenName(), principal.getUserInfo().getEmail());
    }

    @GetMapping("/principal")
    public OidcUser getPrincipal(@AuthenticationPrincipal OidcUser principal) {
        return principal;
    }

    @GetMapping("/claims")
    public Map<String, Object> getClaims() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.getPrincipal() instanceof OidcUser) {
            OidcUser principal = ((OidcUser) authentication.getPrincipal());
            return principal.getClaims();
        }
        return Collections.emptyMap();
    }
}
MainController.java

Lo último necesario es configurar qué rutas necesitan autenticación con OpenID Connect, en caso de que el usuario no esté autenticado se realiza la redirección al proveedor de autenticación. Con la configuración indicada se requiere autenticación para acceder a todas las rutas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.github.picodotdev.blogbitix.springoauth;

import java.util.HashSet;
import java.util.Set;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;

@Configuration
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");
 
        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);
 
        http
          .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
          .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint().oidcUserService(googleUserService));
    }
}
OAuth2LoginSecurityConfig.java

Al acceder a una de las URLs del cliente se solicita la autenticación con una cuenta de Google, una vez autenticado se accede a la página que en este caso obtiene la información del usuario como el nombre y correo electrónico.

Aplicación de Spring autenticada con una cuenta de Google

Aplicación de Spring autenticada con una cuenta de Google

Configuración de autenticación con cuentas de Google

En el ejemplo de este artículo muestro la autenticación con Google como proveedor de autenticación Oauth 2 pero perfectamente podría ser otro como Keycloak, en todos básicamente se trata de obtener las credenciales client-id y client-secret que permiten validar la autenticación tanto en estos casos Nginx como la aplicación de Spring Boot. Con una cuenta de Google y desde la consola para desarrolladores en el apartado credenciales es posible generar las credenciales para la autenticación de usuarios en una aplicación. Estas credenciales son los mencionados client-id y client-secret.

Los pasos para obtener las credenciales para una aplicación con autenticación OAuth con cuentas de Google son:

  • Crear un proyecto
  • Configurar la pantalla de consentimiento. En este paso se pide configurar la pantalla de consentimiento, la pantalla de consentimiento se muestra al usuario cuando realicen la autenticación entre la información está que aplicación solicita el consentimiento, opcionalmente es posible añadir un logotipo o enlaces de términos de uso.
  • Crear las credenciales del cliente. Aquí se configura las URL de retorno válidas cuando el usuario se haya autenticado. Si no se ha configurado la pantalla de consentimiento se solicita como paso previo a este.

Las credenciales tienen unos valores similares a, estas se indican tanto en la configuración de oauth-proxy como de la aplicación de Spring Boot:

  • client-id: 949347437228-m2c85v7bkmo1qb90vso702j27j4tccvr.apps.googleusercontent.com
  • client-secret: szgiplYPZR-pGgHP9MiJ-6-q

Al crear las credenciales para el cliente se indican las URL de retorno permitidas a las que se retorna al proxy o a la aplicación después del inicio de sesión.

Pasos para la creación de credenciales en Google para la autenticación OAuth

Pasos para la creación de credenciales en Google para la autenticación OAuth
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
docker-compose up

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run

» Leer más, comentarios, etc...

info.xailer.com

Nuevos controles visuales en Xailer 7

noviembre 18, 2020 07:18

En los próximos artículos vamos a repasar todos los nuevos controles que incluye el futuro Xailer 7. Existen nuevos controles que son simplemente versiones más vistosas de controles ya existentes, pero adaptados al estilo de Windows. Motivo por el cual todos estos nuevos controles se califican como ‘modernos’ y tiene una pestaña propia en el propio IDE. Sería el caso de los controles:

  • TButtonMod: Botón
  • TEditMod: Edit (Ya existente en Xailer 6, pero se ha adaptado para su funcionamiento en modo oscuro)
  • TMemoMod: Memo (Ya existente en Xailer 6, pero se ha adaptado para su funcionamiento en modo oscuro)
  • TCheckBoxMod: Checkbox o casilla de verificación
  • TRadioMod: Botón tipo Radio
  • TProgressBarMod: Barra de progreso
  • TTrackBarMod: Barra de seguimiento
  • TSwitch: Interruptor (Ya existente en Xailer 6, pero se ha adaptado para su funcionamiento en modo oscuro)
  • TGroupBoxMod: Cuadro de grupo
  • TDateEditMod: Edit especializado en la edición de fechas, utilizando el nuevo control de TCalendarMod
  • THeaderMod: Control para manejar cabeceras de browses o barras de botones

Además de estos controles se han incorporado algunos nuevos que o son completamente nuevos o por su gran mejora, merece la pena dedicarles un artículo a cada uno de ellos, como son:

  • TWebView: Control tipo browser basado en Microsoft Edge con integración total con sus aplicaciones.
  • TListBoxMod: Control tipo listbox con todo lo que pueda necesitar
  • TComboBoxMod: Control tipo ComboBox
  • TBtnPanelMod: Panel contenedor con efecto botón
  • TProgressCircle: Barra de progreso circular
  • TCalendarMod: Control tipo calendario
  • TTreeViewMod: Control tipo Treeview muy potente y completamente configurable
  • TCircularImage: Control tipo imagen con enmarcado circular
  • TBrowseMod: Control tipo browse que mejora sustancialmente el actual browse.

Todos estos nuevos controles que son susceptibles de tener su equivalente como DataControl, también han sido creados para el nuevo Xailer 7. Por lo tanto y como pueden imaginar, Xailer 7 es sin duda el mayor esfuerzo que se ha realizado en una actualización de Xailer desde Xailer 2.

Otra característica fundamental de los nuevos controles es que admiten el uso de etiquetas básicas tipo HTML en las propiedades de tipo cadena, como por ejemplo la propiedad cText del TButtonMod.

Comenzamos con el control TBtnPanelMod. Este nuevo control de Xailer 7 ofrece una gran funcionalidad que consiste en poder establecer que un contenedor de controles se comporte como un botón en su conjunto.

Esto nos permite crear botones con una mayor funcionalidad que la que teníamos hasta ahora. Seguro que ya habrá descubierto que son ampliamente utilizados en Windows 10.

Espero que les guste. Pronto más controles. Estén atentos. Gracias.

» Leer más, comentarios, etc...

Variable not found

Error 404 cargando páginas Blazor Server con parámetros que contienen un punto

noviembre 17, 2020 07:05

Blazor

Un alumno del curso de Blazor que tutorizo en CampusMVP me exponía problema bastante curioso con el que se había topado al implementar una página que, simplificando el escenario, venía a ser como la siguiente:

@page "/sum/{a:decimal}/{b:decimal}"

<h1>The sum is: @(A+B)</h1>

@code {
[Parameter] public decimal A { get; set; }
[Parameter] public decimal B { get; set; }
}

Esta página funcionaba correctamente con números enteros: podíamos acceder a "/sum/1/2" y veíamos el resultado correcto ("The sum is: 3"). Esto era así tanto accediendo a través del sistema de routing de Blazor (es decir, usando un link hacia esa ruta desde dentro de la aplicación) como accediendo directamente a la página introduciendo la ruta en la barra de direcciones del navegador.

Sin embargo, el problema surgía al usar valores decimales. Si desde dentro de la aplicación pulsábamos un enlace hacia "/sum/1.1/2.2", el resultado obtenido era correcto ("The sum is: 3.3"). Sin embargo, si en ese momento refrescábamos esa página, o bien accedíamos directamente a ella introduciendo la URL en el navegador, el servidor retornaba un error 404.

Extraño, ¿no?

Acotando el problema

Tras reproducir el escenario, vi claro que el problema no estaba en el sistema de routing de Blazor, pues los accesos a la página usando links funcionaban correctamente.

El error 404 lo estaba retornando el servidor cuando cargábamos la página por completo, es decir, refrescando o accediendo directamente a la URL con el navegador. Como consecuencia, el origen no estaba en Blazor sino más abajo, en ASP.NET Core.

Y aunque podría parecer lo contrario en un principio, el tema no tenía que ver con decimales o algo parecido, sino con la propia ruta que estamos usando en la petición. Es decir, el problema era exactamente el mismo con el siguiente componente si hacíamos una petición a "/hello/john.smith":

@page "/hello/{name}"

<h1>Hello: @Name</h1>
@code {
[Parameter] public string Name { get; set;}
}

Es decir, este comportamiento ocurre cuando el último fragmento de la ruta de acceso a la página contiene un punto ".", como en "1.3" o "john.smith".

Esto podemos comprobarlo si añadimos a la ruta cualquier fragmento adicional, de forma que el valor con punto no sea el último fragmento, como en el siguiente ejemplo; en este caso, peticiones como "/john.smith/hello" funcionará sin problemas:

@page "/{name}/hello"

¿Por qué ocurre esto? Y sobre todo, ¿cómo lo solucionamos?

Pues básicamente porque si la ruta de acceso termina por un fragmento que contiene un punto, el framework piensa que se trata de una petición a un archivo estático, y, según la configuración por defecto, no ejecuta el fallback hacia _Host.cshtml.

El culpable se encuentra en la llamada a MapFallbackToPage() que encontramos en el método Configure() de la clase Startup:

app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});

En principio, esa línea mapearía un fallback para que cualquier ruta que no haya sido configurada en otro endpoint sea procesada por la página Pages/_Host.cshtml. El problema es que internamente esa línea equivale a la siguiente:

endpoints.MapFallbackToPage("{*path:nonfile}", "/_Host");

Como se puede ver, el fallback mapea una ruta catch-all para asociarla a la página _Host, pero le añade la restricción nonfile, lo que indica que no se tendrán en cuenta peticiones que aparentemente vayan dirigidas a un recurso estático.

De esta forma, cualquiera de las peticiones que hacíamos más arriba, como "/sum/1.1/2.2" o "/hello/john.smith" no superaban la restricción y, por tanto, no se hacía el fallback hacia la página host de la aplicación Blazor. Además, dado que no existía otro endpoint para dichas URLs, la única opción para el sistema era retornar un 404, indicando que el recurso no había podido ser localizado.

Para solucionarlo, simplemente debemos sustituir el mapeo del fallback, eliminando la restricción nonfile de la siguiente forma:

app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
// endpoints.MapFallbackToPage("/_Host");
endpoints.MapFallbackToPage("{*path}", "/_Host");
});

¡Espero que os sea de ayuda!

Publicado en Variable not found.

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 421

noviembre 16, 2020 07:05

Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada, con jugosas novedades procedentes de la .NET Conf 2020. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET / Blazor

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

    Web / HTML / CSS / Javascript

    Visual Studio / Complementos / Herramientas

    Xamarin

    Otros

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    Blog Bitix

    Crear un archivo Zip con Java, comprimir y descomprimir datos

    noviembre 14, 2020 11:30

    Java

    Desde las primeras versiones de Java en el JDK se incluyen clases en el paquete java.util.zip que heredan de las del paquete java.io del sistema de entrada y salida con las que crear archivos comprimidos en formato Zip y comprimir datos en este formato. Comprimir los archivos que permite reducir sensiblemente el tamaño de algunos tipos de archivos como los basados en texto o ciertos archivos que de por si no están comprimidos. En otros tipos de archivo que en su formato ya están comprimidos como las imágenes JPEG o WebP la reducción de tamaño no es tan grande. Al contrario que los los algoritmos de compresión para imágenes JPEG y WebP que son com pérdida de datos, los algoritmos de compresión de archivos son sin pérdida, esto quiere decir que los datos obtenidos al comprimirlos y posteriormente descomprimirlos son exactamente los mismos que los originales.

    Los archivos de texto o binarios no comprimidos comprimirlos supone un considerable reducción respecto del tamaño del archivo original no comprimido, un archivo XML del 20 MiB se puede quedar en 1.5 MiB, es decir, una tasa de compresión de 13 o lo que es lo mismo el fichero comprimido ocupa 13 veces menos.

    Aparte del ahorro de espacio, crear un archivo Zip que agrupe varios es útil en ciertos casos. Las aplicaciones web como respuesta solo pueden devolver un archivo o documento por petición, un documento HTML, un recurso o un archivo. Para devolver varios archivos a la vez como una exportación de varios documentos dada la limitación del protocolo HTTP de petición y respuesta no es posible. Para dar solución a esta limitación normalmente se crea un archivo Zip que contenga los varios a devolver. Configurar los servidores web Nginx y Apache para comprimir los recursos minimiza los datos transferidos desde el servidor al cliente.

    Contenido del artículo

    Clases de Java para comprimir y descomprimir

    Las clases del paquete java.util.zip permiten comprimir y descomprimir datos utilizando diferentes algoritmos. Se proporcionan soporte para el formato archivo Zip, GZIP, el algoritmo de compresión DEFLATE usando la librería ZLIB y otras clases para la corrección de errores o checksums.

    Para la creación de archivos Zip las clases básicas son ZipEntry que representa un flujo de datos comprimido o archivo en un archivo Zip, ZipFile representa un archivo Zip, ZipInputStream que permite descomprimir los flujos de datos al leerlos y ZipOutputStream que permite comprimir flujos de datos.

    Cómo crear un archivo Zip con Java

    El siguiente código permite comprime varios archivos en diferentes formatos en un único archivo Zip. Este caso es el de una aplicación Java de línea de comandos, el código para una aplicación web consistiría en crear el archivo Zip y devolver como resultado el stream de ZipOutputStream de datos en el OutputStream empleado por la aplicación web para devolver el resultado al cliente.

    Los archivos de extensión jar utilizados en Java para crear librerías y war para empaquetar aplicaciones web no son más que archivos Zip con estas extensiones.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    package io.github.picodotdev.blogbitix.javazip;
    
    ...
    
    public class Main {
    
        ...
    
        public void compress() throws Exception {
            ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(new File("target/file.zip"))));
            try (zos) {
                for (String f : FILES) {
                    System.out.printf("Deflating %s...%n", f);
                    InputStream resource = getClass().getClassLoader().getResourceAsStream("cantrbry/" + f);
    
                    zos.putNextEntry(new ZipEntry(f));
                    zos.write(resource.readAllBytes());
                    zos.closeEntry();
                }
            }
        }
    
        ...
    }
    
    Main-compress.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    Deflating alice29.txt...
    Deflating asyoulik.txt...
    Deflating cp.html...
    Deflating fields.c...
    Deflating grammar.lsp...
    Deflating kennedy.xls...
    Deflating lcet10.txt...
    Deflating plrabn12.txt...
    Deflating ptt5...
    Deflating sum...
    Deflating xargs.1...
    System.out-compress

    Cómo descomprimir un archivo Zip con Java

    El proceso contrario a la compresión es la descompresión, la descompresión permite recuperar los datos originales. Para esto se utiliza la clase ZipFile en el caso de leer de un archivo, la clase ZipInputStream también permite leer el Zip creado con ZipOutputStream.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    package io.github.picodotdev.blogbitix.javazip;
    
    ...
    
    public class Main {
    
        private static final List<String> FILES = List.of("alice29.txt", "asyoulik.txt", "cp.html", "fields.c", "grammar.lsp", "kennedy.xls", "lcet10.txt", "plrabn12.txt", "ptt5", "sum", "xargs.1");
    
        ...
    
        public void decompress() throws Exception {
            ZipFile zf = new ZipFile(new File("target/file.zip"));
            try (zf) {
                Enumeration<? extends ZipEntry> entries = zf.entries();
                for (ZipEntry ze : Collections.list(entries)) {
                    System.out.printf("Inflating %s (compressed: %s, size: %s, ratio: %.2f)%n", ze.getName(), ze.getCompressedSize(), ze.getSize(), (double) ze.getSize() / ze.getCompressedSize());
                    InputStream is = zf.getInputStream(ze);
                    FileOutputStream fos = new FileOutputStream(new File("target", ze.getName()));
                    try (is; fos) {
                        fos.write(is.readAllBytes());
                    }
                }
            }
        }
    
        ...
    }
    
    Main-decompress.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    Inflating alice29.txt (compressed: 54398, size: 152089, ratio: 2,80)
    Inflating asyoulik.txt (compressed: 48891, size: 125179, ratio: 2,56)
    Inflating cp.html (compressed: 7955, size: 24603, ratio: 3,09)
    Inflating fields.c (compressed: 3116, size: 11150, ratio: 3,58)
    Inflating grammar.lsp (compressed: 1216, size: 3721, ratio: 3,06)
    Inflating kennedy.xls (compressed: 203986, size: 1029744, ratio: 5,05)
    Inflating lcet10.txt (compressed: 144898, size: 426754, ratio: 2,95)
    Inflating plrabn12.txt (compressed: 195255, size: 481861, ratio: 2,47)
    Inflating ptt5 (compressed: 56459, size: 513216, ratio: 9,09)
    Inflating sum (compressed: 12984, size: 38240, ratio: 2,95)
    Inflating xargs.1 (compressed: 1730, size: 4227, ratio: 2,44)
    System.out-decompress

    Listar el contenido de un archivo Zip con Java

    En Java el contenido de un ZipFile se representa con la clase ZipEntry, el siguiente código lista el contenido de un archivo Zip.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package io.github.picodotdev.blogbitix.javazip;
    
    ...
    
    public class Main {
    
        ...
    
        public void list() throws Exception {
            ZipFile zf = new ZipFile(new File("target/file.zip"));
            try (zf) {
                Enumeration<? extends ZipEntry> entries = zf.entries();
                for (ZipEntry ze : Collections.list(entries)) {
                    System.out.printf("File %s (compressed: %s, size: %s, ratio: %.2f)%n", ze.getName(), ze.getCompressedSize(), ze.getSize(), (double) ze.getSize() / ze.getCompressedSize());
                }
            }
        }
    
        ...
    }
    
    Main-list.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    File alice29.txt (compressed: 54398, size: 152089, ratio: 2,80)
    File asyoulik.txt (compressed: 48891, size: 125179, ratio: 2,56)
    File cp.html (compressed: 7955, size: 24603, ratio: 3,09)
    File fields.c (compressed: 3116, size: 11150, ratio: 3,58)
    File grammar.lsp (compressed: 1216, size: 3721, ratio: 3,06)
    File kennedy.xls (compressed: 203986, size: 1029744, ratio: 5,05)
    File lcet10.txt (compressed: 144898, size: 426754, ratio: 2,95)
    File plrabn12.txt (compressed: 195255, size: 481861, ratio: 2,47)
    File ptt5 (compressed: 56459, size: 513216, ratio: 9,09)
    File sum (compressed: 12984, size: 38240, ratio: 2,95)
    File xargs.1 (compressed: 1730, size: 4227, ratio: 2,44)
    System.out-list

    Usando la aplicación integrada en el entorno de escritorio o sistema operativo para visualizar el contenido del archivo Zip se observa que contiene comprimidos el conjunto de archivos añadidos en él.

    Contenido de archivo Zip

    Contenido de archivo Zip

    O listar el contenido de un archivo Zip desde la linea de comandos.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    $ unzip -l app/target/file.zip
    Archive:  file.zip
      Length      Date    Time    Name
    ---------  ---------- -----   ----
       152089  2020-11-15 01:55   alice29.txt
       125179  2020-11-15 01:55   asyoulik.txt
        24603  2020-11-15 01:55   cp.html
        11150  2020-11-15 01:55   fields.c
         3721  2020-11-15 01:55   grammar.lsp
      1029744  2020-11-15 01:55   kennedy.xls
       426754  2020-11-15 01:55   lcet10.txt
       481861  2020-11-15 01:55   plrabn12.txt
       513216  2020-11-15 01:55   ptt5
        38240  2020-11-15 01:55   sum
         4227  2020-11-15 01:55   xargs.1
    ---------                     -------
      2810784                     11 files
    unzip.sh

    Otros formatos de archivos comprimidos

    Los algoritmos ofrecidos en la API de Java no son los únicos, hay otros con diferentes características. Zip es un formato de archivos comprimidos popular disponible en los sistemas operativos mayoritarios como Windows, GNU/Linux y macOS ofrecen soporte para comprimir y descomprimir archivos con este formato. Otro algoritmos distintos al Zip son mejores en varios aspectos, ofreciendo ratios de compresión mayores o con velocidades de compresión y descompresión mayores.

    Algunos algoritmos de compresión alternativos son bzip2, lzma, lzo o zstd para los que Java no proporciona clases en el JDK y en algunos de ellos no hay ninguna librería que permite trabajar en Java con estos formatos de archivo. La única alternativa es ejecutar un proceso del sistema que invoque el comando del sistema para realizar la compresión, la descompresión o listar el contenido del archivo.

    Terminal

    El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
    ./gradlew run

    » Leer más, comentarios, etc...

    Blog Bitix

    Herramienta para ejecutar consultas SQL en la base de datos de producción

    noviembre 13, 2020 04:00

    No siempre las aplicaciones proporcionan los datos que se necesitan. A veces para obtener cierta información de forma puntual o para corregir un dato que no se puede hacer desde la aplicación es necesario lanzar una consulta SQL a la base de datos relacional. Esto no es lo ideal, simplemente en ocasiones es lo más simple y rápido. Por otro lado, para tareas de análisis algunos usuarios necesitan una forma de tener acceso a los datos y obtener gráficas para analizarlos. En el artículo comento varias herramientas para tener acceso a las diferentes bases de datos ya sean de producción o del entorno de QA.

    Sí, es cierto, lanzar consultas SQL directamente en la base de datos de producción o una réplica no es lo ideal ni debería ser habitual, sin embargo, a veces tener acceso a los datos de la base de datos de producción es la forma más rápida y sencilla de obtener información para resolver un bug del que ni siquiera un sistema centralizado de almacenamiento de trazas como ELK proporciona información relevante o simplemente porque alguien del departamento financiero, marketing u otro departamento solicita una información que necesita.

    En ambos casos del bug o de alguien que necesita información en ocasiones tienen la mala costumbre de necesitar ser atendidos con rapidez. Si para obtener la información se necesita un proceso que involucra a varias personas, cada una con sus propias tareas y prioridades, el proceso es lento y el demandante de esa información acabará poco contento quejándose del departamento de IT. El proceso también será lento si es complicado o sólo determinadas personas tienen los permisos necesarios pare realizarlo o con riesgos si las herramientas que se usan otorgan gran poder pero requieren gran responsabilidad.

    Proporcionando una herramienta a modo de sírvete tú mismo uno se sorprende de las consultas que alguien del departamento financiero o de marketing son capaces de hacer ellos mismos para obtener la información que necesitan si tienen el interés de aprender algo de SQL. Lo cual es beneficioso en varios sentidos, ellos son más autónomos en su trabajo y las personas de IT no se ven interrumpidas para atender este tipo de tareas, sólo cuando la consulta tiene cierta dificultad.

    Si además como en los microservicios hay múltiples bases de datos para varios de ellos tener acceso a todas las bases de datos es más complicado de gestionar. Por otro lado, al decir la base de datos de producción también es válido para tener acceso a las bases de datos del entorno de QA.

    Contenido del artículo

    Herramienta para obtener y modificar datos de una base de datos relacional

    SQLPad es una pequeña herramienta que actúa como cliente de una base de datos relacional con la que es posible lanzar consultas SQL mediante una interfaz web. Esta herramienta hace muy sencillo el acceso a la base de datos de producción, entorno de QA o a una de sus réplicas, dado que utiliza una interfaz web no es necesario instalar ni configurar ninguna herramienta adicional, solo requiere un navegador web que todo ordenador ya posee.

    Simplemente es necesario seleccionar la base de datos de la que se quieren los datos y ejecutar la consulta SQL deseada para obtener los datos como resultado. Permite explorar los esquemas de la base de datos, las tablas, los campos y cuales son sus tipos.

    SQLPad tiene una licencia de software libre y soporta las bases de datos relacionales más populares como Postgres, MySQL, SQL Server, Cassandra, Google BigQuery, SQLite entre otras.

    SQLPad

    SQLPad

    Otra funcionalidad habitual es poder exportar los datos en formato CSV o xlsx para procesarlos con algún comando o programa en el equipo en local o simplemente para hacer algún tipo de tratamiento con el programa ofimático de hoja de cálculo.

    1
    2
    3
    4
    
    id,name
    1,one
    2,two
    3,two
    data.csv

    OnlyOffice CSV

    OnlyOffice CSV

    Aunque SQLPad no ofrece la potencia de otras herramientas específicas para la tarea permite visualizar los datos de forma básica en gráficas. Por ejemplo obtener un gráfico de barras de una agrupación de datos.

    1
    2
    
    $ docker-compose up
    
    
    docker-compose-up.sh

    SQLPad Visualization SQLPad Visualization

    SQLPad Visualization

    Permite además guardar las consultas que sirve para compartir entre diferentes miembros de un equipo aquellas que son habituales o proporcionarles a personas que no saben SQL la consulta que deben ejecutar para obtener los datos que necesitan de forma periódica.

    Consultas compartidas en SQLPad

    Consultas compartidas en SQLPad

    Una herramienta que proporciona acceso a los datos de producción requiere autenticación y permisos para otorgar únicamente el acceso a las bases de datos o conceder acceso de solo lectura a los datos. Los usuarios con el rol de administrador puede crear conexiones a bases de datos, crear usuarios y definir qué conexiones pueden utilizar cada usuario con una fecha de expiración para cada usuario. Para crear una conexión de solo lectura hay que crear un usuario en la base de datos que únicamente tenga permisos de solo lectura, posteriormente en SQLPad para definir una conexión de solo lectura se usa este usuario. Del mismo modo con los permisos del usuario de conexión de la base de datos es posible limitar las tablas a las que se tienen acceso u otros permisos que permita la base de datos.

    SQLPad ofrece un contenedor de Docker y un archivo de Docker Compose con el que iniciar la herramienta. El siguiente archivo de Docker Compose define un contenedor con una base de datos PostgreSQL a la que se conecta e inicializa el contenedor de SQLPad. Los archivos del código fuente completo del ejemplo incluye algunos adicionales, SQLPad se accede con la siguiente URL http://localhost:3000/ que solicita una credenciales admin@sqlpad.com/admin.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    version: '3'
    services:
      postgres:
        image: postgres
        restart: always
        environment:
          POSTGRES_USER: sqlpad
          POSTGRES_PASSWORD: sqlpad
        volumes:
          - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    
      sqlpad:
        # To use Dockerfile at root of this project, use build instead of image
        # build: ../../
        image: sqlpad/sqlpad:5
        hostname: 'sqlpad'
        ports:
          - '3000:3000'
        environment:
          SQLPAD_ADMIN: 'admin@sqlpad.com'
          SQLPAD_ADMIN_PASSWORD: 'admin'
          SQLPAD_APP_LOG_LEVEL: debug
          SQLPAD_WEB_LOG_LEVEL: warn
          SQLPAD_SEED_DATA_PATH: /etc/sqlpad/seed-data
          SQLPAD_CONNECTIONS__pgdemo__name: Postgres demo
          SQLPAD_CONNECTIONS__pgdemo__driver: postgres
          SQLPAD_CONNECTIONS__pgdemo__host: postgres
          SQLPAD_CONNECTIONS__pgdemo__database: sqlpad
          SQLPAD_CONNECTIONS__pgdemo__username: sqlpad
          SQLPAD_CONNECTIONS__pgdemo__password: sqlpad
          SQLPAD_CONNECTIONS__pgdemo__multiStatementTransactionEnabled: 'true'
          SQLPAD_CONNECTIONS__pgdemo__idleTimeoutSeconds: 86400
        volumes:
          - ./seed-data:/etc/sqlpad/seed-data
    docker-compose.yml
    1
    2
    
    $ docker-compose up
    
    
    docker-compose-up.sh

    Herramientas para visualizar datos

    Obtener los datos y descargarlos en formato csv o xlsx permite extraer información, para comprender mejor un gran volumen de datos se utilizan diferentes tipos de gráficas.

    SQLPad tiene una funcionalidad básica para representar los datos en formato gráfico pero no es una herramienta especializada en esta tarea.

    Redash, Metabase, Apache Superset son herramientas dedicadas a construir gráficas a partir de datos de la categoría inteligencia empresarial o business intelligence. Permiten crear paneles de control con la información que se desee y ver la evolución y tendencia de los datos. Esta información sirve como apoyo para tomar decisiones basadas en los datos.

    Algunas herramientas tienen un coste si se usan con el hosting que ofrecen pero son gratuitas si se hospeda en infraestructura propia.

    Redash Metabase Apache Superset

    Redash, Metabase y Apache Superset

    Cómo proteger los datos

    Además de limitar a que bases de datos tiene permisos de acceso cada usuario algunos datos almacenados son sensibles, por motivos de seguridad si se tratan de contraseñas si no se utiliza una forma correcta de guardar contraseñas con salted-password-hashing en la base de datos, datos personales protegidos por leyes que de no custodiar correctamente los datos en caso de filtración suponen sanciones como importantes multas económicas o datos de tarjetas de crédito, códigos de seguridad, fechas de expiración o cuentas bancarias.

    Una forma de proteger los datos aún teniendo acceso a ellos es cifrarlos es utilizando Vault con su funcionalidad de protección de datos. Vault permite actuar como servicio para cifrar los datos a guardar en la base de datos y descifrar los datos al recuperarlos de modo que aún teniendo acceso a la base de datos no supone ningún problema de seguridad. Aún así siempre es recomendable otorgar a los usuarios los permisos mínimos que necesiten o limitar únicamente a ciertos usuarios los permisos.

    Vault Encryption

    Vault Encryption

    » Leer más, comentarios, etc...

    Variable not found

    Enlaces interesantes 420

    noviembre 09, 2020 07:05

    Por si te lo perdiste...

    .NET Core / .NET

    ASP.NET Core / ASP.NET / Blazor

    Azure / Cloud

    Conceptos / Patrones / Buenas prácticas

    Data

    Machine learning / IA / Bots

    Web / HTML / CSS / Javascript

    Visual Studio / Complementos / Herramientas

    Xamarin

    Otros

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    Blog Bitix

    Qué es, por qué y cómo activar un segundo factor de autenticación en Google, Amazon, PayPal y otros servicios

    noviembre 07, 2020 10:30

    Las contraseñas son un mecanismo de seguridad empleado para autenticar a un usuario en un servicio al iniciar sesión. Pero las contraseñas son un mecanismo potencialmente débil, por ello varios de los servicios más importantes implementan mecanismos de seguridad adicionales para proteger en mayor medida las cuentas de los usuarios. El segundo factor de autenticación o 2FA es un mecanismo adicional, las contraseñas son algo que se conoce, el 2FA es algo que se tiene. Varios de los servicios más importantes como Google, Amazon y PayPal entre otros lo ofrecen y es recomendable activarlos en aquellos en los que lo hacen.

    KeePassXC

    La autenticación es el mecanismo de seguridad que permite identificar a un usuario y es quien dice ser. Para comprobar que el usuario es quien dice ser se utiliza una contraseña que solo conoce el usuario. Pero las contraseñas como medida de seguridad no son perfectas, ya que algunos de los usuarios utilizan contraseñas débiles con pocos caracteres, con palabras de diccionario o conocidas y utilizan la misma contraseña en varias cuentas de modo que si un servicio sufre un problema de seguridad las cuentas de otros servicios con la misma contraseña quedan vulnerables.

    Toda la seguridad de las contraseñas depende de que solo sea conocida por el propietario de la cuenta, las contraseñas son un factor de seguridad de algo que sólo el legítimo propietario conoce. Otro mecanismo de seguridad es basar la seguridad adicionalmente en algo que se tiene, esto es el segundo factor de autenticación.

    Contenido del artículo

    Qué es un segundo factor de autenticación o 2FA

    El segundo factor de autenticación o 2FA es un mecanismo de seguridad que se suele utilizar adicionalmente al mecanismo de seguridad de contraseña. Adicionalmente a la contraseña el usuario ha de proporcionar un código generado con el segundo factor de autenticación.

    El generador de segundo factor de autenticación puede ser de diferentes formas, una de ellas es una aplicación para móvil y otra una aplicación de escritorio, también hay dispositivos hardware específicos de 2FA que se conectan por USB al ordenador. También se denomina doble factor de autenticación o autenticación de dos factores.

    Por qué activar un segundo factor de autenticación

    Con el segundo factor de autenticación activado aunque la contraseña de un servicio se vea comprometida la cuenta seguirá protegida ya que adicionalmente a la contraseña se necesita el segundo factor de seguridad. Las contraseñas quedan expuestas si un servicio tiene una vulnerabilidad de seguridad que es aprovechada por delincuentes para obtener las contraseñas de seguridad de usuarios con el objetivo de poder acceder a sus cuentas. Estos fallos de seguridad son explotables a través de la red sin necesidad de acceso físico a los servidores del servicio.

    El segundo factor de autenticación añade más seguridad ya que los delincuentes deben comprometer no solo el servicio sino adicionalmente el dispositivo generador de segundo factor de autenticación.

    Activar el segundo factor de autenticación es recomendable porque añade una seguridad adicional mucho mayor que utilizando únicamente una contraseña. Los servicios como el correo electrónico de Google, la plataforma de comercio electrónico Amazon, el sistema de pagos PayPal permite a los usuarios activar el 2FA. Es recomendable hacerlo cuando sea posible ya que servicios como estos se utilizan para tareas importantes como toda la comunicación e información del usuario, compras por internet donde se guardan tarjetas de crédito y números de cuenta bancaria.

    Cómo funciona un segundo factor de autenticación

    Al realizar la autenticación en un servicio para iniciar sesión se ha de introducir el identificativo del usuario y la contraseña, después con el segundo factor de autenticación activado adicionalmente se solicita un código adicional que el usuario ha de proporcionar. El código adicional es proporcionado por las aplicaciones generadores de 2FA.

    Las aplicaciones generadoras de códigos 2FA se inicializan al activar el 2FA en la cuenta, en el caso de una aplicación móvil normalmente se hace escaneando un código QR con la cámara de fotos, para otros dispositivos se proporciona una secuencia de letras. El código QR o la secuencia de letras son la semilla con la que se generan los códigos.

    Los códigos 2FA que se generan adicionalmente son temporales y solo son válidos durante un periodo corto de tiempo, a estos códigos 2FA se les denomina TOTP acrónimo de time-based one-time password normalmente formados por 6 dígitos. De modo que aunque un código quedase comprometido sólo sería válido durante un periodo corto de tiempo de unos segundos, habitualmente de unos 30 segundos. Los generadores de códigos generan un nuevo código de forma periódica.

    En la autenticación en dos pasos las credenciales para autenticar un usuario están compuestas de dos factores, la contraseña y el código TOTP.

    Cómo activar un segundo factor de autenticación

    El segundo factor de autenticación ha de activarse en cada servicio de forma individual. Al realizar la activación se proporciona el código QR a escanear por la aplicación móvil del generador de códigos con la cámara de fotos y alternativamente la secuencia de letras para otro tipo de aplicaciones o dispositivos que no tiene cámara de fotos.

    El proceso de activar el 2FA es similar en todos los servicios, varía la interfaz que se utilizan pero básicamente todos proporcionan el código QR y la secuencia de letras. En este caso muestro como activar el segundo factor de autenticación en los servicios de Google, Amazon y PayPal, para otros servicios como Twitter, Facebook u cualquier otro el proceso es similar.

    Cómo activar 2FA en una cuenta de Google

    Para activar el segundo factor de autenticación en una cuenta de Google hay que realizar la configuración desde Gestionar tu cuenta de Google ubicado en el desplegable del menú del usuario, después acceder a las opciones de seguridad del menú Protegemos tu cuenta. En la Revisión de seguridad el proceso de configuración de 2FA se inicia desde la opción Verificación en dos pasos, hay varios opciones de utilizar 2FA una de ellas es con una notificación de Google en el teléfono móvil, un SMS o un código de seguridad, la otra opción es utilizar una aplicación de autenticación o authenticator.

    La autenticación con una aplicación de authenticator se inicia desde el enlace configuración de la verificación en dos pasos para configurar la aplicación authenticator. Es un proceso de varios pasos en el que se muestra el código QR a escánear por la aplicación authenticator y el código formado por varios grupos de 4 letras equivalente, el código de letras solo se muestra con el enlace ¿No puedes escanearlo? debajo del código QR. Para asegurar que la aplicación authenticator ha escaneado correctamente el código QR y genéra códigos de 6 dígitos se ha de verificar introduciendo su valor actual.

    Activación de 2FA en una cuenta de Google Activación de 2FA en una cuenta de Google Activación de 2FA en una cuenta de Google

    Activación de 2FA en una cuenta de Google Activación de 2FA en una cuenta de Google Activación de 2FA en una cuenta de Google

    Activación de 2FA en una cuenta de Google

    Activación de 2FA en una cuenta de Google

    Una vez configurado 2FA al iniciar sesión Google por defecto envía una notificación al teléfono móvil con la que solo es necesario realizar la confirmación del inicio de sesión desde el móvil, con la opción Probar de otra manera permite usar otro método de autenticación 2FA entre ellos el de la aplicación authenticator.

    Inicio de sesión en una cuenta de Google con 2FA Inicio de sesión en una cuenta de Google con 2FA Inicio de sesión en una cuenta de Google con 2FA

    Inicio de sesión en una cuenta de Google con 2FA

    Inicio de sesión en una cuenta de Google con 2FA

    Cómo activar 2FA en una cuenta de Amazon

    En el caso de Amazon la configuración del segundo factor de autenticación se realiza desde Mi cuenta > Inicio de sesión y seguridad > Configuración de la verificación en dos pasos (2SV). Hay que registra un autenticador de verificación de dos pasos, al seleccionar utilizar una app de verificación se muestra el cogido QR y en código de letras en el enlace ¿No puedes escanear el código de barras?. Escáneado el código QR el y el código de letras se verifica la aplicación autenticator está configurada correctamente y genera códigos TOTP correctos.

    Activación de 2FA en una cuenta de Amazon Activación de 2FA en una cuenta de Amazon Activación de 2FA en una cuenta de Amazon

    Activación de 2FA en una cuenta de Amazon Activación de 2FA en una cuenta de Amazon Activación de 2FA en una cuenta de Amazon

    Activación de 2FA en una cuenta de Amazon

    Activación de 2FA en una cuenta de Amazon

    Al iniciar sesión en la cuenta de Amazon se solicita el correo electrónico y la contraseña, con 2FA activado adicionalmente se solicita el cógido TOTP, marcando la opción No vuelvas a pedir ningún código en este navegador el código TOTP solo se solicita una vez por dispositivo.

    Inicio de sesión en una cuenta de Amazon Inicio de sesión en una cuenta de Amazon

    Inicio de sesión en una cuenta de Amazon

    Cómo activar 2FA en una cuenta de PayPal

    En PayPal la configuración de segundo factor de autenticación se realiza desde el Centro de seguridad con la opción Verificación en dos pasos para configurarlo. En la configuración se muestra el código QR y el código de letras introducir en la aplicación authenticator. Como en los otros casos para verificar que la aplicación authenticator se ha configurado correctamente para generar los código TOTP se solicita validar uno.

    Activación de 2FA en una cuenta de PayPal Activación de 2FA en una cuenta de PayPal Activación de 2FA en una cuenta de PayPal

    Activación de 2FA en una cuenta de PayPal

    Al iniciar sesión en la cuenta de PayPal se solicita también el correo electrónico y la contraseña, con 2FA activado adicionalmente se solicita el código TOTP, marcando la opción Confiar en este dispositivo el código TOTP solo se solicita una vez por dispositivo.

    Inicio de sesión en una cuenta de PayPal Inicio de sesión en una cuenta de PayPal

    Inicio de sesión en una cuenta de PayPal

    Aplicación para smartphone generador de TOTP

    Dos aplicaciones gratuitas para teléfono inteligente o smartphone son Google Authenticator y Microsoft Authenticator disponibles tanto para los que utilizan Android como iOS como sistema operativo en sus respectivas tiendas de aplicaciones.

    Instalada la aplicación en el smartphone el primer paso es iniciar el proceso de activación de 2FA en un servicio, este servicio proporciona el código QR que escaneándolo con la aplicación permite generar generar los códigos temporales 2FA. Una vez escaneado el código QR se muestra como información el servicio y cuenta, el código temporal válido y un indicador de tiempo que una vez expirado se genera un nuevo código.

    Las aplicaciones móviles ofrecen comodidad pero ¿qué ocurre si el móvil se pierde, nos lo roban o se estropea? No disponer del dispositivo generador de 2FA es un problema ya que sin él no se impide el inicio de sesión ya que en algún momento se solicitará el código 2FA.

    Google Authenticator Google Authenticator Microsoft Authenticator

    Aplicaciones Google Authenticator y Microsoft Authenticator

    Aplicación de escritorio generador de TOTP

    KeePassXC es una aplicación de escritorio que sirve como base de datos para guardar contraseñas de forma segura y generar contraseñas únicas seguras para los servicios. Está disponible para los sistemas operativos Windows, GNU/Linux y macOS.

    Aplicación gestor de contraseñas KeePassXC

    Aplicación gestor de contraseñas KeePassXC

    KeePassXC también sirve para guardar el 2FA siendo capaz de generar los mismo códigos TOTP de las aplicaciones de smartphone. Para guardar la semilla con la que se generan los TOTP hay que ir a la opción del menú Apuntes > TOTP > Configurar TOTP o con el menú contextual del botón derecho sobre el apunte, esta opción muestra una ventana en la que introducir el código de letras alternativo al código QR al activar el 2FA del servicio. También es capaz de mostrar el código QR original, lo que permite migrar el 2FA a otro móvil.

    Código TOTP generador por KeePassXC

    Configuración TOTP en KeePassXC

    Si se configura como generador de códigos un smartphone y KeePassXC es posible comprobar que generan los mismos TOTP y están bien configurados, con la opción Apuntes > TOTP > Mostrar TOTP se muestra un diálogo con el código TOTP válido.

    Código TOTP generador por KeePassXC

    Código TOTP generador por KeePassXC

    Por qué utilizar KeePassXC adicionalmente a una aplicación del móvil

    No conviene guardar los 2FA únicamente en el smartphone, perder el dispositivo generador de códigos 2FA impide iniciar sesión en los servicios cuando se solicite el TOTP. Algunos servicios permiten recuperar la cuenta utilizando otras formas de autenticación pero algunos otros servicios perder el 2FA quizá suponga la pérdida del acceso a la cuenta.

    Utilizar KeePassXC permite tener un segundo dispositivo con el que generar los TOTP, además es fácil hacer una copia de seguridad del archivo de la base de datos de las contraseñas de KeePassXC ya que es simplemente un archivo cifrado y permite configurar otros dispositivos como generadores de TOTP a través del código QR original.

    » Leer más, comentarios, etc...

    info.xailer.com

    Visor e impresor de archivos PDF en Xailer 7

    noviembre 05, 2020 07:18

    Xailer 7 incorpora controles para visualizar e imprimir cualquier archivo PDF de una forma muy sencilla, sin limitaciones, con una increíble velocidad y sin que nuestra aplicación vaya a crecer demasiado por ello. Para conseguirlo Xailer 7 se apoya en la utilidad Sumatra PDF.

    Sumatra PDF reader es una estupenda herramienta para visualizar e imprimir archivos PDF. Es muy rápida, liviana y su integración con Xailer es total, ya que se comporta como un control más dentro de su aplicación, que puede fácilmente seleccionar desde la propia paleta de controles del IDE.

    Para poder utilizar el control es necesario que su aplicación pueda acceder a un único fichero de nombre SUMATRAPDF.EXE. Con Xailer 7 ofrecemos dos versiones de dicho ejecutable. La versión 3.1 por tener un tamaño ridículo de 2.721 Kb que es posible que sea suficiente para los archivos que usted desea mostrar. Y la la versión 3.2 que tiene un tamaño de 14.017 Kb que tampoco es muy grande teniendo en cuenta todo lo que hace.

    Sumatra PDF viewer soporta los siguientes tipos de archivos: PDF, EPUB, MOBI, CHM, XPS, DjVu, CBZ y CBR. Y las siguientes extensiones:

    • PDF (.pdf), unencrypted EPUB (.epub), MOBI (.mobi)
    • Fiction Wise (.fb2, .fb2z, .zfb2), Palm DOC format (.pdb)
    • comic book files: (.cbz, .cbr, .cbt, .cb7z)
    • DjVu (.djv, .djvu), Microsoft Compiled HTML Html (.chm)
    • XPS (.xps), images (.jpg, .png,.gif, .webp, .tiff, tga, .j2k)

    Puede obtener más información sobre Sumatra en esta dirección: https://www.sumatrapdfreader.org

    La licencia con la que se ofrece este producto es: GNU General Public License v3, que en principio es compatible con el uso comercial del producto siempre que sigan todas las restricciones que impone la licencia. No obstante, el equipo de Xailer se puso en contacto con el desarrollador Krzysztof Kowalczyk para confirmarlo. Puede seguir la pregunta y respuesta en el siguiente enlace en su foro.

    » Leer más, comentarios, etc...

    info.xailer.com

    Mejoras en los Data Controls de Xailer 7

    noviembre 04, 2020 12:29

    Os sigo comentando las mejoras importantes que incluirá el futuro Xailer 7. Como muchos sabéis, cuando se utilizan DataControls, Xailer es capaz de acceder a los distintas campos de una tabla o cursor como si se trataran de miembros de la propia clase TDataset. Un ejemplo:

    WITH OBJECT oDataset
      :Edit()
      :Codigo := 1 // campo
      :Nombre := "Ignacio" // campo
      :Update()
    END WITH

    Cuando el nombre del campo coincide con un miembro real del propio dataset, el acceso al campo no es posible, ya que tiene preferencia el miembro de la clase TDataset. Para resolver este problema, hasta ahora se podía utilizar el método TDataset:FieldPut() directamente o bien recuperar el objeto TDataField con el método oFieldByName() y luego asignarle el valor.

    A partir de Xailer 7 tenemos una solución más elegante para acceder a cualquier campo del TDataset aunque coincida su nombre con algún miembro de la clase y consiste en utilizar la siguiente instrucción: TDataset:!Campo

    Observe como la única diferencia existente es la admiración después de ‘:’. Además, de esta forma, usted mismo como programador, sabrá si está accediendo a un campo o a un miembro de la clase, lo que hará que su código sea mucho más legible.

    Otra mejora importante: En Xailer 7 hemos hecho un pequeño guiño a las bases de datos NoSQL. En las bases de datos NOSQL los esquemas de datos son dinámicos, es decir, no existe una estructura fija de tabla y la consistencia de los datos es menos importante. En Xailer 7 hemos querido incluir alguna de sus cualidades y ahora cualquier dataset, incluso una tabla DBF puede tener la habilidad de guardar en su tabla cualquier campo, aunque éste no exista ni siquiera en la tabla. Sólo es necesario la existencia de un campo en la base de datos de nombre MoreData, que lógicamente deberá de ser tipo BLOB o MEMO.

    Para acceder o establecer el valor de éste campo virtual tan sólo que hay que usar la sintaxis: oDataset:!Campo. Es decir, hay que incluir el carácter ‘!’ del que hemos hablado anteriormente. ¡Eso es todo! Internamente toda la información se guarda con formato JSON dentro de ese campo y por lo tanto se pueden realizar búsquedas en el campo ‘MoreData’ e incluso editarlo desde su editor de bases de datos preferido. Nuestro editor de tablas SQLite ya soporte el tipo de campo JSON, por lo que en dicho caso, lo preferible es definir el campo con ese tipo.

    Las posibilidades de este nuevo sistema son múltiples. A modo de ejemplo:

    • Guardar en una tabla de clientes las direcciones completas de todas sus delegaciones (no es necesario crear una tabla clientes-delegaciones)
    • Guardar en una tabla de artículos una relación completa de todas las ofertas que tiene por periodo o cantidad
    • Evitar tener que modificar una tabla por la necesidad de incluir un nuevo campo

    Una de las grandes ventajas de este nuevo sistema, es que puede guardar en la tabla cualquier tipo de dato, incluso matrices y objetos Hash.

    No obstante, existen limitaciones, que son insalvables, como son:

    • Los campos virtuales no pueden ser asignados a la propiedad ‘oField‘ de un datacontrol
    • No se pueden realizar búsquedas directas sobre campos virtuales. Deberá buscar internamente en el campo ‘MoreData’

    » Leer más, comentarios, etc...

    Fixed Buffer

    IoT Hub DPS: Creando dispositivos bajo demanda

    noviembre 03, 2020 09:00

    Tiempo de lectura: 8 minutos
    Imagen ornamental para la entrada "IoT Hub DPS: Creando dispositivos bajo demanda"

    ¡Otra semana más! parece que fue ayer cuando estábamos de vacaciones todavía, pero nada más lejos de la realidad. Arrancan nuevos proyectos en mi trabajo y por suerte siempre estoy ahí moviéndome en lo que me gusta, Kubernetes y IoT.
    Es por eso qué hoy toca seguir hablando de IoT en Azure 🙂

    En la última entrada planteamos cómo crear módulos especializados para desplegar sobre un dispositivo IoT Edge y hoy toca hablar de otra situación muy habitual al hablar de IoT: cómo aprovisionar los dispositivos.

    ¿En qué consiste aprovisionar dispositivos?

    Hasta ahora, siempre que hemos trabajado con IoT Hub, hemos creado individualmente cada dispositivo, sea de IoT Hub o de IoT Edge, y hemos recuperado su cadena de conexión.

    Existe otro modo en el que podemos registrar esos diferentes dispositivos que consiste en utilizar un servicio de Azure llamado Azure IoT Hub Device Provisioning Service (DPS). Este servicio nos va a permitir asociar diferentes IoT Hub, sobre los cuales se van a crear los dispositivos de manera automática.

    Este gráfico (sacado de la documentación oficial) describe muy claramente cuál es el flujo que sigue IoT Hub DPS para crear un dispositivo nuevo:

    1- El fabricante del dispositivo agrega la información de registro del dispositivo a la lista de inscripción en Azure Portal. 2- El dispositivo contacta con el punto de conexión de DPS configurado de fábrica.  3- DPS valida la identidad del dispositivo. 4- DPS registra el dispositivo con un IoT Hub. 5- El centro de IoT devuelve la información del identificador de dispositivo a DPS. 6- DPS devuelve la información de conexión del centro de IoT al dispositivo. El dispositivo ya puede empezar a enviar datos directamente a la instancia de IoT Hub. 7- El dispositivo se conecta a IoT Hub. 8- Obtiene el estado deseado del dispositivo gemelo en la instancia de IoT Hub.
    1. El fabricante del dispositivo agrega la información de registro del dispositivo a la lista de inscripción en Azure Portal.
    2. El dispositivo contacta con el punto de conexión de DPS configurado de fábrica. 
    3. DPS valida la identidad del dispositivo.
    4. DPS registra el dispositivo con un IoT Hub.
    5. El centro de IoT devuelve la información del identificador de dispositivo a DPS.
    6. DPS devuelve la información de conexión del centro de IoT al dispositivo. El dispositivo ya puede empezar a enviar datos directamente a la instancia de IoT Hub.
    7. El dispositivo se conecta a IoT Hub.
    8. Obtiene el estado deseado del dispositivo gemelo en la instancia de IoT Hub.

    ¿Por qué aprovisionar dispositivos con IoT Hub DPS?

    Crear manualmente los dispositivos está bien siempre que el número de dispositivos que queremos registrar no sea excesivamente grande, o siempre que cada dispositivo que creamos físicamente tenga algún proceso de fabricación individual.

    Por ejemplo, si cada dispositivo es arrancado por un técnico para hacer algún proceso propio de la fabricación, no es descabellado que durante ese proceso se cree el dispositivo y se registre.

    En cambio, imagina un escenario en el que no existe ese proceso manual y se fabrican en serie cientos de dispositivos iguales, que pueden ir a parar a cualquier lugar del mundo y caer en cualquier mano. En caso de estar en un escenario como el que acabo de describir, seguramente nos interese tener diferentes IoT Hub distintos desplegados en diferentes regiones y que los dispositivos se creen y conecten en el Hub más cercano pare reducir la latencia (geolocalización). Eso en el caso más exigente, pero otro caso quizás más común es aquél donde el usuario final no tiene los conocimientos o interés en configurar el dispositivo.

    En este tipo de escenarios masivos en los que no hay intervención humana especializada durante el proceso de fabricación, es vital tener un sistema de aprovisionamiento automático.

    Creando nuestro IoT Hub DPS

    Doy por hecho que si estas leyendo esto, es porque conoces IoT Hub y como crearlo, por lo que voy a partir del punto en el que conoces como crear un IoT Hub y como funciona, así como en que consiste IoT Edge. En caso de que no sea así, te dejo unas entradas sobre el tema:

    Aunque existen varias maneras de crear un IoT Hub DPS, a fin de simplificar al máximo vamos a utilizar Az Cli con su extension ‘iot‘.

    Lo primero va a ser crear el servicio, para ello, basta con ejecutar:

     az iot dps create --name $dpsName --resource-group $rg --location $region
    

    Esto nos va a devolver entre otras cosas, dos datos que necesitamos guardar para utilizar más adelante (aunque también los podemos recuperar más adelante desde Az Cli o desde el portal). Estos datos son ‘idScope‘ y ‘serviceOperationsHostName‘ y los vamos a necesitar cuando estemos configurando los dispositivos para auto aprovisionarse con IoT Hub DPS.

    Lo siguiente que vamos a hacer una vez que se ha creado el servicio, es asociarlo con las instancias de IoT Hub donde queremos que sea posible aprovisionar dispositivos. Para esto vamos a necesitar tener una cadena de conexión con permisos de administración sobre cada uno de los IoT Hub que queremos conectar. Podemos conseguirla desde el portal o simplemente ejecutando:

    $hubConnectionString=$(az iot hub show-connection-string --name $iotHubName --key primary --query connectionString -o tsv)
    

    Y una vez que la tengamos, asociamos IoT Hub DPS con la instancia de IoT Hub ejecutando:

    az iot dps linked-hub create --dps-name $dpsName --resource-group $rg --connection-string $hubConnectionString --location $region
    

    Adicionalmente, podemos añadir el parámetro ‘–allocation-weight‘, que no es más que un valor de prioridad que podemos utilizar en el siguiente paso para decidir en que IoT Hub se van aprovisionar los dispositivos en caso de que exista más de uno asociado a la instancia de IoT Hub DPS.

    Manejando las inscripciones

    Vamos a pasar revista de lo que tenemos hasta el momento:

    • Tenemos un IoT Hub o varios.
    • Tenemos un IoT Hub DPS junto a su idScope y serviceOperationsHostName.
    • Hemos asociado nuestro/s IoT Hub con el DPS.

    Ya casi estamos listos para poder empezar a utilizar nuestro IoT Hub DPS, solo nos falta registrar dentro del propio servicio el método por el que vamos a identificar a los dispositivos. Para esto tenemos 2 modelos:

    • Registrar dispositivos individualmente.
    • Crear grupos de registro.

    Estos dos modelos nos van a permitir gestionar como queremos que se inscriban los diferentes dispositivos. Llegados a este punto, puede parecer un poco absurdo el hecho de registrar dispositivos individualmente, ¿no? Pues la verdad es que tiene todo el sentido del mundo ya que el hecho de utilizar IoT Hub DPS para un dispositivo individual nos garantiza que en caso de que borremos el dispositivo de IoT Hub por cualquier razón, se volverá a crear automáticamente sin necesidad de interacción humana. Por ejemplo, puede darse el caso de que una instancia de IoT Hub este sobrecargada y no la podamos escalar por ‘x’ razón, gracias a este aprovisionamiento individual, podríamos cambiar dispositivos concretos de un Hub a otro con un mínimo esfuerzo.
    Por su parte en cambio, la inscripción de grupos nos va a permitir registrar múltiples dispositivos con un mismo sistema.

    Independientemente del sistema de inscripción que elijamos, vamos a tener 2 mecanismos para que un dispositivo se identifique ante IoT Hub DPS y un tercero en caso de dispositivos individuales:

    • X.509
    • Clave Simétrica
    • TPM (dispositivos individuales)

    Teniendo en cuenta estos dos tipos de registros y sus diferentes modos de identificación, vamos a crear una inscripción de grupo a modo de ejemplo (aunque una individual sería prácticamente igual). Para este proceso, vamos a necesitar tomar algunas decisiones: ¿Qué criterio vamos a usar para elegir el IoT Hub? Las opciones disponibles son:

    • Latencia mínima.
    • Peso de los IoT Hub.
    • Siempre al mismo.
    • Utilizar una Azure Function.

    ¿Qué instancias de IoT Hub queremos que sean elegibles para registrar el dispositivo? ¿Qué propiedades y/o tags queremos que tengan automáticamente los dispositivos? Estas últimas por ejemplo son muy útiles cuando queremos que un deployment de módulos se aplique directamente a los dispositivos IoT Edge.

    En este caso para probar, vamos a crear dos grupos de inscripción, uno que sirva para dispositivos IoT Edge y añada el tag {"IoTEdge" : "FixedBuffer"} y otro que sirva para IoT Hub y tenga el tag {"IoTHub" : "FixedBuffer"}.

    Para eso, vamos a ejecutar:

    # Grupo para IoT Edge
    az iot dps enrollment-group create -g $rg --dps-name $dpsName --enrollment-id "IoTEdge" --primary-key $primaryKey --secondary-key $secondayKey --initial-twin-tags "{'IoTEdge' : 'FixedBuffer'}" --edge-enabled=true
    # Grupo para IoT Hub
    az iot dps enrollment-group create -g $rg --dps-name $dpsName --enrollment-id "IoTHub" --primary-key $primaryKey --secondary-key $secondayKey --initial-twin-tags "{'IoTHub' : 'FixedBuffer'}" --edge-enabled=false
    # Verificamos que se han creado
    az iot dps enrollment-group list --dps-name $dpsName --resource-group $rg
    

    Probando IoT Hub DPS

    Con esto que hemos hecho ya estamos preparados para poder empezar a aprovisionar nuestros dispositivos. Bueno, en realidad no del todo… He elegido aprovisionar grupos ya que tiene un paso más respecto a aprovisionar dispositivos individuales.

    No es una buena idea el distribuir de manera masiva una clave que permite registrar tantos dispositivos quieras de manera automática. Es por esto que IoT Hub DPS trabaja con una clave derivada cuando se trata de inscripción de grupos. Esta clave derivada se puede calcular utilizando HMACSHA256, para lo cual la propia documentación nos ofrece un código de ejemplo.

    Probar el aprovisionamiento de un dispositivo IoT Hub es muy sencillo ya que basta con que nos bajemos el repositorio de ejemplos de IoT, y ejecutemos el ejemplo ‘ProvisioningDeviceSamples‘ seleccionando el proyecto de inicio en ‘SymmetricKeySample‘. El propio fichero Program.cs nos guía mediante comentarios sobre que tenemos que rellenar y donde para poder validar el funcionamiento.

    En mi caso, una vez ejecutado se ha registrado un dispositivo llamado ‘sample-iot-hub’, al que al consultar sus tags con az iot hub device-identity list --ee false -n $iotHubName --query="[*].tags" me devuelve esto:

    {
      "IoTHub": "FixedBuffer"
    }
    

    De igual modo, vamos a probar que se registra correctamente si el dispositivo es de tipo Edge. Esto requiere que configuremos su fichero config.yaml y le indiquemos los datos que necesita para poder conectarse (que son los mismos que en el caso anterior). Para poder probar que todo esto funciona de manera más sencilla, he creado una imagen Docker que a la que indicándole las variables de entorno ‘SCOPE_ID‘ y ‘SYMMETRIC_KEY‘, va a registrar un dispositivo IoT Edge con el nombre del contenedor utilizando IoT Hub DPS. No es una imagen 100% funcional, pero para la prueba que vamos a hacer nosotros es suficiente. Puedes ver el código de la imagen en GitHub si tienes interés 🙂

    docker run --privileged=true -e SCOPE_ID=$SCOPE_ID -e SYMMETRIC_KEY=$primaryKey jorturfer/iot-edge-dps-post
    

    Una vez se ejecute y se conecte, puedo comprobar que se ha aprovisionado correctamente ejecutando az iot hub device-identity list --ee true -n $iotHubName --query="[*].tags", que esta vez me devolverá:

    {
      "IoTEdge": "FixedBuffer"
    }
    

    Conclusión

    Una vez más, me dejo muchas cosas chulas en el tintero, pero la verdad es que si las revisásemos todas en detalle, ni yo mismo leería mi entrada por el tamaño final…

    Hemos hecho una pequeña aproximación al aprovisionamiento de dispositivos utilizando IoT Hub DPS ya que es una herramienta que personalmente me parece muy potente y útil. Respecto a su coste hay que decir también que, aunque sí tiene coste, es ridículo a cambio del trabajo que ahorra, nada más y nada menos que €0,085 por cada 1.000 operaciones.

    En futuras entradas, me gustaría unificar todos y cada uno de estos conceptos que hemos ido planteando tanto aquí como en las entradas que escribí para Plain Concepts en una entrada con una ejemplo funcional E2E. ¿Qué opinas? ¿Crees que sería interesante? ¿Conocías previamente IoT Hub Device Provisioning Service?

    **La entrada IoT Hub DPS: Creando dispositivos bajo demanda se publicó primero en Fixed Buffer.**

    » Leer más, comentarios, etc...

    Variable not found

    Enlaces interesantes 419

    noviembre 03, 2020 07:05

    Enlaces interesantes

    Durante las entregas anteriores hemos estado comentando los códigos de estado HTTP coincidentes con la entrega de enlaces interesantes de la semana, por aquello de tener culturilla general. En esta ocasión le llegó el turno al HTTP 419, que simplemente no existe, por lo que omitiremos esta sección hasta volvamos a coincidir de nuevo ;)

    Y dicho esto, ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :)

    Por si te lo perdiste...

    .NET Core / .NET

    ASP.NET Core / ASP.NET / Blazor

    Azure / Cloud

    Conceptos / Patrones / Buenas prácticas

    Data

    Web / HTML / CSS / Javascript

    Visual Studio / Complementos / Herramientas

    Xamarin / Mobile

    Otros

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    info.xailer.com

    Animaciones en Xailer 7

    octubre 27, 2020 07:24

    Las animaciones son clases que permiten establecer distintos tipos de animaciones en cualquier control visual. Su funcionamiento se basa en utilizar Futuros para de esta forma no provocar la congelación de la aplicación durante todo el tiempo que dure la animación. Para más información acerca de los futuros, consulte el artículo donde se trata.

    Demo de animaciones en el canal de Xailer en YouTube

    La clase base de todas las animaciones es TAnimation la cual ofrece toda funcionalidad necesaria para realizar cualquier tipo de animación. Existen dos clases que heredan de TAnimation que están especializadas en realizar la animación de una forma muy sencilla:

    TAniNumProperty: Esta clase permite establecer la animación en base a una de las propiedades del control.

    En este ejemplo puede entender perfectamente su comportamiento. El objeto TAniNumProperty controla la propiedad ‘nWidth‘ del control ‘oEdit1‘ y va modificar dicha propiedad desde su valor actual (lStartFromCurrent a verdadero) hasta un valor de 300 (nStopValue) y lo hará en un periodo de 1 segundo (nDuration). Todo el proceso comenzará en cuanto la propiedad lEnabled se encuentre a verdadero o ejecute su método Start(). Es así de fácil establecer una animación en cualquier control.

    TAniControlSize: Esta clase permite establecer la animación en base a las dimensiones del control. Es decir, de su tamaño marcado por sus propiedades: nLeft, nTop, nWidth y nHeight. Su uso se restringe básicamente a modificar el tamaño de cualquier control desde una posición y tamaño inicial a una posición y tamaño final.

    En este ejemplo puede entender perfectamente su comportamiento. El objeto TAniControlSize controla las dimensiones del control ‘oMemo1‘ y va modificar dichas dimensiones desde su valor actual (lStartFromCurrent a verdadero) hasta un valor de {96,48,300,200} (aStopValues) y lo hará en un periodo de 1 segundo (nDuration). Todo el proceso comenzará en cuanto la propiedad lEnabled se encuentre a verdadero o ejecute su método Start().

    Ambas clases tienen además una serie de propiedades muy interesantes, que son:

    • nInterpolation: Esta propiedad permite establecer el tipo de animación. La forma más básica sería una interpolación lineal (aiLINEAR) que es la única existente en Xailer Personal y Xailer Professional. Xailer Enterprise dispone de muchos más tipos: rebote, explosión, elástica y circular.
    • nDelay: Permite establecer una demora en milisegundos en el inicio de la animación. Esta propiedad es muy útil cuando se pone la propiedad lEnabled a verdadero en el propio IDE y por lo tanto es necesario que se cargue y muestre el formulario antes de que se procese la animación.
    • lLoop: Si esta propiedad está a verdadero la animación comenzará de nuevo automáticamente cuando termine. Y por lo tanto la animación seguirá hasta que lEnabled pase a ser falso.
    • lReverse: Si esta propiedad está a verdadero se intercambian los valores de ‘Start‘ y ‘Stop
    • lAutoReverse: Esta propiedad permite que se haga una reversión automática cuando termine un ciclo, de tal forma que en el siguiente ciclo se convierte el efecto contrario. Esta propiedad conjuntada con la propiedad lLoop a verdadero provoca el clásico efecto de zoom in- zoom out.
    • nSyncsInSec: Esta propiedad permite establecer el número de sincronizaciones que se harán del control por segundo. Cuanto mayor sea este número, en principio, más suave y fluido se verá el movimiento, pero no es así. Dependerá de la velocidad de su PC y el trabajo que tenga que hacer la animación, ya que puede ser que arrastre a otros controles que también estén visibles.

    Espero que estas animaciones sean de su agrado. Estarán disponibles en Xailer 7.

    » Leer más, comentarios, etc...

    info.xailer.com

    Exprimiendo el FOR EACH de Harbour

    octubre 21, 2020 10:03

    La cláusula FOR EACH que Harbour incorporó al lenguaje CA-Clipper fue una gran mejora, pero hay mucha gente que sólo lo utiliza de su forma más básica:

    FOR EACH value IN aValues
      ? value
    NEXT

    Sin embargo ofrece multitud de posibilidades. Vamos a enumerar unas cuantas:

    • Iterar sobre más de una variable:
      FOR EACH a, b, c IN aVal, cVal, hVal
        ? a, b, c
      NEXT
    • Establecer orden descendente:
      FOR EACH a IN aVal DESCEND
        ? a
      NEXT
    • Asignar caracteres de una cadena de forma directa: (atención a la @)
      s := "abcdefghijk"
      FOR EACH c IN @s
        IF c $ "aei"
          c := Upper( c )
        ENDIF
      NEXT
      ? s // AbcdEfghIjk
    • Control por POO sobre las variables de iterador:
      • :__enumIndex() retorna el actual ordinal en la iteración. El equivalente a nFor
      • :__enumIsFirst() retorna si es el primer elemento en la iteración
      • :__enumIsLast() retorna si es el último elemento en la iteración
      • :__enumBase() retorna la variable base que está siendo procesada. Útil cuando se procesan más de una variable.
      • :__enumValue() retorna el valor de la variable que está siendo procesada. Útil cuando se procesan más de una variable.
        FOR EACH a IN aVal
          ? a:__enumIndex()
        NEX

    Harbour incluso permite modificar como se debe de recorrer la iteración utilizando los métodos :__enumStart(), :__enumSkip() y :__EnumStop(). Tenéis un ejemplo de uso en el fichero \harbour\tests\foreach2.prg.

    Espero que os haya sido de utilidad.

    Un saludo

    » Leer más, comentarios, etc...

    Fixed Buffer

    Creando módulos especializados para Azure IoT Edge

    octubre 20, 2020 08:00

    Tiempo de lectura: 12 minutos
    Imagen ornamental para la entrada "Creando módulos especializados para Azure IoT Edge"

    Después de unas semanas muy liado con unas complicaciones personales (solo he tenido tiempo de publicar la review de NDepend), ya estamos de vuelta y además con una entrada que toca muchos palos. Vamos a hablar de Azure, vamos a hablar de C# y vamos a hablar de Docker.

    Por el título de la entrada, no es ningún secreto el tema del que vamos a hablar de Azure IoT Edge y sus módulos. La idea de escribir esto es porque no hace mucho escribí en el blog de Plain Concepts una entrada sobre cómo desplegar cargas de trabajo ‘on-prem’ utilizando IoT Hub.

    Aunque lo que se plantea en la entrada es totalmente cierto y el runtime de IoT Edge permite correr imágenes Docker sin más, la verdad es que nos permite ir un paso más allá. IoT Edge nos va a permitir crear módulos especializados con los que vamos a poder utilizar las funcionalidades propias de IoT Hub y todo ello empaquetado en una imagen Docker. Con esto vamos a conseguir que nuestro IoT Edge deje de ser un ‘nodo’ de Docker al que podemos mandar cosas y se convierta en un verdadero punto de computación de borde.

    ¿Por qué debería usar módulos de IoT Hub en lugar de imágenes Docker normales?

    Es cierto que con una imagen Docker las cosas soy mucho más simples comunes a la hora de desarrollar. Tienes un fichero Dockerfile, expones unos puertos, creas tus APIs, y si necesitas comunicarte con IoT Hub para enviar mensajes, puedes utilizar directamente el cliente de IoT Hub para el lenguaje con el que estés trabajando. ¿Por qué debería entonces complicarme la vida?

    Pues, aunque los módulos implican añadir ciertos cambios a la hora de programar (más bien de desplegar), no dejan de ser una aplicación de consola sin más, al que se le ha dotado de funcionalidad para interactuar con el runtime sobre el que está corriendo. Esto se traduce en 2 grandes ventajas:

    • Delegamos en el runtime el enrutado de los mensajes entre los diferentes módulos.
    • Tenemos a nuestra disposición toda la potencia de IoT Hub en cada módulo (envío de métricas/mensajes, dispositivo gemelo, mensajes desde la nube al dispositivo, etc…), utilizando la conexión del runtime de IoT Edge.

    ¿Las ventajas son realmente ventajas?

    Vale, he planteado las 2 cosas que a mi parecer son las principales mejoras, ¿pero esto realmente son mejoras? Analicemos la primera de ellas, el enrutado de mensajes.

    El planteamiento de IoT Edge es tener pequeños programas que vayan añadiendo procesado sobre los datos de entrada. Imaginemos un caso donde tenemos una cámara capturando imágenes de una autopista, las imágenes que se obtienen se procesan y por último se etiquetan. Es cierto que esto se podría hacer de manera monolítica todo en el mismo proceso, pero eso dificulta el reutilizar código. Si separamos la adquisición en una imagen, el procesado en otra y el etiquetado en otra, vamos a poder cambiar una de las piezas por otra y reutilizar al máximo.

    Por poner un caso, podríamos tener otro escenario en el que solo queramos capturar imágenes y mandarlas a un almacenamiento, el hecho de que la adquisición este modularizada, nos va a permitir reaprovechar ese módulo y añadir solo la parte nueva de persistencia.

    Hecha esta aclaración sobre la modularización y reutilización, podríamos extraer varios módulos del conjunto de los dos escenarios:

    • Adquisición de imágenes.
    • Procesado de las imágenes.
    • Etiquetado de las imágenes.
    • Persistencia de las imágenes.

    Y su representación sería algo así:

    La imagen muestra un diagrama de bloques en que que hay 2 zonas. La primera zona dice "escenario 1" y tienes 3 bloques unidos por flechas. Desde bloque Captura sale una flecha hacia Procesado, y desde ahí sale una flecha hacia Etiquetado. La segunda zona es "escenario 2" y tiene una flecha que sale desde el bloque Captura y llega a Persistencia

    En una situación multi contenedor corriendo sobre Docker, simplemente podríamos decirle a un contenedor, por ejemplo, Captura, que una vez que tenga una imagen la mande al siguiente contenedor siguiendo un modelo push (es el propio contenedor el que se encarga de empujar los datos al siguiente). Incluso podríamos cambiar el planteamiento y utilizar un modelo pull donde cada paso obtenga los datos del paso previo.

    Esto tiene problemas a la hora de reutilizar los módulos, ya que cada módulo necesita tener consciencia o desde donde obtiene los datos o a donde los tiene que enviar. Tenemos acoplamiento entre los módulos. Si por ejemplo quisiéramos añadir al escenario 1 la persistencia entre la captura y el procesado (o en cualquier otra posición), tendríamos que modificar el código del módulo para que se adapte al nuevo escenario, no nos vale solo con modificar las configuraciones.

    En este punto, voy a aclarar que en programación casi todo es posible, e incluso esto se puede solucionar añadiendo código o creando nosotros mismos sistemas complejos que lo solucionen. En algunos casos puede que sea necesario, pero generalmente es tiempo y esfuerzo en balde.

    Para evitar esto, podríamos añadir algún sistema de colas donde cada módulo publique los resultados a una cola y lea las entradas desde otra cola. De este modo cada módulo no necesita saber nada al respecto del anterior ni del siguiente, simplemente lee los datos desde una cola y los deja en otra cola. El problema de este planteamiento es que somos nosotros los que vamos a tener que mantener la infraestructura de colas… y aquí es donde coge importancia el delegar en el runtime el enrutado de mensajes.

    IoT Edge nos proporciona de serie un sistema de enrutado que podemos utilizar desde los módulos especializados de IoT Edge. Nosotros simplemente vamos a definir una o más entradas para nuestro módulo de IoT Edge, y de igual manera vamos a escribir una serie de salidas del módulo. Después, desde la propia configuración de IoT Edge vamos a definir mediante lenguaje de consulta el coger las salidas un el módulo A y enviarlas a la cola de entrada del módulo B. Es importante aquí el punto de lenguaje de consulta, ya que vamos a poder aplicar condiciones para que la ruta se cumpla.

    Esto desacopla totalmente los módulos entre si, ya que estamos usando un sistema de colas, pero en este caso es el propio runtime quien lo gestiona.

    Por otro lado, comentaba que la segunda gran ventaja era que el propio módulo de IoT Edge era capaz de ser un dispositivo IoT en si mismo, aprovechando la conexión del runtime. Esto significa que por ejemplo que podemos utilizar el sistema de rutas para enviar métricas a IoT Hub, es decir, es posible especificarle a IoT Edge que coja la cola de salida del módulo A y que directamente la envíe como una métrica a IoT Hub.

    Creando la solución IoT Edge

    Como hablar siempre es muy fácil, vamos a crear un módulo IoT Edge desde 0, que enrute los mensajes generados desde el simulador de pruebas que ofrece Microsoft. Lo primero de todo es preparar el entorno, el resumen ejecutivo de los elementos que necesitamos es .Net Core y una extensión para Visual Studio o Visual Studio Code que nos de soporte para desarrollar módulos IoT Edge. Una vez instalada la extensión, seguiremos la documentación específica para configurar la extensión con nuestro IoT Hub.

    En este caso por versatilidad, yo estoy utilizando Visual Studio Code, por lo que, una vez instalada y configurada la extensión, basta con escribir «Azure IoT Edge: New IoT Edge solution» en la paleta de comandos.

    La imagen muestra la paleta de Visual Studio Code con el comando "Azure IoT Edge: New IoT Edge solution" escrito

    Esto iniciará un pequeño asistente en el que nos va a pedir una ruta, un nombre para la solución, y nos va a ofrecer una plantilla de módulo IoT Edge en varios lenguajes diferentes. Una vez seleccionado el nombre del módulo, nos va a pedir el último paso que es decirle cual es el registro de Docker en el que se tiene que subir la imagen con el módulo. Este registro puede ser público o privado.

    Una vez hecho esto, ya tenemos listo el andamiaje del proyecto, y ya podemos editar y generar el módulo, desplegarlo, depurarlo,… Este andamiaje está compuesto de varios ficheros que vamos a analizar.

    La imagen muestra la plantilla de IoT Edge end Visual Studio Code

    El fichero .env contendrá variables que se reemplazan durante el proceso de generación. Principalmente valores sobre el registro de Docker.

    Dentro de la carpeta .vscode, se ha creado el código necesario para ejecutar en local el código (que no deja de ser una aplicación de consola), o para depurarlo dentro de un contenedor Docker.

    La carpeta config va a contener los ficheros de configuración que generamos como artefacto de salida del proceso. Estos ficheros son muy útiles ya que son el json que podemos utilizar para desplegar sobre un dispositivo IoT Edge el conjunto de módulos que definamos. Esto es así porque realmente hemos creado una solución completa de IoT Edge que contiene módulos, algunos pueden ser de terceros y algunos nuestros (como es el caso).

    En la carpeta modules es donde encontramos la magia. Aquí es donde vamos a encontrar los diferentes módulos que estemos creando. Ahora entraremos en detalle.

    Por último, nos encontramos los ficheros deployment.template.json y deployment.debug.template.json. Estos ficheros son los que van a servir de entrada para que durante el proceso de generación del módulo se cree el artefacto de salida en la carpeta config. La principal diferencia entre ellos es que el que contiene debug nos va a permitir depurar el código del contenedor.

    El módulo IoT Edge

    Para acabar de revisar que tenemos, cuando entramos en la carpeta modules nos encontramos con un buen número de ficheros Dockerfile, un fichero module.json y un program.cs.

    Si comprobamos que contiene el fichero program.cs, nos encontramos con que no es más que una aplicación de consola. Hay que reconocer que la plantilla no es todo lo óptimo que debería ser en cuanto a buenas prácticas, pero podríamos reducirlo a algo así:

    using System;
    using System.Runtime.Loader;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.Devices.Client;
    using Microsoft.Azure.Devices.Client.Transport.Mqtt;
    
    namespace SampleModule
    {
        class Program
        {
            static int counter;
    
            static async Task Main(string[] args)
            {
                await InitAsync();
    
                var cts = new CancellationTokenSource();
                AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
                Console.CancelKeyPress += (sender, cpe) => cts.Cancel();
                await WhenCancelledAsync(cts.Token);
            }
    
            public static Task WhenCancelledAsync(CancellationToken cancellationToken)
            {
                var tcs = new TaskCompletionSource<bool>();
                cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
                return tcs.Task;
            }
    
            static async Task InitAsync()
            {
                MqttTransportSettings mqttSetting = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only);
                ITransportSettings[] settings = { mqttSetting };
    
                ModuleClient ioTHubModuleClient = await ModuleClient.CreateFromEnvironmentAsync(settings);
                await ioTHubModuleClient.OpenAsync();
    
                await ioTHubModuleClient.SetInputMessageHandlerAsync("input1", PipeMessage, ioTHubModuleClient);
            }
    
            static async Task<MessageResponse> PipeMessage(Message message, object userContext)
            {
                int counterValue = Interlocked.Increment(ref counter);
    
                var moduleClient = userContext as ModuleClient;
                if (moduleClient == null)
                {
                    throw new InvalidOperationException("UserContext doesn't contain " + "expected values");
                }           
                
                //Encolamos el mensaje como salida
                await moduleClient.SendEventAsync("output1", message);
                return MessageResponse.Completed;
            }
        }
    }
    

    Si analizamos el código (he cambiado ligeramente para usar await en vez de .Wait() respecto a la plantilla y borrado código que no aporta), podemos comprobar que tenemos un Main que inicializa el dispositivo llamando a InitAsync (Init en la plantilla original). Dentro de este método, se inicializa el módulo IoT Edge a través del tipo ModuleClient.

    Sobre este módulo registramos un manejador para cada vez que llegue un mensaje por la cola «input1» con la línea:

    await ioTHubModuleClient.SetInputMessageHandlerAsync("input1", PipeMessage, ioTHubModuleClient);
    

    Añadir nuevas entradas es tan simple como registrar más manejadores para otras colas de entrada.

    Para escribir los mensajes a una cola de salida, se utiliza la línea:

    await moduleClient.SendEventAsync("output1", message);
    

    En este caso, simplemente estamos utilizando el mensaje de la cola de entrada «input1» del módulo IoT Edge y lo estamos colocando en la cola de salida «output1».

    Adicionalmente a lo que nos ofrece la plantilla, el módulo nos ofrece también varios métodos con los que vamos a poder ampliar la utilidad del módulo mediante configuración de dispositivo gemelo, recepción de mensajes CloudToDevice, o llamadas a métodos directos. Esto se consigue simplemente añadiendo a la inicialización las líneas:

    await ioTHubModuleClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertiesUpdateAsync, ioTHubModuleClient);
    await ioTHubModuleClient.SetMessageHandlerAsync(HandleMessageAsync,ioTHubModuleClient);
    await ioTHubModuleClient.SetMethodHandlerAsync("hello",HelloMethodHandlerAsync,ioTHubModuleClient);
    

    A este código hay que añadirle los callback de los manejadores, para no alargar una entrada ya de por si larga, puedes consultar el código completo en el repositorio de Github.

    Hay que tener en cuenta que tanto las colas de entrada y salida, como el resto de las funcionalidades como métodos directos o dispositivo gemelo están ligados al módulo y estarán disponibles en cualquier runtime de IoT Edge donde despleguemos el módulo y dependerá de si queremos usarlos o no.

    Una vez visto el código, ¿por qué tantos Dockerfile?. La idea de que haya tantos es que estamos creando un módulo que queremos que se pueda desplegar en cualquier sitio que pueda correr IoT Edge, y esto son muchas arquitecturas de microprocesador diferente. Gracias a tener todos esos Dockerfile ya en la plantilla, nos abre la puerta a poder hacer que nuestro módulo de IoT Edge corra en Linux, Windows, en microprocesadores AMD, ARM,…

    Vamos a poder configurar la arquitectura de destino simplemente ejecutando en la paleta de comandos «Azure IoT Edge: Set Default Target Platform for Edge Solution«. Esto nos va a dar varias opciones disponibles, que son las que están definidas en el fichero module.json, donde vamos a relacionar las arquitecturas con los Dockerfile concretos entre otras cosas como especificar la versión del módulo.

    Actualmente, Docker soporta de forma experimental el crear manifiestos de imágenes para múltiples arquitecturas simultáneamente, siendo el runtime de Docker el que se descarga la imagen de su arquitectura especifica.

    Una vez que hemos terminado de configurar y programar nuestro módulo, vamos a poder generar la imagen o imágenes y subirlas al repositorio haciendo click derecho sobre el json del módulo o de la solución y pulsando sobre «Build and Push IoT Edge Module Image» o «Build and Push IoT Edge Solution» respectivamente.

    En caso de que hayamos elegido la segunda opción, además de generar las imágenes y subirlas, va a generar el fichero de deployment en la carpeta config de la solución.

    Configurando las rutas y desplegando la solución

    Ya casi estamos listos para desplegar la solución con nuestro módulo en un dispositivo IoT Edge. Tenemos el módulo listo, pero para poder probar el sistema de módulos de manera sencilla, vamos a necesitar desplegar varios módulos juntos.

    Por suerte la plantilla de la solución de IoT que hemos usado, ya nos ha creado los ficheros deployment con un módulo extra de simulación, que va a sacar por su cola de salida «temperatureOutput«. En la sección de rutas (routes) del json de despliegue deployment.template.json vamos a definir los diferentes enrutados internos y externos de los mensajes como parte de $edgeHub. En este caso la plantilla ya nos ofrece 2 rutas que son desde la salida del simulador a la entrada del módulo, y desde la salida del módulo a $upstream. Es importante aclarar $upstream significa que el mensaje será enviado directamente a IoT Hub como una métrica del dispositivo:

    "routes": {
      "SampleModuleToIoTHub": "FROM /messages/modules/SampleModule/outputs/* INTO $upstream",
      "sensorToSampleModule": "FROM /messages/modules/SimulatedTemperatureSensor/outputs/temperatureOutput INTO BrokeredEndpoint(\"/modules/SampleModule/inputs/input1\")"
    }
    

    Cada vez que actualicemos la plantilla de despliegue, será necesario generar de nuevo el manifiesto de despliegue para la arquitectura concreta. Para esto vale con hacer click derecho sobre el json de la plantilla y seleccionar «Generate IoT Edge Deployment Manifest«.

    Por último, ya solo queda desplegar la solución. Para esto es suficiente con hacer click derecho sobre el json del manifiesto en la carpeta de config y seleccionar «Create Deployment for Single Device«. Con esto vamos a poder elegir el dispositivo sobre el que queremos desplegar y «et voilà«. Ya hemos desplegado nuestro propio módulo IoT Edge y hemos configurado sus mensajes.

    Conclusión

    Soy consciente de que se han quedado muchas cosas en el tintero, no he entrado a como depurar el código, usar el simulador, configuraciones complejas,… La idea detrás de todo esto era dar una visión de alto nivel sobre cómo es posible crear y desplegar módulos IoT Edge sin sufrir mucho por el camino.

    Siempre que he usado IoT Edge he intentado usar directamente imágenes stand-alone porque pensaba que el sistema de módulos era complejo y que no valía la pena entrar en él, pero evidentemente me equivocaba.

    Una cosa importante que no he dicho pero que también es importante, es que IoT Edge no tiene coste adicional, sobre el coste de mensajes que tengas en IoT Hub y además funciona con el tier gratuito, por lo que es posible utilizar IoT Edge gratis siempre que no mandemos más de 8 mil mensajes al día, momento en el que se cortará la comunicación con IoT Hub hasta el día siguiente.

    De momento, te dejo el enlace a la documentación oficial de IoT Edge, donde vas a poder encontrar no solo como desarrollar módulos, sino como hacer ajustes finos en muchas partes del sistema.

    ¿Y tú qué opinas? ¿Conocías los módulos para IoT Edge? Déjame en los comentarios si quieres que haga otra entrada más en detalle sobre cómo desarrollar módulos, depurarlos y hacer configuraciones más complejas.

    **La entrada Creando módulos especializados para Azure IoT Edge se publicó primero en Fixed Buffer.**

    » Leer más, comentarios, etc...

    Header Files

    QDateEdit con valor nulo

    octubre 17, 2020 10:07

    A principios de este año se cumplieron 438 años desde la promulgación de la bula Inter Gravissimas, el 24 de febrero de 1582, en la que el Para Gregorio XIII instituía un nuevo calendario; el que ahora conocemos como calendario gregoriano y que es el vigente en muchos países, especialmente occidentales.

    A raíz de esto me acordé (aunque la pandemia dejó este borrador en el olvido) de un pequeño tunning que tuve que hacer tiempo atrás al widget de selección de fechas de Qt, QDateEdit. Uno de los requerimientos era el poder tener una fecha nula, algo que indicase que el usuario no había querido introducir una fecha (de nacimiento en este caso, que era opcional).

    QAbstractSpinBox

    » Leer más, comentarios, etc...

    Picando Código

    Sega celebra 60 años con una promoción especial en Steam y 60 días de contenido

    octubre 15, 2020 11:00

    Go Sega
    2020 marca el aniversario N° 60 de SEGA, una de las compañías de videojuegos más importantes de la historia. Sega West lo celebra con 60 días de contenidos. La semana de festejos empezó ayer miércoles 14 de octubre en Steam. En la página de la promoción de Steam podemos encontrar descuentos en varios títulos. Particularmente interesante también varios minijuegos nuevos con inspiración retro, gratuitos, creados por los estudios SEGA específicamente para el 60° aniversario:

    • Armor of Heroes – Relic Entertainment™ – Un título multijugador de tanques para hasta 4 jugadores en combate versus y cooperativo offline. Disponible desde el 15 al 19 de Octubre.
    • Endless Zone – Amplitude Studios™ – El universo Endless y la Fantasy Zone™chocan en este shoot ’em up de scroll lateral. Disponible desde el 16 al 19 de Octubre.
    • Streets of Kamurocho – SEGA® – Kiryu y Majima de la premiada serie Yakuza™de SEGA pelean en las calles de Kamurocho en un formato nuevo pero familiar. Disponible desde el 17 al 19 de Octubre.
    • Golden Axed – SEGA® – Una versión sin finalizar y nunca antes vista de un proyecto cancelado llamado Golden Axe™: Reborn. Disponible desde el 18 al 19 de Octubre. Sí, solamente un día.

    Golden Axed me resulta bastante interesante. El original fue uno de los juegos en los que invertí bastantes fichas en la época de los arcades, estaba genial. Ya podemos ver imágenes y video del gameplay de Golden Axed y agregarlo a nuestro wishlist en Steam:

    Al comienzo de la década, SEGA Studios Australia estaba trabajando en una serie de reboots de propiedad intelectual clásica de SEGA, conocidos colectivamente como la serie SEGA Reborn. Esto incluia reboots 2.5D de Golden Axe, Altered Beast y Streets of Rage, así como una versión runner infinita de Shinobi, todo incluido en un mundo hub universal. Lamentablemente SEGA Studio Australia cerró sus puertas en 2013, y con eso el proyecto SEGA reborn se perdió en los anales de la historia de los videojuegos. Con la ocasión del 60° aniversario de SEGA, como regalo especial y para decir “¡Gracias!” a nuestros fans, SEGA está publicando un prototipo funcional de Golden Axe Reborn, un nivel único creado como prueba de concepto. Contiene representaciones de gore extremas, decapitaciones, corte de enemigos en dos y mucha, mucha sangre.

    Con suerte de repente vemos una nueva versión de Golden Axe en breve si a esto le va bien.

    Para canjear tus juegos gratis, todo lo que necesitarás hacer es dirigirte a la página del producto en Steam indicada arriba y hacer click en “Jugar Juego”. Esto agrega al título a tu biblioteca, y será tuyo para siempre. Actúa rápido, ya que una vez que estén retirados de Steam, será para siempre. Además, los usuarios de Steam que vinculen sus cuentas a su dirección de e-mail en sega60th.com recibirán un clásico de SEGA Saturn, NiGHTS™ into Dreams, gratuitamente durante los 60 días de celebración. Visiten el sitio de Sega 60 para ver más contenidos, historia y descargas de arte para nuestra biblioteca de juegos SEGA.

    Los nuevos minijuegos serán lanzados a las 10am (Hora Estándar del Pacífico) en el día especificado y serán removidos a las 10am (Hora Estándar del Pacífico) en el día especificado. El período de 60 días es desde el 14 de Octubre al Domingo 13 de Diciembre.

    Desde ayer las redes sociales y el sitio web del 60º Aniversario están iluminadas con 60 días de contenido de SEGA. Se están realizando let’s plays, competiciones regulares, entrevistas a incondicionales de SEGA de todos los rincones del negocio, y ofreciendo contenido de video y editorial cubriendo diferentes partes de la vasta historia de SEGA. Podemos mantenernos al tanto en las cuentas de SEGA en Twitter, Facebook, Instagram y Youtube para ver actualizaciones diarias de contenido.

    YouTube Video

    The post Sega celebra 60 años con una promoción especial en Steam y 60 días de contenido first appeared on Picando Código.

    » Leer más, comentarios, etc...

    Meta-Info

    ¿Que es?

    Planeta Código es un agregador de weblogs sobre programación y desarrollo en castellano. Si eres lector te permite seguirlos de modo cómodo en esta misma página o mediante el fichero de subscripción.

    rss subscripción

    Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
    planetacodigo

    planetacodigo

    Si tienes un weblog de programación y quieres ser añadido aquí, envíame un email solicitándolo.

    Idea: Juanjo Navarro

    Diseño: Albin