backend: lo del movil sale de backend.js a mobile_layer.js
De 20 trozos nuestros en backend.js quedan 9 (21 lineas). Pasan a src/backend/mobile_layer.js, que monta el router propio delante de las rutas del dev: - blockexplorer, actualizar con git y presets de modulos, cortados en el movil - los modulos que no van en el movil, apagados al arrancar y al guardar - avatar de la barra de arriba, filtro y lanzador - fotograma en los videos y pestaña actual marcada - descargas de la copia de seguridad por GET El cambio de tema del movil lo atiende karvan_routes.js.
This commit is contained in:
parent
e7892c4bf9
commit
1148e6a472
4 changed files with 164 additions and 89 deletions
|
|
@ -69,6 +69,9 @@ Aquí se puede hacer lo que haga falta.
|
|||
- `src/views/hive_views.js`: lanzador y barras de arriba y abajo.
|
||||
- `src/views/mobile_icons.js`: los iconos.
|
||||
- `src/backend/karvan_routes.js`: nuestro router. Va justo delante del del dev.
|
||||
- `src/backend/mobile_layer.js`: lo que el móvil necesita del backend (rutas que se
|
||||
cortan, módulos que no van, avatar y lanzador, retoques del HTML, descargas de la
|
||||
copia de seguridad). La monta `karvan_routes.js`.
|
||||
- `src/models/fork_workflows.js`: nuestros workflows.
|
||||
- `src/client/assets/translations/i18n_fork.js`: nuestros textos.
|
||||
- `src/client/assets/images/ux-*.svg`: nuestras imágenes.
|
||||
|
|
@ -172,8 +175,12 @@ inspeccionar la WebView desde `chrome://inspect`.
|
|||
Cosas de antes de estas normas que aún no las cumplen, por orden de lo que más
|
||||
duele:
|
||||
|
||||
1. Quedan 21 trozos nuestros en `backend.js` (copia de seguridad, avatar de la barra
|
||||
de arriba, vídeos, filtro del lanzador...). Tienen que irse al router propio.
|
||||
1. En `backend.js` quedan 9 trozos pequeños (21 líneas) que no se pueden sacar desde
|
||||
fuera: tres expresiones regulares que con el Node 18 de Android tumbaban el
|
||||
arranque (esto habría que contárselo al dev), no abrir el navegador al arrancar,
|
||||
no repetir fotos ya limpiadas, el modo chats, Karvan en la lista de Módulos de
|
||||
escritorio y la línea que monta nuestro router. Lo demás ya está en
|
||||
`src/backend/mobile_layer.js`.
|
||||
2. En `workflows_model.js` queda la logica de skins del movil (`themeOf`, `sameTheme`):
|
||||
`settings_view.js` coge `currentWorkflow` al cargar y no se puede envolver desde
|
||||
fuera sin tocar esa vista. Es poco, pero es del dev.
|
||||
|
|
|
|||
|
|
@ -1644,17 +1644,6 @@ debug("Current configuration: %O", config);
|
|||
debug(`You can save the above to ${defaultConfigFile} to make \
|
||||
these settings the default. See the readme for details.`);
|
||||
const { saveConfig, getConfig } = require('../configs/config-manager');
|
||||
if (process.env.OASIS_MOBILE === '1') {
|
||||
try {
|
||||
const cfg = getConfig();
|
||||
const forced = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'].filter(m => cfg.modules && cfg.modules[`${m}Mod`] !== 'off');
|
||||
let dirty = false;
|
||||
if (forced.length) { forced.forEach(m => { cfg.modules[`${m}Mod`] = 'off'; }); dirty = true; }
|
||||
const w = cfg.wallet || {};
|
||||
if (w.url === 'http://127.0.0.1:17474' && w.user === 'oasis' && w.pass === 'testpass123') { cfg.wallet = { ...w, url: 'http://127.0.0.1:7474', user: '', pass: '' }; dirty = true; }
|
||||
if (dirty) saveConfig(cfg);
|
||||
} catch (_) {}
|
||||
}
|
||||
const oasisCheckPath = "/.well-known/oasis";
|
||||
process.on("uncaughtException", function (err) {
|
||||
if (err["code"] === "EADDRINUSE") {
|
||||
|
|
@ -5114,7 +5103,6 @@ router
|
|||
})
|
||||
|
||||
.get('/blockexplorer', async (ctx) => {
|
||||
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; }
|
||||
const userId = getViewerId();
|
||||
const query = ctx.query || {};
|
||||
const search = {
|
||||
|
|
@ -5165,7 +5153,6 @@ router
|
|||
ctx.body = renderBlockchainView(blockchainData, filter, userId, search, { inspect, censusBlocks });
|
||||
})
|
||||
.get('/blockexplorer/block/:id', async (ctx) => {
|
||||
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; }
|
||||
const userId = getViewerId();
|
||||
const query = ctx.query || {};
|
||||
const search = {
|
||||
|
|
@ -8487,7 +8474,7 @@ router
|
|||
profile: true,
|
||||
ux: true,
|
||||
workflow: isLoopbackRequest(ctx),
|
||||
theme: true, // en el movil ofrece los skins de movil (WIZARD_THEMES)
|
||||
theme: true,
|
||||
community: checkMod(ctx, 'invitesMod'),
|
||||
greeting: checkMod(ctx, 'feedMod')
|
||||
};
|
||||
|
|
@ -11522,40 +11509,6 @@ router
|
|||
try { await forumModel.removeOpenInvite(ctx.params.id); } catch (_) { actionFail(ctx); return; }
|
||||
ctx.redirect(safeReturnTo(ctx, `/forum/${encodeURIComponent(ctx.params.id)}`, ['/forum']));
|
||||
})
|
||||
.get('/backup/keys/export', async (ctx) => {
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
if (process.env.OASIS_MOBILE !== '1') { ctx.redirect('/backup'); return; }
|
||||
const pw = String(ctx.query.password || '');
|
||||
if (!pw || pw.length < 32) return ctx.redirect('/backup');
|
||||
try {
|
||||
const { filename, data } = await backupModel.exportKeys({ password: pw });
|
||||
ctx.set('Content-Type', 'application/octet-stream');
|
||||
ctx.set('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
ctx.set('Content-Length', String(data.length));
|
||||
ctx.body = data;
|
||||
try { onboardingModel.markStep('backup'); } catch (_) {}
|
||||
} catch (error) { sendErrorPage(ctx, error.message); }
|
||||
})
|
||||
.get('/backup/export', async (ctx) => {
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
if (process.env.OASIS_MOBILE !== '1') { ctx.redirect('/backup'); return; }
|
||||
if (!checkMod(ctx, 'backupMod')) return ctx.redirect('/modules');
|
||||
const b = ctx.query || {};
|
||||
const pw = String(b.password || '');
|
||||
if (pw.length < 32) return ctx.redirect('/backup');
|
||||
const rawMods = b.modules === undefined ? [] : (Array.isArray(b.modules) ? b.modules : [b.modules]);
|
||||
const outPath = path.join(os.tmpdir(), `oasis-backup-${process.pid}-${Date.now()}.oasisbk`);
|
||||
try {
|
||||
const res = await backupModel.createBackup({ scope: b.scope, blobs: b.blobs, modules: rawMods, since: b.since }, pw, outPath);
|
||||
ctx.set('Content-Type', 'application/octet-stream');
|
||||
ctx.set('Content-Disposition', `attachment; filename="${res.filename}"`);
|
||||
ctx.set('Content-Length', String(res.bytes));
|
||||
const stream = fs.createReadStream(outPath);
|
||||
stream.on('close', () => { try { fs.unlinkSync(outPath); } catch (_) {} });
|
||||
ctx.body = stream;
|
||||
try { onboardingModel.markStep('backup'); } catch (_) {}
|
||||
} catch (error) { try { fs.unlinkSync(outPath); } catch (_) {} sendErrorPage(ctx, error.message); }
|
||||
})
|
||||
.post('/backup/keys/export', koaBody(), async (ctx) => {
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
const pw = ctx.request.body.password;
|
||||
|
|
@ -14648,7 +14601,6 @@ router
|
|||
})
|
||||
.post("/update", koaBody(), async (ctx) => {
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/settings'); return; }
|
||||
const exec = require("node:util").promisify(require("node:child_process").exec);
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const { stdout, stderr } = await exec("git reset --hard && git pull", { cwd: repoRoot });
|
||||
|
|
@ -14669,8 +14621,6 @@ router
|
|||
})
|
||||
.post("/settings/theme", koaBody(), async (ctx) => {
|
||||
const theme = String(ctx.request.body.theme || "").trim(), cfg = getConfig();
|
||||
// en el movil solo los skins de movil: un tema de escritorio deja la app sin maqueta
|
||||
if (process.env.OASIS_MOBILE === '1' && !require('../views/main_views').MOBILE_THEMES.some(([value]) => value === theme)) { ctx.redirect("/settings#theme"); return; }
|
||||
cfg.themes.current = theme || "Dark-SNH";
|
||||
saveConfig(cfg);
|
||||
ctx.cookies.set("theme", cfg.themes.current, { httpOnly: true, sameSite: 'strict', secure: ctx.secure });
|
||||
|
|
@ -15177,7 +15127,6 @@ router
|
|||
})
|
||||
.post("/modules/preset", koaBody(), async (ctx) => {
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/modules'); return; }
|
||||
const ALL_MODULES = workflowsModel.ALL_MODULES;
|
||||
const PRESETS = workflowsModel.PRESETS;
|
||||
const preset = String(ctx.request.body.preset || '');
|
||||
|
|
@ -15189,14 +15138,10 @@ router
|
|||
ctx.redirect('/modules');
|
||||
})
|
||||
.post("/save-modules", koaBody(), async (ctx) => {
|
||||
const MOBILE_UNAVAILABLE_MODS = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'];
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
const modules = workflowsModel.ALL_MODULES;
|
||||
const cfg = getConfig();
|
||||
modules.forEach(mod => cfg.modules[`${mod}Mod`] = ctx.request.body[`${mod}Form`] === 'on' ? 'on' : 'off');
|
||||
if (process.env.OASIS_MOBILE === '1') {
|
||||
MOBILE_UNAVAILABLE_MODS.forEach(mod => cfg.modules[`${mod}Mod`] = 'off');
|
||||
}
|
||||
saveConfig(cfg);
|
||||
ctx.redirect(`/modules`);
|
||||
})
|
||||
|
|
@ -15347,19 +15292,6 @@ const folderBytes = async (dir) => {
|
|||
return bytes;
|
||||
};
|
||||
const refreshAll = async (now) => {
|
||||
try {
|
||||
// avatar propio para el circulo de la topbar movil (lo lee renderMobileTopbar)
|
||||
const meId = getViewerId();
|
||||
const selfImg = meId ? await about.image(meId) : null;
|
||||
const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256';
|
||||
if (typeof selfImg === 'string' && selfImg[0] === '&' && selfImg !== NULL_IMG) {
|
||||
global.__OASIS_SELF_AVATAR__ = `/image/256/${encodeURIComponent(selfImg)}`;
|
||||
} else if (typeof selfImg === 'string' && selfImg[0] === '/') {
|
||||
global.__OASIS_SELF_AVATAR__ = selfImg;
|
||||
} else {
|
||||
global.__OASIS_SELF_AVATAR__ = null;
|
||||
}
|
||||
} catch (_) {}
|
||||
try { await bankingModel.ensureSelfAddressPublished(); } catch (_) {}
|
||||
try { await bankingModel.getUserEngagementScore(getViewerId()); } catch (_) {}
|
||||
try {
|
||||
|
|
@ -15479,14 +15411,6 @@ const kickRefresh = () => {
|
|||
};
|
||||
setTimeout(() => { if (!sharedState.getLastRefresh()) kickRefresh(); }, 5000);
|
||||
const middleware = [
|
||||
async (ctx, next) => {
|
||||
await next();
|
||||
if (process.env.OASIS_MOBILE !== '1' || typeof ctx.body !== 'string' || !String(ctx.type || '').includes('html')) return;
|
||||
ctx.body = ctx.body.replace(/<video\b[^>]*>/g, (tag) => tag.replace(/\bsrc="(\/blob\/[^"#]+)"/, 'src="$1#t=0.1"'));
|
||||
const tab = ['/mentions', '/inbox', '/phone', '/data', '/tags', '/search', '/settings'].find((t) => ctx.path === t || ctx.path.startsWith(t + '/'));
|
||||
const headerEnd = ctx.body.indexOf('<div class="main-content">');
|
||||
if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(`<a href="${tab}"`, `<a href="${tab}" aria-current="page"`) + ctx.body.slice(headerEnd);
|
||||
},
|
||||
async (ctx, next) => {
|
||||
if (ctx.method === 'GET' && /^recent$/i.test(String(ctx.query.filter || '')) && ['q', 'search'].some(k => String(ctx.query[k] || '').trim())) ctx.query = { ...ctx.query, filter: ctx.query.filter === 'RECENT' ? 'ALL' : 'all' };
|
||||
await next();
|
||||
|
|
@ -15586,18 +15510,13 @@ const middleware = [
|
|||
},
|
||||
async (ctx, next) => {
|
||||
if (!ctx.path.startsWith('/assets/') && !ctx.path.startsWith('/image/') && !ctx.path.startsWith('/blob/') && !ctx.path.startsWith('/qr') && !ctx.path.startsWith('/c/')) {
|
||||
// filtro de la home (Personal/Community); lo leen la topbar y el lanzador
|
||||
global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : '';
|
||||
// el lanzador de modulos SOLO en la home; en el resto de paginas el top queda limpio
|
||||
global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/');
|
||||
if (Date.now() - sharedState.getLastRefresh() > 60000) kickRefresh();
|
||||
}
|
||||
await next();
|
||||
},
|
||||
// rutas propias de la rama (Karvan, barra inferior, exportacion): fuera de la cadena de upstream
|
||||
require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId,
|
||||
getConfig, saveConfig,
|
||||
isLoopbackRequest, sendErrorPage }),
|
||||
// la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md)
|
||||
require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId, getConfig, saveConfig,
|
||||
isLoopbackRequest, sendErrorPage, about, backupModel, onboardingModel }),
|
||||
routes,
|
||||
async (ctx) => {
|
||||
if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,10 @@ const wrapUpstream = async (ctx, next) => {
|
|||
};
|
||||
|
||||
module.exports = ({ cooler, pull, pmModel, checkMod, getViewerId,
|
||||
getConfig, saveConfig, isLoopbackRequest, sendErrorPage }) => {
|
||||
getConfig, saveConfig, isLoopbackRequest, sendErrorPage,
|
||||
about, backupModel, onboardingModel }) => {
|
||||
const mobileLayer = require('./mobile_layer')({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel,
|
||||
checkMod, isLoopbackRequest, sendErrorPage });
|
||||
// FORK_IA_UX Fase 2 (cross-device): relay de MENSAJES de Karvan por privados SSB. Funciona vía un pub
|
||||
// (el privado se replica al otro móvil); los SIGNAL de WebRTC NO se relayan (serían demasiados/lentos por el log).
|
||||
// No toca el arranque del SSB. Ingesta por stream EN VIVO (no polling) → eficiente.
|
||||
|
|
@ -116,6 +119,19 @@ async function karvanInviteAuthor(roomId) {
|
|||
|
||||
const forkRouter = new koaRouter();
|
||||
forkRouter
|
||||
// tema en el movil: solo los skins de movil (uno de escritorio deja la app sin
|
||||
// maqueta). Hace lo mismo que la ruta del dev, con esa comprobacion delante
|
||||
.post('/settings/theme', koaBody(), async (ctx, next) => {
|
||||
if (!isMobile()) return next();
|
||||
const theme = String((ctx.request.body || {}).theme || '').trim();
|
||||
const { MOBILE_THEMES } = require('../views/main_views');
|
||||
if (!MOBILE_THEMES.some(([value]) => value === theme)) { ctx.redirect('/settings#theme'); return; }
|
||||
const cfg = getConfig();
|
||||
cfg.themes.current = theme;
|
||||
saveConfig(cfg);
|
||||
ctx.cookies.set('theme', theme, { httpOnly: true, sameSite: 'strict', secure: ctx.secure });
|
||||
ctx.redirect('/settings#theme');
|
||||
})
|
||||
// Modulos en el movil: la vista propia (mobile_modules_view.js); en escritorio, la de upstream
|
||||
.get('/modules', async (ctx, next) => {
|
||||
if (!isMobile()) return next();
|
||||
|
|
@ -287,5 +303,5 @@ async function karvanInviteAuthor(roomId) {
|
|||
setTimeout(() => process.exit(0), 500);
|
||||
});
|
||||
|
||||
return compose([wrapUpstream, forkRouter.routes()]);
|
||||
return compose([mobileLayer, wrapUpstream, forkRouter.routes()]);
|
||||
};
|
||||
|
|
|
|||
133
src/backend/mobile_layer.js
Normal file
133
src/backend/mobile_layer.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"use strict";
|
||||
// Lo que el movil necesita del backend, fuera de backend.js.
|
||||
//
|
||||
// Antes eran trozos metidos en backend.js (rutas, middleware y refreshAll), que es
|
||||
// del dev y cambia en 34 de cada 39 versiones. Ahora va aqui y lo monta el router
|
||||
// propio (karvan_routes.js) delante de las rutas del dev. Nada de esto corre en
|
||||
// escritorio. Ver GUIA_DE_DESARROLLO.md.
|
||||
//
|
||||
// Hace cuatro cosas:
|
||||
// - corta en el movil rutas que alli no tienen sentido (blockexplorer, actualizar
|
||||
// con git, presets de modulos); el cambio de tema lo atiende karvan_routes.js
|
||||
// - mantiene apagados los modulos que no van en el movil, al arrancar y al guardar
|
||||
// - prepara lo que leen la barra de arriba y el lanzador (avatar, filtro, lanzador)
|
||||
// - retoca el HTML que sale (videos con fotograma, pestaña actual marcada)
|
||||
// Y sirve las dos descargas de la copia de seguridad, que en el movil van por GET.
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const compose = require('../server/node_modules/koa-compose');
|
||||
|
||||
const MOBILE_UNAVAILABLE_MODS = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'];
|
||||
const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256';
|
||||
const TAB_PATHS = ['/mentions', '/inbox', '/phone', '/data', '/tags', '/search', '/settings'];
|
||||
const isMobile = () => process.env.OASIS_MOBILE === '1';
|
||||
const isPage = (p) => !/^\/(assets|image|blob|qr|c)(\/|$)/.test(p);
|
||||
|
||||
module.exports = ({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel,
|
||||
checkMod, isLoopbackRequest, sendErrorPage }) => {
|
||||
// --- al arrancar: modulos que no van en el movil, apagados; y la cartera de
|
||||
// pruebas que traia la config de serie, fuera
|
||||
if (isMobile()) {
|
||||
try {
|
||||
const cfg = getConfig();
|
||||
let dirty = false;
|
||||
MOBILE_UNAVAILABLE_MODS.forEach(m => { if (cfg.modules && cfg.modules[`${m}Mod`] !== 'off') { cfg.modules[`${m}Mod`] = 'off'; dirty = true; } });
|
||||
const w = cfg.wallet || {};
|
||||
if (w.url === 'http://127.0.0.1:17474' && w.user === 'oasis' && w.pass === 'testpass123') {
|
||||
cfg.wallet = { ...w, url: 'http://127.0.0.1:7474', user: '', pass: '' };
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty) saveConfig(cfg);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// --- avatar propio para el circulo de la barra de arriba (lo lee hive_views)
|
||||
let avatarAt = 0;
|
||||
const refreshAvatar = async () => {
|
||||
avatarAt = Date.now();
|
||||
try {
|
||||
const me = getViewerId();
|
||||
const img = me ? await about.image(me) : null;
|
||||
if (typeof img === 'string' && img[0] === '&' && img !== NULL_IMG) global.__OASIS_SELF_AVATAR__ = `/image/256/${encodeURIComponent(img)}`;
|
||||
else if (typeof img === 'string' && img[0] === '/') global.__OASIS_SELF_AVATAR__ = img;
|
||||
else global.__OASIS_SELF_AVATAR__ = null;
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
// --- rutas que en el movil se cortan antes de llegar al dev
|
||||
const guards = async (ctx, next) => {
|
||||
if (!isMobile()) return next();
|
||||
const p = ctx.path;
|
||||
if (ctx.method === 'GET' && (p === '/blockexplorer' || p.startsWith('/blockexplorer/block/'))) { ctx.redirect('/'); return; }
|
||||
if (ctx.method === 'POST' && p === '/update') { ctx.redirect('/settings'); return; }
|
||||
if (ctx.method === 'POST' && p === '/modules/preset') { ctx.redirect('/modules'); return; }
|
||||
return next();
|
||||
};
|
||||
|
||||
// --- antes y despues de las rutas del dev
|
||||
const around = async (ctx, next) => {
|
||||
if (!isMobile()) return next();
|
||||
if (isPage(ctx.path)) {
|
||||
// filtro de la home (Personal/Social) y lanzador solo en la portada
|
||||
global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : '';
|
||||
global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/');
|
||||
if (!avatarAt) await refreshAvatar();
|
||||
else if (Date.now() - avatarAt > 60000) refreshAvatar();
|
||||
}
|
||||
await next();
|
||||
// guardar modulos: los que no van en el movil siguen apagados
|
||||
if (ctx.method === 'POST' && ctx.path === '/save-modules' && ctx.status >= 300 && ctx.status < 400) {
|
||||
try {
|
||||
const cfg = getConfig();
|
||||
MOBILE_UNAVAILABLE_MODS.forEach(m => { cfg.modules[`${m}Mod`] = 'off'; });
|
||||
saveConfig(cfg);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (typeof ctx.body !== 'string' || !String(ctx.type || '').includes('html')) return;
|
||||
// los videos sin poster salen en negro en el WebView: se pide el fotograma 0.1
|
||||
ctx.body = ctx.body.replace(/<video\b[^>]*>/g, (tag) => tag.replace(/\bsrc="(\/blob\/[^"#]+)"/, 'src="$1#t=0.1"'));
|
||||
// la pestaña de la cabecera que corresponde a esta pagina
|
||||
const tab = TAB_PATHS.find((t) => ctx.path === t || ctx.path.startsWith(t + '/'));
|
||||
const headerEnd = ctx.body.indexOf('<div class="main-content">');
|
||||
if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(`<a href="${tab}"`, `<a href="${tab}" aria-current="page"`) + ctx.body.slice(headerEnd);
|
||||
};
|
||||
|
||||
// --- copia de seguridad: en el movil se descarga por GET (el WebView no guarda
|
||||
// la respuesta de un POST como fichero)
|
||||
const backup = async (ctx, next) => {
|
||||
if (ctx.method !== 'GET' || (ctx.path !== '/backup/keys/export' && ctx.path !== '/backup/export')) return next();
|
||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||
if (!isMobile()) { ctx.redirect('/backup'); return; }
|
||||
const q = ctx.query || {};
|
||||
const pw = String(q.password || '');
|
||||
if (!pw || pw.length < 32) { ctx.redirect('/backup'); return; }
|
||||
if (ctx.path === '/backup/keys/export') {
|
||||
try {
|
||||
const { filename, data } = await backupModel.exportKeys({ password: pw });
|
||||
ctx.set('Content-Type', 'application/octet-stream');
|
||||
ctx.set('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
ctx.set('Content-Length', String(data.length));
|
||||
ctx.body = data;
|
||||
try { onboardingModel.markStep('backup'); } catch (_) {}
|
||||
} catch (error) { sendErrorPage(ctx, error.message); }
|
||||
return;
|
||||
}
|
||||
if (!checkMod(ctx, 'backupMod')) { ctx.redirect('/modules'); return; }
|
||||
const rawMods = q.modules === undefined ? [] : (Array.isArray(q.modules) ? q.modules : [q.modules]);
|
||||
const outPath = path.join(os.tmpdir(), `oasis-backup-${process.pid}-${Date.now()}.oasisbk`);
|
||||
try {
|
||||
const res = await backupModel.createBackup({ scope: q.scope, blobs: q.blobs, modules: rawMods, since: q.since }, pw, outPath);
|
||||
ctx.set('Content-Type', 'application/octet-stream');
|
||||
ctx.set('Content-Disposition', `attachment; filename="${res.filename}"`);
|
||||
ctx.set('Content-Length', String(res.bytes));
|
||||
const stream = fs.createReadStream(outPath);
|
||||
stream.on('close', () => { try { fs.unlinkSync(outPath); } catch (_) {} });
|
||||
ctx.body = stream;
|
||||
try { onboardingModel.markStep('backup'); } catch (_) {}
|
||||
} catch (error) { try { fs.unlinkSync(outPath); } catch (_) {} sendErrorPage(ctx, error.message); }
|
||||
};
|
||||
|
||||
return compose([around, guards, backup]);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue