sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
⎇ 84e3737e10e17927380ed3e232e50cebfdd39123
recipes/atuq/extensions/descargas/fondo.js
Historia
Quién tocó qué
crudo
atuq: una descarga deja de ser un archivo con nombre y pasa a ser un objeto con identidad
1"use strict";
2// descargas — una descarga con identidad (SDD 26 §6.2), v1.
3//
4// Cuando una descarga TERMINA, esta extensión le pasa al host nativo la ruta del
5// fichero, su nombre y de dónde salió. El host lo ingiere a un CAS BLAKE3 —el de
6// `arje-cas`, el mismo formato y el mismo hash que usan arje, takana y tejido— y
7// contesta el hash, el tamaño, con qué nombre apareció la PRIMERA vez y cuántas
8// veces se bajó ese mismo contenido.
9//
10// Para qué: ningún navegador trata una descarga como algo con identidad. Para
11// todos es un blob con nombre, y por eso lo bajás dos veces y no lo sabés. Acá el
12// mismo contenido con otro nombre es UN objeto y dos nombres, y se dice.
13//
14// ── LO QUE NO HACE, Y ES DELIBERADO ──────────────────────────────────────────
15// No toca el fichero que bajaste. El host lo COPIA al CAS y lo deja donde estaba.
16// Mover o borrar lo que alguien acaba de bajar —aunque «ya esté en el CAS»— no es
17// una decisión que le toque a un navegador tomar por su cuenta.
18//
19// Y no intercepta la descarga: la observa cuando terminó. Meterse en el medio
20// obligaría a decidir qué hacer cuando el CAS falla, y la respuesta correcta a eso
21// («que la descarga siga igual») es exactamente lo que se consigue no metiéndose.
22
23const HOST = "puriy_costura";
24
25let puerto = null;
26// La descarga que motivó cada mensaje, para poder pintar la insignia en su
27// respuesta: el host devuelve el `id` que le mandamos, y el `id` de una descarga
28// es justo un entero.
29const pendientes = new Map();
30
31function di(s) {
32 try { dump("DESCARGAS " + s + "\n"); } catch (e) {}
33}
34
35function conectar() {
36 try {
37 puerto = browser.runtime.connectNative(HOST);
38 puerto.onMessage.addListener(alRecibir);
39 puerto.onDisconnect.addListener(p => {
40 di("DESCONECTADO " + (p.error ? p.error.message : "sin error"));
41 puerto = null;
42 });
43 di("CONECTADO " + HOST);
44 } catch (e) {
45 di("SIN HOST " + e.message);
46 puerto = null;
47 }
48}
49
50function enviar(msg) {
51 if (!puerto) { conectar(); }
52 if (!puerto) { return false; }
53 try {
54 puerto.postMessage(msg);
55 return true;
56 } catch (e) {
57 di("ENVÍO FALLÓ " + e.message);
58 puerto = null;
59 return false;
60 }
61}
62
63function alRecibir(r) {
64 if (!r || r.ok !== true) {
65 di("RECHAZO " + (r && r.error ? r.error : "sin error"));
66 return;
67 }
68 if (r.verb !== "cas.ingest" || !r.object) { return; }
69 const o = r.object;
70 di("INGERIDO " + o.hash + " nombre=" + o.name + " bytes=" + o.bytes +
71 " veces=" + o.times + " dedup=" + (r.deduped === true));
72 pendientes.delete(r.id);
73 pintar(o, r.deduped === true);
74}
75
76// El aviso es la insignia del botón, no un modal: es información, no una alarma.
77// Sólo marca cuando hay algo que decir —que ese contenido ya lo tenías—, porque
78// una insignia que está siempre encendida deja de leerse.
79function pintar(o, dedup) {
80 try {
81 if (dedup) {
82 browser.browserAction.setBadgeText({ text: String(o.times) });
83 browser.browserAction.setBadgeBackgroundColor({ color: "#1b6470" });
84 browser.browserAction.setTitle({
85 title: "Esto ya lo tenías: «" + o.name + "»\n" +
86 "es la vez " + o.times + " que bajás el mismo contenido\n" +
87 "blake3:" + o.hash.slice(0, 16) + "…",
88 });
89 } else {
90 browser.browserAction.setBadgeText({ text: "" });
91 browser.browserAction.setTitle({
92 title: "«" + o.name + "» — " + o.bytes + " bytes\nblake3:" +
93 o.hash.slice(0, 16) + "…",
94 });
95 }
96 browser.browserAction.getBadgeText({}).then(
97 t => di("INSIGNIA texto=" + JSON.stringify(t)),
98 e => di("INSIGNIA NO SE PUDO LEER " + e.message));
99 } catch (e) { di("INSIGNIA FALLÓ " + e.message); }
100}
101
102async function terminada(id) {
103 let item;
104 try {
105 const r = await browser.downloads.search({ id });
106 item = r && r[0];
107 } catch (e) {
108 di("BUSCAR FALLÓ " + e.message);
109 return;
110 }
111 if (!item || !item.filename) {
112 di("SIN RUTA para la descarga " + id);
113 return;
114 }
115 // `filename` de la API de descargas es la ruta ABSOLUTA en disco, no el nombre
116 // (el nombre visible es el último segmento). El host además la exige absoluta.
117 const nombre = item.filename.split("/").pop();
118 di("TERMINADA " + id + " " + item.filename + " (" + item.fileSize + " bytes)");
119 pendientes.set(id, nombre);
120 enviar({
121 id,
122 verb: "cas.ingest",
123 path: item.filename,
124 name: nombre,
125 origin: item.url || "",
126 });
127}
128
129browser.downloads.onChanged.addListener(delta => {
130 if (delta.state && delta.state.current === "complete") {
131 terminada(delta.id);
132 }
133});
134
135conectar();
136di("LISTA v1 observa descargas terminadas; no toca el fichero");