README corto y sin comentarios en el código
This commit is contained in:
parent
cb551e2a3c
commit
7ed0276704
9 changed files with 7 additions and 165 deletions
120
README.md
120
README.md
|
|
@ -1,117 +1,11 @@
|
||||||
# acampada26s.net
|
# acampada26s.net
|
||||||
|
|
||||||
Web de la acampada del 26S en Madrid. Comunicados, directo, cómo organizarse
|
Web de la acampada del 26S en Madrid. HTML y CSS, sin JavaScript.
|
||||||
y dónde están los colectivos.
|
|
||||||
|
|
||||||
HTML y CSS. Sin JavaScript en el navegador, sin cookies, sin analítica y sin
|
1. `site/` va a `/var/www/acampada26s.net`. `deploy/nginx.conf` y `deploy/hub.conf` a nginx.
|
||||||
peticiones a terceros. La tipografía va dentro del repo.
|
2. Directo es el HUB del pub OASIS (`/c`), servido en local en `127.0.0.1:3000`.
|
||||||
|
3. Comunicados, comisiones y debates salen de OASIS con `deploy/web26s.timer`. Rellenar `CUENTA` y `TRIBU` en `deploy/web26s.service`.
|
||||||
|
4. Publica quien está en la tribu pública de la acampada y lo sigue la cuenta de la acampada. Comunicado: post con `#comunicado26s`, primera línea de título. Comisión: subtribu pública. Debate: foro público.
|
||||||
|
5. Fotos subidas a mano: `tools/limpia-img.sh` antes, siempre.
|
||||||
|
|
||||||
La comunicación viva no depende de esta web: va por un pub de
|
Código AGPL-3.0. Textos CC BY-SA 4.0. Tipografía Anton, OFL.
|
||||||
[OASIS](https://code.03c8.net/krakenslab/oasis) (Secure Scuttlebutt). Si la web
|
|
||||||
cae o la tiran, el pub y lo publicado siguen en la red.
|
|
||||||
|
|
||||||
## Estructura
|
|
||||||
|
|
||||||
```
|
|
||||||
site/ lo que se sirve (copiar tal cual a /var/www/acampada26s.net)
|
|
||||||
deploy/ nginx y timer de systemd
|
|
||||||
tools/ generador desde OASIS y limpieza de imágenes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Qué sale de dónde
|
|
||||||
|
|
||||||
- **Directo**: enlace al HUB del pub (`/c?type=feed`). Lo sube la gente de
|
|
||||||
comunicación desde OASIS. No pasa por esta web.
|
|
||||||
- **Comunicados, comisiones y debates**: los genera `tools/` a partir de OASIS
|
|
||||||
cada dos minutos. Nadie edita HTML a mano.
|
|
||||||
- **Colectivos**: lista fija en `site/index.html`. Para añadir uno, un `<li>`
|
|
||||||
más. Solo webs o cuentas comprobadas.
|
|
||||||
|
|
||||||
## Publicar desde OASIS
|
|
||||||
|
|
||||||
Quien publica en la web: miembros de la tribu pública de la acampada **a los
|
|
||||||
que la cuenta de la acampada sigue**. Hacen falta las dos cosas. Unirse a una
|
|
||||||
tribu es un mensaje que cualquiera puede publicar sobre sí mismo (el código de
|
|
||||||
invitación solo lo comprueba la interfaz), así que la tribu sola no vale como
|
|
||||||
permiso. El follow lo firma la cuenta de la acampada y no se puede falsificar.
|
|
||||||
|
|
||||||
- Dar permiso: invitar a la tribu y seguir desde la cuenta de la acampada.
|
|
||||||
- Quitarlo: dejar de seguir. Sus comunicados desaparecen en la siguiente pasada.
|
|
||||||
|
|
||||||
Qué se publica:
|
|
||||||
|
|
||||||
- **Comunicado**: un post con `#comunicado26s`. La primera línea es el título.
|
|
||||||
Las imágenes adjuntas salen en la página. Si se borra en OASIS, se borra aquí.
|
|
||||||
- **Comisión**: una subtribu pública de la tribu de la acampada, creada por
|
|
||||||
alguien con permiso.
|
|
||||||
- **Debate**: un foro público abierto por alguien con permiso.
|
|
||||||
|
|
||||||
Las tribus privadas y lo que se escribe dentro de las tribus va cifrado; eso
|
|
||||||
no sale en la web ni debe salir.
|
|
||||||
|
|
||||||
La tribu tiene que ser pública (no anónima) y con invitación estricta, y la
|
|
||||||
tiene que crear la cuenta de la acampada.
|
|
||||||
|
|
||||||
## Imágenes
|
|
||||||
|
|
||||||
Las de los comunicados las sirve el pub (`/c/blob/...`). Las que se suban a
|
|
||||||
mano a `site/img/`, siempre limpias antes:
|
|
||||||
|
|
||||||
```
|
|
||||||
tools/limpia-img.sh site/img/*.jpg
|
|
||||||
```
|
|
||||||
|
|
||||||
El EXIF lleva GPS, modelo de móvil y hora. Caras tapadas salvo permiso expreso.
|
|
||||||
|
|
||||||
## Despliegue
|
|
||||||
|
|
||||||
Requisitos: nginx, certbot, el pub OASIS en la misma máquina, Node 22 y
|
|
||||||
Python 3.9+.
|
|
||||||
|
|
||||||
```
|
|
||||||
rsync -a --exclude comunicados/ site/ /var/www/acampada26s.net/
|
|
||||||
cp deploy/nginx.conf /etc/nginx/sites-available/acampada26s.net
|
|
||||||
cp deploy/hub.conf /etc/nginx/snippets/acampada26s-hub.conf
|
|
||||||
ln -s ../sites-available/acampada26s.net /etc/nginx/sites-enabled/
|
|
||||||
nginx -t && systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
Un `rsync` de `index.html` deja los bloques generados vacíos; el timer los
|
|
||||||
repone en dos minutos.
|
|
||||||
|
|
||||||
### Pub OASIS
|
|
||||||
|
|
||||||
`sh oasis.sh server`. El HUB web escucha en `:3000`; mejor solo en local
|
|
||||||
(`--host=127.0.0.1`). nginx publica `/c` en solo lectura y sin la IP de quien
|
|
||||||
visita.
|
|
||||||
|
|
||||||
### Generador
|
|
||||||
|
|
||||||
```
|
|
||||||
cp -r . /opt/26s
|
|
||||||
cp deploy/web26s.service deploy/web26s.timer /etc/systemd/system/
|
|
||||||
# rellenar CUENTA, TRIBU, OASIS_DIR y User en web26s.service
|
|
||||||
systemctl enable --now web26s.timer
|
|
||||||
```
|
|
||||||
|
|
||||||
A mano:
|
|
||||||
|
|
||||||
```
|
|
||||||
OASIS_DIR=~/oasis CUENTA=@...ed25519 TRIBU=%...sha256 node tools/extrae.js \
|
|
||||||
| python3 tools/genera.py site
|
|
||||||
```
|
|
||||||
|
|
||||||
`extrae.js` solo lee del sbot por su socket local. `genera.py` escapa todo lo
|
|
||||||
que viene de SSB, solo deja enlaces `http(s)` y escribe de forma atómica.
|
|
||||||
|
|
||||||
## Comprobaciones antes de subir
|
|
||||||
|
|
||||||
```
|
|
||||||
grep -rniE '<script|javascript:|\son[a-z]+=' site/ # nada
|
|
||||||
grep -rnoE 'https?://[^"]+' site/ # solo enlaces salientes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Licencia
|
|
||||||
|
|
||||||
Código: AGPL-3.0 (`LICENSE`). Textos e imágenes: CC BY-SA 4.0.
|
|
||||||
Tipografía Anton: SIL OFL 1.1 (`site/fonts/OFL.txt`).
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# Copiar a /etc/nginx/snippets/acampada26s-hub.conf
|
|
||||||
# OASIS escuchando en 127.0.0.1:3000 (node backend.js --public --no-open --host=127.0.0.1)
|
|
||||||
limit_except GET HEAD { deny all; }
|
limit_except GET HEAD { deny all; }
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
@ -9,7 +7,6 @@ proxy_set_header X-Forwarded-For "";
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 120s;
|
proxy_read_timeout 120s;
|
||||||
proxy_buffering on;
|
proxy_buffering on;
|
||||||
# add_header en location anula los del server: se repiten aquí todos menos la CSP.
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header Referrer-Policy "no-referrer" always;
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
# acampada26s.net
|
|
||||||
# Estático en /var/www/acampada26s.net (contenido de site/).
|
|
||||||
# /c -> HUB clearnet de OASIS (solo lectura) del pub de la acampada.
|
|
||||||
|
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
# Logs sin IP completa: se guarda solo la /24.
|
|
||||||
map $remote_addr $ip_anon {
|
map $remote_addr $ip_anon {
|
||||||
~(?P<ip>\d+\.\d+\.\d+)\.\d+ $ip.0;
|
~(?P<ip>\d+\.\d+\.\d+)\.\d+ $ip.0;
|
||||||
~(?P<ip>[^:]+:[^:]+): $ip::;
|
~(?P<ip>[^:]+:[^:]+): $ip::;
|
||||||
|
|
@ -48,7 +43,6 @@ server {
|
||||||
expires 7d;
|
expires 7d;
|
||||||
}
|
}
|
||||||
|
|
||||||
# HUB de OASIS. Trae CSS inline, así que hub.conf redefine las cabeceras sin la CSP.
|
|
||||||
location = /c { include /etc/nginx/snippets/acampada26s-hub.conf; }
|
location = /c { include /etc/nginx/snippets/acampada26s-hub.conf; }
|
||||||
location ^~ /c/ { include /etc/nginx/snippets/acampada26s-hub.conf; }
|
location ^~ /c/ { include /etc/nginx/snippets/acampada26s-hub.conf; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,9 @@ After=oasis.service
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
User=oasis
|
User=oasis
|
||||||
Environment=OASIS_DIR=/home/oasis/oasis
|
Environment=OASIS_DIR=/home/oasis/oasis
|
||||||
# cuenta de la acampada y clave (%...sha256) de su tribu pública
|
|
||||||
Environment=CUENTA=
|
Environment=CUENTA=
|
||||||
Environment=TRIBU=
|
Environment=TRIBU=
|
||||||
Environment=WEB=/var/www/acampada26s.net
|
Environment=WEB=/var/www/acampada26s.net
|
||||||
# node tiene que estar en el PATH del servicio (o poner la ruta completa)
|
|
||||||
ExecStart=/bin/sh -c 'set -e; node /opt/26s/tools/extrae.js > /tmp/26s.json; python3 /opt/26s/tools/genera.py "$WEB" < /tmp/26s.json'
|
ExecStart=/bin/sh -c 'set -e; node /opt/26s/tools/extrae.js > /tmp/26s.json; python3 /opt/26s/tools/genera.py "$WEB" < /tmp/26s.json'
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ h1 { font-size: clamp(48px, 11vw, 120px); margin: 24px 0 16px; }
|
||||||
h2 { font-size: 40px; margin: 48px 0 16px; border-bottom: var(--bd); padding-bottom: 6px; }
|
h2 { font-size: 40px; margin: 48px 0 16px; border-bottom: var(--bd); padding-bottom: 6px; }
|
||||||
h3 { font-size: 26px; margin: 0 0 6px; }
|
h3 { font-size: 26px; margin: 0 0 6px; }
|
||||||
|
|
||||||
/* cuadros y botones */
|
|
||||||
.cuadros, .botonera { display: grid; gap: var(--gap); padding: 0; margin: 0; list-style: none; }
|
.cuadros, .botonera { display: grid; gap: var(--gap); padding: 0; margin: 0; list-style: none; }
|
||||||
.cuadros { grid-template-columns: repeat(3, 1fr); }
|
.cuadros { grid-template-columns: repeat(3, 1fr); }
|
||||||
.botonera { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin-top: var(--gap); }
|
.botonera { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin-top: var(--gap); }
|
||||||
|
|
@ -68,7 +67,6 @@ h3 { font-size: 26px; margin: 0 0 6px; }
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* comunicados */
|
|
||||||
.lista { list-style: none; margin: 0; padding: 0; }
|
.lista { list-style: none; margin: 0; padding: 0; }
|
||||||
.lista > li { border-bottom: 1px solid var(--fg); padding: 20px 0; }
|
.lista > li { border-bottom: 1px solid var(--fg); padding: 20px 0; }
|
||||||
.lista a:hover, .lista a:focus-visible { color: var(--acc); }
|
.lista a:hover, .lista a:focus-visible { color: var(--acc); }
|
||||||
|
|
@ -97,7 +95,6 @@ footer { margin: 64px 0 24px; border-top: var(--bd); padding-top: 12px; font-siz
|
||||||
.bt:hover, .bt:focus-visible { transform: none; }
|
.bt:hover, .bt:focus-visible { transform: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* colectivos */
|
|
||||||
.colectivos { list-style: none; margin: 0 0 16px; padding: 0; columns: 2 280px; column-gap: 32px; }
|
.colectivos { list-style: none; margin: 0 0 16px; padding: 0; columns: 2 280px; column-gap: 32px; }
|
||||||
.colectivos li { break-inside: avoid; padding: 6px 0; border-bottom: 1px solid var(--fg); }
|
.colectivos li { break-inside: avoid; padding: 6px 0; border-bottom: 1px solid var(--fg); }
|
||||||
.colectivos a { font-family: var(--tit); font-size: 22px; text-transform: uppercase; text-decoration: none; }
|
.colectivos a { font-family: var(--tit); font-size: 22px; text-transform: uppercase; text-decoration: none; }
|
||||||
|
|
|
||||||
BIN
tools/__pycache__/genera.cpython-311.pyc
Normal file
BIN
tools/__pycache__/genera.cpython-311.pyc
Normal file
Binary file not shown.
|
|
@ -1,16 +1,4 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Lee del sbot de OASIS lo que va a la web y lo saca en JSON para tools/genera.py.
|
|
||||||
//
|
|
||||||
// Quién publica: miembros de la tribu TRIBU a los que CUENTA sigue, más CUENTA.
|
|
||||||
// La tribu sola no basta: en SSB cualquiera puede publicar que se une a una tribu
|
|
||||||
// (la invitación solo la comprueba la interfaz de OASIS). El follow de CUENTA sí
|
|
||||||
// está firmado por CUENTA.
|
|
||||||
//
|
|
||||||
// comunicados posts con #comunicado26s de quien publica
|
|
||||||
// comisiones subtribus públicas de TRIBU creadas por quien publica
|
|
||||||
// debates foros públicos abiertos por quien publica
|
|
||||||
//
|
|
||||||
// Uso: OASIS_DIR=~/oasis CUENTA=@...ed25519 TRIBU=%...sha256 node tools/extrae.js
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const dir = process.env.OASIS_DIR || path.join(process.env.HOME, 'oasis');
|
const dir = process.env.OASIS_DIR || path.join(process.env.HOME, 'oasis');
|
||||||
|
|
@ -26,7 +14,6 @@ if (!CUENTA || !CUENTA.startsWith('@') || !TRIBU || !TRIBU.startsWith('%')) {
|
||||||
const ssbConfig = require(path.join(dir, 'src', 'server', 'ssb_config'));
|
const ssbConfig = require(path.join(dir, 'src', 'server', 'ssb_config'));
|
||||||
const ssbClient = require(path.join(dir, 'src', 'server', 'node_modules', 'ssb-client'));
|
const ssbClient = require(path.join(dir, 'src', 'server', 'node_modules', 'ssb-client'));
|
||||||
|
|
||||||
// igual que STRUCTURAL_FIELDS en src/models/tribes_model.js
|
|
||||||
const ESTRUCTURA = ['title', 'description', 'image', 'location', 'tags', 'isAnonymous',
|
const ESTRUCTURA = ['title', 'description', 'image', 'location', 'tags', 'isAnonymous',
|
||||||
'inviteMode', 'status', 'parentTribeId', 'mapUrl'];
|
'inviteMode', 'status', 'parentTribeId', 'mapUrl'];
|
||||||
|
|
||||||
|
|
@ -44,9 +31,6 @@ const todos = (src) => new Promise((res, rej) => {
|
||||||
const lista = (x) => (Array.isArray(x) ? x : []);
|
const lista = (x) => (Array.isArray(x) ? x : []);
|
||||||
const igual = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
const igual = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
||||||
|
|
||||||
// Cadena de una tribu (create + updates por `replaces`) con las mismas reglas que
|
|
||||||
// buildTribeIndex: el autor raíz cambia lo que quiera; otro miembro solo puede
|
|
||||||
// entrar o salir él mismo. Devuelve el último estado válido.
|
|
||||||
function puntaTribu(raiz, msgs) {
|
function puntaTribu(raiz, msgs) {
|
||||||
const porPadre = new Map();
|
const porPadre = new Map();
|
||||||
for (const m of msgs) {
|
for (const m of msgs) {
|
||||||
|
|
@ -98,7 +82,6 @@ ssbClient(ssbConfig.keys, { remote, caps: ssbConfig.caps }).then(async (ssb) =>
|
||||||
const [contactos, tribus, posts, feeds, foros, lapidas] = await Promise.all(
|
const [contactos, tribus, posts, feeds, foros, lapidas] = await Promise.all(
|
||||||
['contact', 'tribe', 'post', 'feed', 'forum', 'tombstone'].map(tipo));
|
['contact', 'tribe', 'post', 'feed', 'forum', 'tombstone'].map(tipo));
|
||||||
|
|
||||||
// a quién sigue CUENTA (el último contact gana)
|
|
||||||
const sigue = new Map();
|
const sigue = new Map();
|
||||||
for (const m of contactos) {
|
for (const m of contactos) {
|
||||||
const c = m.value.content;
|
const c = m.value.content;
|
||||||
|
|
@ -115,7 +98,6 @@ ssbClient(ssbConfig.keys, { remote, caps: ssbConfig.caps }).then(async (ssb) =>
|
||||||
const miembros = lista(puntaTribu(raiz, deTribu(raiz)).value.content.members);
|
const miembros = lista(puntaTribu(raiz, deTribu(raiz)).value.content.members);
|
||||||
const editores = new Set([CUENTA, ...miembros.filter((id) => sigue.get(id) === true)]);
|
const editores = new Set([CUENTA, ...miembros.filter((id) => sigue.get(id) === true)]);
|
||||||
|
|
||||||
// borrado: tombstone del propio autor
|
|
||||||
const borrado = new Set(lapidas
|
const borrado = new Set(lapidas
|
||||||
.filter((m) => typeof m.value.content.target === 'string')
|
.filter((m) => typeof m.value.content.target === 'string')
|
||||||
.map((m) => `${m.value.author} ${m.value.content.target}`));
|
.map((m) => `${m.value.author} ${m.value.content.target}`));
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,4 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Escribe en la web lo que sale de tools/extrae.js.
|
|
||||||
|
|
||||||
Lee el JSON por stdin y, dentro de SITE:
|
|
||||||
- rellena en index.html los bloques entre <!-- X:inicio --> y <!-- X:fin -->
|
|
||||||
para X = comunicados, comisiones, debates
|
|
||||||
- crea comunicados/AAAA-MM-DD-xxxxxxxx.html por comunicado y borra los que
|
|
||||||
ya no están (borrados en OASIS o de alguien que ya no publica)
|
|
||||||
|
|
||||||
Todo el texto que viene de SSB se escapa. Escritura atómica: nginx nunca sirve
|
|
||||||
un fichero a medias.
|
|
||||||
|
|
||||||
node tools/extrae.js | python3 tools/genera.py /var/www/acampada26s.net
|
|
||||||
"""
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
|
|
@ -40,7 +27,6 @@ def blob(ref):
|
||||||
|
|
||||||
|
|
||||||
def plano(texto):
|
def plano(texto):
|
||||||
"""Texto sin markdown de imágenes/enlaces ni la etiqueta, en una línea."""
|
|
||||||
texto = IMG.sub("", texto)
|
texto = IMG.sub("", texto)
|
||||||
texto = ENLACE.sub(r"\1", texto)
|
texto = ENLACE.sub(r"\1", texto)
|
||||||
texto = ETIQUETA.sub("", texto)
|
texto = ETIQUETA.sub("", texto)
|
||||||
|
|
@ -48,7 +34,6 @@ def plano(texto):
|
||||||
|
|
||||||
|
|
||||||
def partir(texto):
|
def partir(texto):
|
||||||
"""Primera línea con contenido = título; el resto = cuerpo."""
|
|
||||||
lineas = ETIQUETA.sub("", texto).replace("\r", "").split("\n")
|
lineas = ETIQUETA.sub("", texto).replace("\r", "").split("\n")
|
||||||
for i, l in enumerate(lineas):
|
for i, l in enumerate(lineas):
|
||||||
t = plano(l).lstrip("# ").strip()
|
t = plano(l).lstrip("# ").strip()
|
||||||
|
|
@ -58,7 +43,6 @@ def partir(texto):
|
||||||
|
|
||||||
|
|
||||||
def cuerpo(texto):
|
def cuerpo(texto):
|
||||||
"""Markdown mínimo: párrafos, saltos, imágenes del pub y enlaces http(s)."""
|
|
||||||
out = []
|
out = []
|
||||||
for parrafo in re.split(r"\n\s*\n", texto.replace("\r", "").strip()):
|
for parrafo in re.split(r"\n\s*\n", texto.replace("\r", "").strip()):
|
||||||
if not parrafo.strip():
|
if not parrafo.strip():
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Quita metadatos (EXIF, GPS, modelo de móvil) de las imágenes antes de subirlas.
|
|
||||||
# Uso: tools/limpia-img.sh foto1.jpg foto2.png ...
|
|
||||||
# Necesita exiftool (libimage-exiftool-perl) o mat2.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
[ $# -gt 0 ] || { echo "uso: $0 imagen..." >&2; exit 1; }
|
[ $# -gt 0 ] || { echo "uso: $0 imagen..." >&2; exit 1; }
|
||||||
|
|
@ -17,7 +14,6 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# comprobación: no debe quedar GPS
|
|
||||||
if command -v exiftool >/dev/null 2>&1; then
|
if command -v exiftool >/dev/null 2>&1; then
|
||||||
if exiftool -q -q -if '$GPSLatitude' -p '$FileName' -- "$@" 2>/dev/null | grep .; then
|
if exiftool -q -q -if '$GPSLatitude' -p '$FileName' -- "$@" 2>/dev/null | grep .; then
|
||||||
echo "ojo: quedan coordenadas GPS en las de arriba" >&2
|
echo "ojo: quedan coordenadas GPS en las de arriba" >&2
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue