mwoffliner 1.17.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +355 -62
- package/docs/functional_architecture.md +34 -38
- package/extensions/wiktionary_fr.js +18 -17
- package/jest.config.cjs +2 -2
- package/lib/DOMUtils.js +0 -1
- package/lib/DOMUtils.js.map +1 -1
- package/lib/Downloader.d.ts +39 -31
- package/lib/Downloader.js +418 -349
- package/lib/Downloader.js.map +1 -1
- package/lib/Dump.d.ts +24 -10
- package/lib/Dump.js +95 -50
- package/lib/Dump.js.map +1 -1
- package/lib/Gadgets.d.ts +2 -1
- package/lib/Gadgets.js +7 -5
- package/lib/Gadgets.js.map +1 -1
- package/lib/Logger.d.ts +6 -6
- package/lib/Logger.js +33 -33
- package/lib/Logger.js.map +1 -1
- package/lib/MediaWiki.d.ts +14 -31
- package/lib/MediaWiki.js +119 -172
- package/lib/MediaWiki.js.map +1 -1
- package/lib/RedisStore.d.ts +3 -3
- package/lib/RedisStore.js +21 -23
- package/lib/RedisStore.js.map +1 -1
- package/lib/S3.js +1 -1
- package/lib/S3.js.map +1 -1
- package/lib/Templates.d.ts +3 -10
- package/lib/Templates.js +5 -14
- package/lib/Templates.js.map +1 -1
- package/lib/cli.js +7 -6
- package/lib/cli.js.map +1 -1
- package/lib/config.d.ts +10 -16
- package/lib/config.js +33 -55
- package/lib/config.js.map +1 -1
- package/lib/error.manager.d.ts +2 -2
- package/lib/error.manager.js +42 -49
- package/lib/error.manager.js.map +1 -1
- package/lib/i18n.d.ts +2 -0
- package/lib/i18n.js +53 -0
- package/lib/i18n.js.map +1 -0
- package/lib/mutex.d.ts +2 -1
- package/lib/mutex.js +2 -1
- package/lib/mutex.js.map +1 -1
- package/lib/mwoffliner.lib.js +370 -222
- package/lib/mwoffliner.lib.js.map +1 -1
- package/lib/parameterList.d.ts +20 -8
- package/lib/parameterList.js +30 -18
- package/lib/parameterList.js.map +1 -1
- package/lib/renderers/abstract.renderer.d.ts +48 -55
- package/lib/renderers/abstract.renderer.js +377 -98
- package/lib/renderers/abstract.renderer.js.map +1 -1
- package/lib/renderers/action-parse.renderer.d.ts +22 -3
- package/lib/renderers/action-parse.renderer.js +123 -81
- package/lib/renderers/action-parse.renderer.js.map +1 -1
- package/lib/renderers/renderer.builder.js +6 -69
- package/lib/renderers/renderer.builder.js.map +1 -1
- package/lib/renderers/rendering.context.d.ts +2 -3
- package/lib/renderers/rendering.context.js +6 -14
- package/lib/renderers/rendering.context.js.map +1 -1
- package/lib/sanitize-argument.d.ts +7 -4
- package/lib/sanitize-argument.js +99 -41
- package/lib/sanitize-argument.js.map +1 -1
- package/lib/util/FileManager.d.ts +36 -0
- package/lib/util/FileManager.js +246 -0
- package/lib/util/FileManager.js.map +1 -0
- package/lib/util/RedisKvs.js +4 -4
- package/lib/util/RedisKvs.js.map +1 -1
- package/lib/util/RedisQueue.js +1 -1
- package/lib/util/RedisQueue.js.map +1 -1
- package/lib/util/builders/url/action-parse.director.d.ts +2 -2
- package/lib/util/builders/url/action-parse.director.js +7 -7
- package/lib/util/builders/url/action-parse.director.js.map +1 -1
- package/lib/util/builders/url/api.director.d.ts +2 -4
- package/lib/util/builders/url/api.director.js +10 -17
- package/lib/util/builders/url/api.director.js.map +1 -1
- package/lib/util/builders/url/base.director.d.ts +0 -4
- package/lib/util/builders/url/base.director.js +0 -25
- package/lib/util/builders/url/base.director.js.map +1 -1
- package/lib/util/builders/url/basic.director.d.ts +1 -1
- package/lib/util/builders/url/basic.director.js +1 -1
- package/lib/util/builders/url/web.director.d.ts +1 -1
- package/lib/util/builders/url/web.director.js +2 -2
- package/lib/util/builders/url/web.director.js.map +1 -1
- package/lib/util/categories.d.ts +5 -5
- package/lib/util/categories.js +175 -174
- package/lib/util/categories.js.map +1 -1
- package/lib/util/const.d.ts +4 -8
- package/lib/util/const.js +9 -17
- package/lib/util/const.js.map +1 -1
- package/lib/util/customCssJs.d.ts +6 -0
- package/lib/util/customCssJs.js +70 -0
- package/lib/util/customCssJs.js.map +1 -0
- package/lib/util/dump.d.ts +15 -4
- package/lib/util/dump.js +282 -86
- package/lib/util/dump.js.map +1 -1
- package/lib/util/index.d.ts +1 -1
- package/lib/util/index.js +1 -1
- package/lib/util/index.js.map +1 -1
- package/lib/util/metaData.js.map +1 -1
- package/lib/util/misc.d.ts +23 -13
- package/lib/util/misc.js +90 -59
- package/lib/util/misc.js.map +1 -1
- package/lib/util/mw-api.d.ts +7 -5
- package/lib/util/mw-api.js +167 -221
- package/lib/util/mw-api.js.map +1 -1
- package/lib/util/pageListMainPage.d.ts +3 -0
- package/lib/util/pageListMainPage.js +10 -0
- package/lib/util/pageListMainPage.js.map +1 -0
- package/lib/util/pages.d.ts +30 -0
- package/lib/util/pages.js +146 -0
- package/lib/util/pages.js.map +1 -0
- package/lib/util/rewriteUrls.d.ts +2 -2
- package/lib/util/rewriteUrls.js +33 -31
- package/lib/util/rewriteUrls.js.map +1 -1
- package/lib/util/savePages.d.ts +8 -0
- package/lib/util/savePages.js +182 -0
- package/lib/util/savePages.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/offliner-definition.json +178 -90
- package/package.json +50 -49
- package/res/page_list_home.js +20 -0
- package/res/{article_not_found.svg → page_not_found.svg} +1 -1
- package/res/script.js +30 -45
- package/res/style.css +52 -68
- package/res/templates/download_error_placeholder.html +7 -8
- package/res/templates/javaScript.html +19 -0
- package/res/templates/pageFallback.html +9 -7
- package/res/templates/pageVector2022.html +9 -7
- package/res/templates/pageVectorLegacy.html +9 -7
- package/res/templates/{article_list_home.html → page_list_home.html} +1 -2
- package/translation/ar.json +21 -21
- package/translation/bn.json +1 -1
- package/translation/de.json +46 -19
- package/translation/en.json +54 -21
- package/translation/es.json +17 -19
- package/translation/fi.json +23 -3
- package/translation/fr.json +57 -21
- package/translation/he.json +8 -6
- package/translation/ia.json +19 -19
- package/translation/id.json +13 -13
- package/translation/it.json +13 -8
- package/translation/ko.json +55 -3
- package/translation/lb.json +5 -2
- package/translation/mk.json +5 -5
- package/translation/nl.json +57 -22
- package/translation/or.json +2 -2
- package/translation/pt.json +2 -2
- package/translation/qqq.json +55 -22
- package/translation/ru.json +2 -2
- package/translation/sc.json +2 -2
- package/translation/sk.json +62 -0
- package/translation/sl.json +54 -21
- package/translation/sv.json +20 -19
- package/translation/sw.json +45 -2
- package/translation/zh-hans.json +56 -22
- package/translation/zh-hant.json +26 -19
- package/lib/renderers/abstractDesktop.render.d.ts +0 -12
- package/lib/renderers/abstractDesktop.render.js +0 -67
- package/lib/renderers/abstractDesktop.render.js.map +0 -1
- package/lib/renderers/abstractMobile.render.d.ts +0 -12
- package/lib/renderers/abstractMobile.render.js +0 -54
- package/lib/renderers/abstractMobile.render.js.map +0 -1
- package/lib/renderers/rest-api.renderer.d.ts +0 -7
- package/lib/renderers/rest-api.renderer.js +0 -68
- package/lib/renderers/rest-api.renderer.js.map +0 -1
- package/lib/renderers/visual-editor.renderer.d.ts +0 -7
- package/lib/renderers/visual-editor.renderer.js +0 -76
- package/lib/renderers/visual-editor.renderer.js.map +0 -1
- package/lib/renderers/wikimedia-desktop.renderer.d.ts +0 -4
- package/lib/renderers/wikimedia-desktop.renderer.js +0 -7
- package/lib/renderers/wikimedia-desktop.renderer.js.map +0 -1
- package/lib/renderers/wikimedia-mobile.renderer.d.ts +0 -19
- package/lib/renderers/wikimedia-mobile.renderer.js +0 -243
- package/lib/renderers/wikimedia-mobile.renderer.js.map +0 -1
- package/lib/util/articleListMainPage.d.ts +0 -3
- package/lib/util/articleListMainPage.js +0 -10
- package/lib/util/articleListMainPage.js.map +0 -1
- package/lib/util/articles.d.ts +0 -26
- package/lib/util/articles.js +0 -102
- package/lib/util/articles.js.map +0 -1
- package/lib/util/builders/url/desktop.director.d.ts +0 -8
- package/lib/util/builders/url/desktop.director.js +0 -15
- package/lib/util/builders/url/desktop.director.js.map +0 -1
- package/lib/util/builders/url/mobile.director.d.ts +0 -8
- package/lib/util/builders/url/mobile.director.js +0 -15
- package/lib/util/builders/url/mobile.director.js.map +0 -1
- package/lib/util/builders/url/rest-api.director.d.ts +0 -8
- package/lib/util/builders/url/rest-api.director.js +0 -18
- package/lib/util/builders/url/rest-api.director.js.map +0 -1
- package/lib/util/builders/url/visual-editor.director.d.ts +0 -9
- package/lib/util/builders/url/visual-editor.director.js +0 -18
- package/lib/util/builders/url/visual-editor.director.js.map +0 -1
- package/lib/util/saveArticles.d.ts +0 -8
- package/lib/util/saveArticles.js +0 -411
- package/lib/util/saveArticles.js.map +0 -1
- package/res/article_list_home.js +0 -24
- package/res/content.parsoid.css +0 -196
- package/res/inserted_style.css +0 -45
- package/res/templates/categories.html +0 -11
- package/res/templates/lead_section_wrapper.html +0 -6
- package/res/templates/pageWikimediaDesktop.html +0 -24
- package/res/templates/pageWikimediaMobile.html +0 -24
- package/res/templates/section_wrapper.html +0 -5
- package/res/templates/subcategories.html +0 -23
- package/res/templates/subpages.html +0 -17
- package/res/templates/subsection_wrapper.html +0 -5
- package/res/webpHandler.js +0 -165
- package/res/wm_mobile_override_script.js +0 -15
- package/res/wm_mobile_override_style.css +0 -20
- package/translation/br.json +0 -8
- package/translation/dag.json +0 -9
- package/translation/es-formal.json +0 -29
- package/translation/ha.json +0 -10
- package/translation/hi.json +0 -9
- package/translation/ig.json +0 -10
- package/translation/kaa.json +0 -9
- package/translation/nb.json +0 -9
- package/translation/nqo.json +0 -8
- package/translation/pt-br.json +0 -10
- package/translation/ro.json +0 -9
- package/translation/scn.json +0 -8
- package/translation/sq.json +0 -9
- package/translation/te.json +0 -9
- package/translation/tn.json +0 -9
- package/translation/tr.json +0 -10
package/lib/mwoffliner.lib.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
/* MODULE VARIABLE SECTION ********** */
|
|
5
5
|
/* ********************************** */
|
|
6
6
|
import fs, { readFileSync } from 'fs';
|
|
7
|
-
import os from 'os';
|
|
8
7
|
import pmap from 'p-map';
|
|
9
8
|
import sharp from 'sharp';
|
|
10
9
|
import domino from 'domino';
|
|
@@ -13,10 +12,11 @@ import { fileURLToPath } from 'url';
|
|
|
13
12
|
import semver from 'semver';
|
|
14
13
|
import * as path from 'path';
|
|
15
14
|
import { Blob, Compression, Creator, StringItem } from '@openzim/libzim';
|
|
16
|
-
import {
|
|
15
|
+
import { getPages } from './util/mw-api.js';
|
|
16
|
+
import { createTranslator } from './i18n.js';
|
|
17
17
|
import { zimCreatorMutex } from './mutex.js';
|
|
18
18
|
import { check_all } from './sanitize-argument.js';
|
|
19
|
-
import {
|
|
19
|
+
import { MIN_IMAGE_THRESHOLD_INDEX_PAGE, downloadAndSaveModule, downloadAndSaveStartupModule, getModuleDependencies, downloadAndSaveCustomCss, downloadAndSaveCustomJs, downloadAndSaveMathJaxSource, genCanonicalLink, genHeaderCSSLink, genHeaderScript, getDumps, getMediaBase, getRelativeFilePath, getSizeFromUrl, isValidEmail, makePageImageTile, makePageListItem, mkdirPromise, sanitizeString, saveStaticFiles, extractPageList, getTmpDirectory, validateMetadata, truncateZimEntryTitleWords, makeZimPath, replaceSafe, } from './util/index.js';
|
|
20
20
|
import S3 from './S3.js';
|
|
21
21
|
import RedisStore from './RedisStore.js';
|
|
22
22
|
import * as logger from './Logger.js';
|
|
@@ -25,49 +25,80 @@ import { config } from './config.js';
|
|
|
25
25
|
import MediaWiki from './MediaWiki.js';
|
|
26
26
|
import Downloader from './Downloader.js';
|
|
27
27
|
import RenderingContext from './renderers/rendering.context.js';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
28
|
+
import { pageListHomeTemplate, htmlRedirectTemplateCode } from './Templates.js';
|
|
29
|
+
import { savePages } from './util/savePages.js';
|
|
30
|
+
import { PAGE_REQUEST_INTERVAL, CATEGORIES_PAGE_SIZE } from './util/const.js';
|
|
31
31
|
import urlHelper from './util/url.helper.js';
|
|
32
|
+
import { parseCustomCssUrls, customCssUrlToFilename, parseCustomJsUrls, customJsUrlToFilename } from './util/customCssJs.js';
|
|
33
|
+
import FileManager from './util/FileManager.js';
|
|
32
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
35
|
const __dirname = path.dirname(__filename);
|
|
34
36
|
const packageJSON = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
35
37
|
async function execute(argv) {
|
|
38
|
+
const { speed: _speed, adminEmail, logLevel, minifyHtml, keepEmptySections, mwUrl, mwActionApiPath, mwModulePath, mwDomain, mwUsername, mwPassword, requestTimeout, customMainPage, customZimTitle, customZimDescription, customZimLongDescription, customZimTags, customZimLanguage, customZimName, customZimFilename, withoutZimFullTextIndex, webp, format, resume, publisher: _publisher, outputDirectory: _outputDirectory, addNamespaces: _addNamespaces, onlyNamespaces: _onlyNamespaces, addContentModels: _addContentModels, javaScript: _javaScript, addModules: _addModules, customZimFavicon, optimisationCacheUrl, customFlavour, forceRender, forceSkin, langVariant, customCss, customJs, userAgent: _userAgent, stableRevision, getCategories, categoriesPageSize, mathJaxSource, mathJaxConfig, mathJaxEntryPoint, mathJaxAllPages, } = argv;
|
|
39
|
+
let { pageList, pageListToIgnore } = argv;
|
|
40
|
+
/* *********************************** */
|
|
41
|
+
/* SYSTEM VARIABLE SECTION */
|
|
42
|
+
/* *********************************** */
|
|
43
|
+
const dumpFormats = getDumps(format);
|
|
44
|
+
const langVariants = getDumps(langVariant);
|
|
36
45
|
/* ********************************* */
|
|
37
46
|
/* CUSTOM VARIABLE SECTION ********* */
|
|
38
47
|
/* ********************************* */
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
logger.setLogLevel(logLevel ?? 'info');
|
|
49
|
+
logger.debug(`Starting mwoffliner v${packageJSON.version}...`);
|
|
50
|
+
// TODO: Move it to sanitize method
|
|
51
|
+
if (pageList)
|
|
52
|
+
pageList = String(pageList);
|
|
53
|
+
if (pageListToIgnore)
|
|
54
|
+
pageListToIgnore = String(pageListToIgnore);
|
|
55
|
+
// Parse --customCss and populate Downloader so renderers can use the list
|
|
56
|
+
if (customCss) {
|
|
57
|
+
Downloader.customCssUrls = parseCustomCssUrls(String(customCss));
|
|
58
|
+
if (Downloader.customCssUrls.length > 0) {
|
|
59
|
+
logger.info(`Custom CSS URLs configured: ${Downloader.customCssUrls.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Parse --customJs and populate Downloader so renderers can use the list
|
|
63
|
+
if (customJs) {
|
|
64
|
+
Downloader.customJsUrls = parseCustomJsUrls(String(customJs));
|
|
65
|
+
if (Downloader.customJsUrls.length > 0) {
|
|
66
|
+
logger.info(`Custom JS URLs configured: ${Downloader.customJsUrls.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (mathJaxSource) {
|
|
70
|
+
Downloader.mathJaxSource = String(mathJaxSource);
|
|
71
|
+
logger.info(`MathJax source configured: ${Downloader.mathJaxSource}`);
|
|
72
|
+
}
|
|
73
|
+
if (mathJaxConfig) {
|
|
74
|
+
Downloader.mathJaxConfig = String(mathJaxConfig);
|
|
75
|
+
logger.info(`MathJax config configured: ${Downloader.mathJaxConfig}`);
|
|
76
|
+
}
|
|
77
|
+
if (mathJaxEntryPoint) {
|
|
78
|
+
Downloader.mathJaxEntryPoint = String(mathJaxEntryPoint);
|
|
79
|
+
}
|
|
80
|
+
if (mathJaxAllPages) {
|
|
81
|
+
Downloader.mathJaxAllPages = true;
|
|
82
|
+
logger.info('MathJax scripts will be injected on all pages');
|
|
83
|
+
}
|
|
48
84
|
const publisher = _publisher || config.defaults.publisher;
|
|
49
|
-
// TODO: Move it to
|
|
85
|
+
// TODO: Move it to sanitize method
|
|
50
86
|
/* HTTP user-agent string */
|
|
51
87
|
// const adminEmail = argv.adminEmail;
|
|
52
88
|
if (!isValidEmail(adminEmail)) {
|
|
53
89
|
throw new Error(`Admin email [${adminEmail}] is not valid`);
|
|
54
90
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (_speed && isNaN(_speed)) {
|
|
59
|
-
throw new Error('speed is not a number, please give a number value to --speed');
|
|
60
|
-
}
|
|
61
|
-
const cpuCount = Math.min(os.cpus().length, MAX_CPU_CORES);
|
|
62
|
-
const speed = Math.max(1, Math.round(cpuCount * (_speed || 1)));
|
|
91
|
+
const speed = _speed || 1;
|
|
92
|
+
const workers = speed >= 1 ? Math.floor(speed) : 1;
|
|
93
|
+
const pageRequestInterval = speed < 1 ? Math.round(PAGE_REQUEST_INTERVAL / speed) : PAGE_REQUEST_INTERVAL;
|
|
63
94
|
/* Check Node.js version */
|
|
64
95
|
const nodeVersionSatisfiesPackage = semver.satisfies(process.version, packageJSON.engines.node);
|
|
65
96
|
if (!nodeVersionSatisfiesPackage) {
|
|
66
97
|
logger.warn(`***********\n\n\tCurrent node version is [${process.version}]. We recommend [${packageJSON.engines.node}]\n\n***********`);
|
|
67
98
|
}
|
|
68
|
-
/*
|
|
69
|
-
logger.
|
|
70
|
-
const customProcessor = customFlavour ? new (await import(customFlavour))() : null;
|
|
99
|
+
/* Instantiate custom flavour module */
|
|
100
|
+
logger.debug(`Using custom flavour: ${customFlavour || 'no'}`);
|
|
101
|
+
const customProcessor = customFlavour ? new (await import(customFlavour)).default() : null;
|
|
71
102
|
let s3Obj;
|
|
72
103
|
// Check for S3 creds
|
|
73
104
|
if (optimisationCacheUrl) {
|
|
@@ -76,26 +107,35 @@ async function execute(argv) {
|
|
|
76
107
|
const s3Url = (s3UrlObj.protocol || 'https:') + '//' + (s3UrlObj.host || '') + (s3UrlObj.pathname || '');
|
|
77
108
|
s3Obj = new S3(s3Url, s3UrlObj.searchParams, requestTimeout * 1000 || config.defaults.requestTimeout, argv.insecure);
|
|
78
109
|
await s3Obj.initialise().then(() => {
|
|
79
|
-
logger.
|
|
110
|
+
logger.info('Successfully logged in S3');
|
|
80
111
|
});
|
|
81
112
|
}
|
|
82
113
|
// Extract S3 obj to pass to downloader class
|
|
83
114
|
const s3 = s3Obj ? s3Obj : {};
|
|
84
115
|
/* Wikipedia/... URL; Normalize by adding trailing / as necessary */
|
|
85
116
|
MediaWiki.base = mwUrl;
|
|
86
|
-
MediaWiki.getCategories = !!
|
|
117
|
+
MediaWiki.getCategories = !!getCategories;
|
|
118
|
+
MediaWiki.categoriesPageSize = categoriesPageSize || CATEGORIES_PAGE_SIZE;
|
|
87
119
|
MediaWiki.actionApiPath = mwActionApiPath;
|
|
88
120
|
MediaWiki.domain = mwDomain;
|
|
89
121
|
MediaWiki.password = mwPassword;
|
|
90
122
|
MediaWiki.username = mwUsername;
|
|
123
|
+
const javaScript = _javaScript || 'trusted';
|
|
124
|
+
const addModules = _addModules ? String(_addModules).split(',') : [];
|
|
125
|
+
const trustedJs = javaScript === 'none' ? null : javaScript === 'trusted' ? config.output.mw.js_trusted.concat(addModules) : [];
|
|
126
|
+
/* HTTP user-agent string */
|
|
127
|
+
const uaString = `${_userAgent || config.userAgent} (${adminEmail})`;
|
|
128
|
+
logger.info(`Using User-Agent: ${uaString}`);
|
|
91
129
|
/* Download helpers; TODO: Merge with something else / expand this. */
|
|
92
130
|
Downloader.init = {
|
|
93
|
-
uaString
|
|
94
|
-
|
|
131
|
+
uaString,
|
|
132
|
+
workers,
|
|
133
|
+
pageRequestInterval: pageRequestInterval,
|
|
95
134
|
reqTimeout: requestTimeout * 1000 || config.defaults.requestTimeout,
|
|
96
135
|
optimisationCacheUrl,
|
|
97
136
|
s3,
|
|
98
137
|
webp,
|
|
138
|
+
trustedJs,
|
|
99
139
|
insecure: argv.insecure,
|
|
100
140
|
};
|
|
101
141
|
/* perform login */
|
|
@@ -110,10 +150,20 @@ async function execute(argv) {
|
|
|
110
150
|
.split(',')
|
|
111
151
|
.map((a) => Number(a))
|
|
112
152
|
: [];
|
|
153
|
+
const addContentModels = _addContentModels
|
|
154
|
+
? String(_addContentModels)
|
|
155
|
+
.split(',')
|
|
156
|
+
.map((a) => a.trim())
|
|
157
|
+
: [];
|
|
158
|
+
const onlyNamespaces = _onlyNamespaces
|
|
159
|
+
? String(_onlyNamespaces)
|
|
160
|
+
.split(',')
|
|
161
|
+
.map((a) => Number(a))
|
|
162
|
+
: [];
|
|
113
163
|
/* Get MediaWiki Info */
|
|
114
164
|
let mwMetaData;
|
|
115
165
|
try {
|
|
116
|
-
mwMetaData = await MediaWiki.getMwMetaData({
|
|
166
|
+
mwMetaData = await MediaWiki.getMwMetaData({ addNamespaces, onlyNamespaces, mwModulePath, forceSkin, langVariants });
|
|
117
167
|
}
|
|
118
168
|
catch (err) {
|
|
119
169
|
logger.error('FATAL - Failed to get MediaWiki Metadata');
|
|
@@ -130,122 +180,122 @@ async function execute(argv) {
|
|
|
130
180
|
};
|
|
131
181
|
validateMetadata(metaDataRequiredKeys);
|
|
132
182
|
// Sanitizing main page
|
|
133
|
-
let mainPage =
|
|
183
|
+
let mainPage = pageList ? '' : mwMetaData.mainPage;
|
|
134
184
|
if (customMainPage) {
|
|
135
|
-
mainPage = customMainPage;
|
|
136
|
-
const mainPageUrl = MediaWiki.webUrl + encodeURIComponent(mainPage);
|
|
137
|
-
if (!(await checkApiAvailability(mainPageUrl))) {
|
|
138
|
-
throw new Error(`customMainPage doesn't return 200 status code for url ${mainPageUrl}`);
|
|
139
|
-
}
|
|
185
|
+
mainPage = customMainPage.replace(/_/g, ' ');
|
|
140
186
|
}
|
|
141
|
-
MediaWiki.
|
|
187
|
+
MediaWiki.apiCheckPageTitle = mwMetaData.mainPage;
|
|
142
188
|
await MediaWiki.hasCoordinates();
|
|
143
|
-
await MediaWiki.hasWikimediaDesktopApi();
|
|
144
|
-
const hasWikimediaMobileApi = await MediaWiki.hasWikimediaMobileApi();
|
|
145
|
-
await MediaWiki.hasRestApi();
|
|
146
|
-
await MediaWiki.hasVisualEditorApi();
|
|
147
189
|
await MediaWiki.hasActionParseApi();
|
|
148
190
|
await MediaWiki.hasModuleApi();
|
|
149
|
-
await
|
|
191
|
+
if (stableRevision && !(await MediaWiki.hasFlaggedRevs())) {
|
|
192
|
+
throw new Error('--stableRevision was specified but this wiki does not support stable revisions (FlaggedRevs extension not found)');
|
|
193
|
+
}
|
|
194
|
+
await RenderingContext.createRenderers(forceRender);
|
|
150
195
|
await RedisStore.connect();
|
|
151
|
-
const { articleDetailXId, filesToDownloadXPath, redirectsXId } = RedisStore;
|
|
152
196
|
// Output directory
|
|
153
197
|
const outputDirectory = path.isAbsolute(_outputDirectory || '') ? _outputDirectory : path.join(process.cwd(), _outputDirectory || 'out');
|
|
154
198
|
await mkdirPromise(outputDirectory);
|
|
155
|
-
logger.
|
|
199
|
+
logger.info(`Using output directory ${outputDirectory}`);
|
|
156
200
|
// Temporary directory
|
|
157
201
|
const tmpDirectory = await getTmpDirectory();
|
|
158
|
-
logger.
|
|
202
|
+
logger.info(`Using temporary directory ${tmpDirectory}`);
|
|
159
203
|
process.on('exit', async (code) => {
|
|
160
|
-
logger.
|
|
161
|
-
logger.
|
|
204
|
+
logger.info(`Exiting with code [${code}]`);
|
|
205
|
+
logger.info(`Deleting temporary directory [${tmpDirectory}]`);
|
|
162
206
|
rimraf.sync(tmpDirectory);
|
|
163
207
|
});
|
|
164
208
|
process.on('SIGTERM', async () => {
|
|
165
|
-
logger.
|
|
209
|
+
logger.info('SIGTERM');
|
|
166
210
|
await RedisStore.close();
|
|
167
211
|
process.exit(128 + 15);
|
|
168
212
|
});
|
|
169
213
|
process.on('SIGINT', async () => {
|
|
170
|
-
logger.
|
|
214
|
+
logger.info('SIGINT');
|
|
171
215
|
await RedisStore.close();
|
|
172
216
|
process.exit(128 + 2);
|
|
173
217
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/* *********************************** */
|
|
177
|
-
const dumpFormats = getDumps(format);
|
|
218
|
+
const filenameDate = new Date().toISOString().slice(0, 7);
|
|
219
|
+
const dumps = [];
|
|
178
220
|
/* ********************************* */
|
|
179
221
|
/* GET CONTENT ********************* */
|
|
180
222
|
/* ********************************* */
|
|
181
|
-
|
|
182
|
-
if (
|
|
223
|
+
const pagesToIgnore = [];
|
|
224
|
+
if (pageListToIgnore) {
|
|
183
225
|
try {
|
|
184
|
-
|
|
185
|
-
logger.
|
|
226
|
+
pagesToIgnore.push(...(await extractPageList(pageListToIgnore)));
|
|
227
|
+
logger.debug(`pageListToIgnore has [${pagesToIgnore.length}] items`);
|
|
186
228
|
}
|
|
187
229
|
catch (err) {
|
|
188
|
-
logger.error(`Failed to read
|
|
230
|
+
logger.error(`Failed to read pageListToIgnore from [${pageListToIgnore}]`, err);
|
|
189
231
|
throw err;
|
|
190
232
|
}
|
|
191
233
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
234
|
+
const pages = [];
|
|
235
|
+
if (pageList) {
|
|
194
236
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
articleListLines = articleListLines.filter((title) => !articleListToIgnoreLines.includes(title));
|
|
198
|
-
}
|
|
199
|
-
logger.info(`ArticleList has [${articleListLines.length}] items`);
|
|
237
|
+
pages.push(...(await extractPageList(pageList)));
|
|
238
|
+
logger.info(`List of pages to include in the ZIM has [${pages.length}] items`);
|
|
200
239
|
}
|
|
201
240
|
catch (err) {
|
|
202
|
-
logger.error(`Failed to read
|
|
241
|
+
logger.error(`Failed to read pageList from [${pageList}]`, err);
|
|
203
242
|
throw err;
|
|
204
243
|
}
|
|
205
244
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
245
|
+
// When the page list contains exactly one page and no custom main
|
|
246
|
+
// page was provided, use that page as the ZIM main page directly
|
|
247
|
+
//(#1891)
|
|
248
|
+
if (pages.length === 1 && !customMainPage) {
|
|
249
|
+
mainPage = pages[0];
|
|
250
|
+
logger.info(`Using single page list entry as main page: ${mainPage}`);
|
|
251
|
+
}
|
|
252
|
+
const t = await createTranslator(mwMetaData.langIso2 || 'en', 'en');
|
|
253
|
+
for (const langVar of langVariants) {
|
|
254
|
+
for (const dumpFormat of dumpFormats) {
|
|
255
|
+
const dump = new Dump(dumpFormat, langVar, {
|
|
256
|
+
tmpDir: tmpDirectory,
|
|
257
|
+
username: mwUsername,
|
|
258
|
+
password: mwPassword,
|
|
259
|
+
outputDirectory,
|
|
260
|
+
mainPage,
|
|
261
|
+
pageList,
|
|
262
|
+
publisher,
|
|
263
|
+
customZimDescription,
|
|
264
|
+
customZimLongDescription,
|
|
265
|
+
customZimTags,
|
|
266
|
+
customZimTitle,
|
|
267
|
+
customZimName,
|
|
268
|
+
customZimFilename,
|
|
269
|
+
withoutZimFullTextIndex,
|
|
270
|
+
resume,
|
|
271
|
+
minifyHtml,
|
|
272
|
+
keepEmptySections,
|
|
273
|
+
tags: customZimTags,
|
|
274
|
+
filenameDate,
|
|
275
|
+
stableRevision,
|
|
276
|
+
}, { ...mwMetaData, mainPage }, customProcessor, t);
|
|
277
|
+
dumps.push(dump);
|
|
278
|
+
// compute ZIM metadata early to fail early in case of issue ; we do that check
|
|
279
|
+
// for each dump, even if it will probably pass on all or fail on first one
|
|
280
|
+
computeZimMetadata(dump);
|
|
215
281
|
}
|
|
216
|
-
// while ((await simplifyGraph(downloader, redisStore)).deletedNodes !== 0) {
|
|
217
|
-
// // keep simplifying graph
|
|
218
|
-
// }
|
|
219
|
-
// await trimUnmirroredPages(downloader); // TODO: improve simplify graph to remove the need for a second trim
|
|
220
282
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
customZimTags,
|
|
238
|
-
customZimTitle,
|
|
239
|
-
customZimLanguage,
|
|
240
|
-
withoutZimFullTextIndex,
|
|
241
|
-
resume,
|
|
242
|
-
minifyHtml,
|
|
243
|
-
keepEmptySections: keepEmptySections ?? argv.keepEmptyParagraphs,
|
|
244
|
-
tags: customZimTags,
|
|
245
|
-
filenameDate,
|
|
246
|
-
}, { ...mwMetaData, mainPage }, customProcessor);
|
|
247
|
-
dumps.push(dump);
|
|
248
|
-
logger.log('Doing dump');
|
|
283
|
+
const allowedContentModels = ['wikitext', ...addContentModels];
|
|
284
|
+
logger.debug('Getting pages details');
|
|
285
|
+
let stime = Date.now();
|
|
286
|
+
await getPages(mainPage, pages, pagesToIgnore, allowedContentModels);
|
|
287
|
+
logger.info(`Got pages details in ${(Date.now() - stime) / 1000} seconds`);
|
|
288
|
+
// Getting total number of pages from Redis
|
|
289
|
+
logger.info(`Total pages found in Redis: ${await RedisStore.pagesStore.len()}`);
|
|
290
|
+
if (mainPage && !(await RedisStore.pagesStore.exists(mainPage)) && !(await RedisStore.redirectsStore.exists(mainPage))) {
|
|
291
|
+
throw new Error(`mainPage '${mainPage}' was not found`);
|
|
292
|
+
}
|
|
293
|
+
for (const dump of dumps) {
|
|
294
|
+
let logStr = 'Doing dump:';
|
|
295
|
+
if (dump.langVar)
|
|
296
|
+
logStr += ' variant=' + dump.langVar;
|
|
297
|
+
logStr += ` format=${dump.format || 'all'}`;
|
|
298
|
+
logger.info(`******************** ${logStr} ********************`);
|
|
249
299
|
let shouldSkip = false;
|
|
250
300
|
try {
|
|
251
301
|
dump.checkResume();
|
|
@@ -254,36 +304,41 @@ async function execute(argv) {
|
|
|
254
304
|
shouldSkip = true;
|
|
255
305
|
}
|
|
256
306
|
if (shouldSkip) {
|
|
257
|
-
logger.
|
|
307
|
+
logger.info('Skipping dump');
|
|
258
308
|
}
|
|
259
309
|
else {
|
|
310
|
+
// Reset FileManager for the new dump
|
|
311
|
+
FileManager.reset();
|
|
312
|
+
// Check scraper is still logged-in (session from previous dump might have expired
|
|
313
|
+
// due to inactivity when downloading files)
|
|
314
|
+
await MediaWiki.login(true);
|
|
260
315
|
await doDump(dump);
|
|
261
|
-
await
|
|
262
|
-
|
|
263
|
-
logger.log('Finished dump');
|
|
316
|
+
await RedisStore.filesStore.flush();
|
|
317
|
+
logger.info('Finished dump');
|
|
264
318
|
}
|
|
265
319
|
}
|
|
266
|
-
logger.
|
|
267
|
-
logger.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
logger.
|
|
271
|
-
dump.
|
|
272
|
-
const metadata = {
|
|
320
|
+
logger.info('Closing HTTP agents...');
|
|
321
|
+
logger.info('All dumping(s) finished with success.');
|
|
322
|
+
function computeZimMetadata(dump) {
|
|
323
|
+
dump.outFile = dump.computeZimFullPath();
|
|
324
|
+
logger.info(`Writing ZIM to ${dump.outFile}`);
|
|
325
|
+
dump.zimMetadata = {
|
|
273
326
|
...metaDataRequiredKeys,
|
|
274
327
|
Tags: dump.computeZimTags(),
|
|
275
|
-
Name: dump.
|
|
328
|
+
Name: dump.computeZimName(),
|
|
276
329
|
Flavour: dump.computeFlavour(),
|
|
277
330
|
Scraper: `mwoffliner ${packageJSON.version}`,
|
|
278
331
|
Source: MediaWiki.webUrl.hostname,
|
|
279
332
|
...(dump.opts.customZimLongDescription ? { LongDescription: `${dump.opts.customZimLongDescription}` } : {}),
|
|
280
333
|
};
|
|
281
|
-
validateMetadata(
|
|
282
|
-
|
|
334
|
+
validateMetadata(dump.zimMetadata);
|
|
335
|
+
}
|
|
336
|
+
async function doDump(dump) {
|
|
337
|
+
const zimCreator = new Creator().configCompression(Compression.Zstd).configVerbose(logLevel === 'debug');
|
|
283
338
|
if (!dump.opts.withoutZimFullTextIndex) {
|
|
284
339
|
zimCreator.configIndexing(true, dump.mwMetaData.langIso3);
|
|
285
340
|
}
|
|
286
|
-
zimCreator.startZimCreation(
|
|
341
|
+
zimCreator.startZimCreation(dump.outFile);
|
|
287
342
|
// Helper function to transform a Buffer into a libzim ContentProvider
|
|
288
343
|
const createBufferContentProvider = (buffer) => {
|
|
289
344
|
let dataSent = false;
|
|
@@ -298,88 +353,180 @@ async function execute(argv) {
|
|
|
298
353
|
},
|
|
299
354
|
};
|
|
300
355
|
};
|
|
301
|
-
Object.entries(
|
|
356
|
+
Object.entries(dump.zimMetadata).forEach(([key, value]) => {
|
|
302
357
|
zimCreator.addMetadata(key, Buffer.isBuffer(value) ? createBufferContentProvider(value) : value, key.startsWith('Illustration_') ? 'image/png' : 'text/plain');
|
|
303
358
|
});
|
|
304
359
|
await saveFavicon(zimCreator, metaDataRequiredKeys['Illustration_48x48@1']);
|
|
305
|
-
if (Downloader.webp) {
|
|
306
|
-
logger.log('Adding webp polyfilling JS scripts');
|
|
307
|
-
await addWebpJsScripts(zimCreator);
|
|
308
|
-
}
|
|
309
360
|
await getThumbnailsData();
|
|
310
361
|
if (!mainPage) {
|
|
311
|
-
logger.
|
|
362
|
+
logger.info('Checking Main Page rendering');
|
|
312
363
|
await createIndexPage(dump, zimCreator, true);
|
|
313
364
|
}
|
|
314
|
-
|
|
365
|
+
// Download and save custom CSS files
|
|
366
|
+
if (Downloader.customCssUrls.length > 0) {
|
|
367
|
+
logger.info(`Downloading ${Downloader.customCssUrls.length} custom CSS file(s)`);
|
|
368
|
+
const cssErrors = [];
|
|
369
|
+
for (const cssUrl of Downloader.customCssUrls) {
|
|
370
|
+
try {
|
|
371
|
+
const filename = customCssUrlToFilename(cssUrl);
|
|
372
|
+
await downloadAndSaveCustomCss(zimCreator, cssUrl, filename);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
cssErrors.push(` - Failed to download [${cssUrl}]: ${err}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (cssErrors.length > 0) {
|
|
379
|
+
throw new Error(`Failed to download custom CSS file(s):\n${cssErrors.join('\n')}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Download and save custom JS files
|
|
383
|
+
if (Downloader.customJsUrls.length > 0) {
|
|
384
|
+
logger.info(`Downloading ${Downloader.customJsUrls.length} custom JS file(s)`);
|
|
385
|
+
const jsErrors = [];
|
|
386
|
+
for (const jsUrl of Downloader.customJsUrls) {
|
|
387
|
+
try {
|
|
388
|
+
const filename = customJsUrlToFilename(jsUrl);
|
|
389
|
+
await downloadAndSaveCustomJs(zimCreator, jsUrl, filename);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
jsErrors.push(` - Failed to download [${jsUrl}]: ${err}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (jsErrors.length > 0) {
|
|
396
|
+
throw new Error(`Failed to download custom JS file(s):\n${jsErrors.join('\n')}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Download MathJax config script and store it for inline injection into each page that needs it
|
|
400
|
+
if (Downloader.mathJaxConfig) {
|
|
401
|
+
logger.info(`Loading MathJax config from [${Downloader.mathJaxConfig}]`);
|
|
402
|
+
if (/^https?:\/\//i.test(Downloader.mathJaxConfig)) {
|
|
403
|
+
const { content } = await Downloader.downloadContent(Downloader.mathJaxConfig, 'data');
|
|
404
|
+
Downloader.mathJaxConfigScript = content.toString();
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
Downloader.mathJaxConfigScript = fs.readFileSync(Downloader.mathJaxConfig, 'utf-8');
|
|
408
|
+
}
|
|
409
|
+
if (!Downloader.mathJaxConfigScript.trimStart().startsWith('<script')) {
|
|
410
|
+
throw new Error(`MathJax config loaded from [${Downloader.mathJaxConfig}] does not start with a <script tag`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Download and extract MathJax source ZIP into the ZIM
|
|
414
|
+
if (Downloader.mathJaxSource) {
|
|
415
|
+
await downloadAndSaveMathJaxSource(zimCreator, Downloader.mathJaxSource);
|
|
416
|
+
}
|
|
417
|
+
logger.info('Getting pages content');
|
|
315
418
|
stime = Date.now();
|
|
316
|
-
const { jsModuleDependencies, cssModuleDependencies, staticFilesList } = await
|
|
317
|
-
logger.
|
|
318
|
-
logger.
|
|
319
|
-
logger.log(`Found [${cssModuleDependencies.size}] style module dependencies`);
|
|
320
|
-
logger.info('Copying Static Resource Files');
|
|
419
|
+
const { jsModuleDependencies, cssModuleDependencies, staticFilesList } = await savePages(zimCreator, dump);
|
|
420
|
+
logger.info(`Got pages content in ${(Date.now() - stime) / 1000} seconds`);
|
|
421
|
+
logger.debug('Copying Static Resource Files');
|
|
321
422
|
await saveStaticFiles(staticFilesList, zimCreator);
|
|
423
|
+
if (javaScript === 'none') {
|
|
424
|
+
jsModuleDependencies.clear();
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Get list of all possible modules from startup
|
|
428
|
+
const allModules = await downloadAndSaveStartupModule(zimCreator, dump.langVar);
|
|
429
|
+
addModules.forEach((oneModule) => {
|
|
430
|
+
jsModuleDependencies.add(oneModule);
|
|
431
|
+
});
|
|
432
|
+
// Include known dynamic dependencies
|
|
433
|
+
const dynamicJsDeps = config.output.mw.js_dynamic_dependencies;
|
|
434
|
+
Object.keys(dynamicJsDeps).forEach((oneDep) => {
|
|
435
|
+
if (jsModuleDependencies.has(oneDep)) {
|
|
436
|
+
dynamicJsDeps[oneDep].forEach((extraDep) => {
|
|
437
|
+
jsModuleDependencies.add(extraDep);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// Include all dependencies of the dependencies
|
|
442
|
+
jsModuleDependencies.forEach((oneDep) => {
|
|
443
|
+
const oneModule = allModules.find((oneModule) => oneModule[0] === oneDep);
|
|
444
|
+
if (!oneModule) {
|
|
445
|
+
jsModuleDependencies.delete(oneDep);
|
|
446
|
+
return logger.warn(`Unknown JS module [${oneDep}] removed`);
|
|
447
|
+
}
|
|
448
|
+
getModuleDependencies(oneModule, allModules).forEach((extraDep) => {
|
|
449
|
+
jsModuleDependencies.add(extraDep);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
// Don't store JS for CSS modules
|
|
453
|
+
cssModuleDependencies.forEach((oneModule) => {
|
|
454
|
+
if (!addModules.includes(oneModule)) {
|
|
455
|
+
jsModuleDependencies.delete(oneModule);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
logger.info(`Found [${jsModuleDependencies.size}] js module dependencies`);
|
|
460
|
+
logger.info(`Found [${cssModuleDependencies.size}] style module dependencies`);
|
|
322
461
|
const allDependenciesWithType = [
|
|
323
462
|
{ type: 'js', moduleList: Array.from(jsModuleDependencies) },
|
|
324
463
|
{ type: 'css', moduleList: Array.from(cssModuleDependencies) },
|
|
325
464
|
];
|
|
326
|
-
logger.
|
|
465
|
+
logger.info('Downloading module dependencies');
|
|
327
466
|
await Promise.all(allDependenciesWithType.map(({ type, moduleList }) => {
|
|
328
467
|
return pmap(moduleList, (oneModule) => {
|
|
329
|
-
|
|
330
|
-
|
|
468
|
+
if (oneModule.startsWith('user')) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
return downloadAndSaveModule(zimCreator, oneModule, type, dump.langVar);
|
|
472
|
+
}, { concurrency: Downloader.workers });
|
|
331
473
|
}));
|
|
332
|
-
await
|
|
333
|
-
logger.
|
|
334
|
-
await
|
|
335
|
-
const mainPath = mainPage ? mainPage : await createIndexPage(dump, zimCreator, false);
|
|
474
|
+
await FileManager.startDownloading(zimCreator, dump);
|
|
475
|
+
logger.info('Writing Pages Redirects');
|
|
476
|
+
await writePageRedirects(dump, zimCreator);
|
|
477
|
+
const mainPath = mainPage ? makeZimPath(mainPage) : await createIndexPage(dump, zimCreator, false);
|
|
336
478
|
zimCreator.setMainPath(mainPath);
|
|
337
|
-
logger.
|
|
479
|
+
logger.info('Finishing ZIM Creation');
|
|
338
480
|
await zimCreator.finishZimCreation();
|
|
339
|
-
logger.
|
|
340
|
-
logger.
|
|
481
|
+
logger.info('Summary of scrape actions:', JSON.stringify(dump.status, null, '\t'));
|
|
482
|
+
logger.info(`ZIM is ready at ${dump.outFile}`);
|
|
341
483
|
}
|
|
342
484
|
/* ********************************* */
|
|
343
485
|
/* FUNCTIONS *********************** */
|
|
344
486
|
/* ********************************* */
|
|
345
|
-
async function
|
|
487
|
+
async function writePageRedirects(dump, zimCreator) {
|
|
346
488
|
let processed = -1;
|
|
347
|
-
const total = await
|
|
348
|
-
logger.
|
|
349
|
-
await
|
|
350
|
-
for (const
|
|
489
|
+
const total = await RedisStore.redirectsStore.len();
|
|
490
|
+
logger.info(`${total} redirects to process`);
|
|
491
|
+
await RedisStore.redirectsStore.iterateItems(Downloader.workers, async (redirects) => {
|
|
492
|
+
for (const { from, to, fragment } of Object.values(redirects)) {
|
|
351
493
|
processed += 1;
|
|
352
|
-
if (processed > 0 && processed % 5000
|
|
353
|
-
logger.
|
|
494
|
+
if (processed > 0 && processed % 5000 === 0) {
|
|
495
|
+
logger.info(`${processed} redirects have been processed (${Math.round((processed / total) * 1000) / 10} %)`);
|
|
354
496
|
}
|
|
355
|
-
if (await RedisStore.
|
|
356
|
-
logger.warn(`Skipping redirect of '${
|
|
497
|
+
if (await RedisStore.pagesStore.exists(from)) {
|
|
498
|
+
logger.warn(`Skipping redirect of '${from}' because it already exists as a page`);
|
|
357
499
|
continue;
|
|
358
500
|
}
|
|
359
|
-
if (
|
|
360
|
-
logger.warn(`Skipping redirect of '${
|
|
501
|
+
if (from === to) {
|
|
502
|
+
logger.warn(`Skipping redirect of '${from}' to self`);
|
|
361
503
|
continue;
|
|
362
504
|
}
|
|
363
|
-
if (!(await RedisStore.
|
|
364
|
-
logger.warn(`Skipping redirect of '${
|
|
505
|
+
if (!(await RedisStore.pagesStore.exists(to))) {
|
|
506
|
+
logger.warn(`Skipping redirect of '${from}' to '${to}' because target is not a known page`);
|
|
365
507
|
continue;
|
|
366
508
|
}
|
|
367
|
-
|
|
368
|
-
const redirectTitle = truncateUtf8Bytes(String(redirectId).replace(/_/g, ' '), 245);
|
|
509
|
+
const redirectTitle = truncateZimEntryTitleWords(from);
|
|
369
510
|
if (fragment) {
|
|
370
|
-
//
|
|
511
|
+
// Ignore redirects to fragment in 'nodet' since there is mostly no chance the section does exists
|
|
512
|
+
if (dump.nodet) {
|
|
513
|
+
dump.status.redirects.ignored += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
// Should we have a fragment (i.e. we redirect to a section of a page), this is not (yet) supported by libzim
|
|
371
517
|
// (to have such a redirect with a fragment inside the path), so we create a "fake" entry with only an HTML-based
|
|
372
518
|
// redirect inside
|
|
373
|
-
const htmlTemplateString = htmlRedirectTemplateCode()
|
|
519
|
+
const htmlTemplateString = replaceSafe(htmlRedirectTemplateCode())
|
|
374
520
|
.replace(/__TITLE__/g, redirectTitle)
|
|
375
521
|
// we have to replace space in fragment with underscores, see https://phabricator.wikimedia.org/T398724
|
|
376
|
-
.replace(/__TARGET__/g, `${
|
|
377
|
-
.replace(/__RELATIVE_FILE_PATH__/g, getRelativeFilePath(
|
|
378
|
-
|
|
522
|
+
.replace(/__TARGET__/g, `${makeZimPath(to)}#${fragment.replace(/ /g, '_')}`)
|
|
523
|
+
.replace(/__RELATIVE_FILE_PATH__/g, getRelativeFilePath(makeZimPath(from), ''))
|
|
524
|
+
.toString();
|
|
525
|
+
await zimCreatorMutex.runExclusive(() => zimCreator.addItem(new StringItem(makeZimPath(from), 'text/html', redirectTitle, { FRONT_ARTICLE: 1 }, htmlTemplateString)));
|
|
379
526
|
}
|
|
380
527
|
else {
|
|
381
528
|
// Otherwise we simply add a "regular" libzim redirect
|
|
382
|
-
await zimCreatorMutex.runExclusive(() => zimCreator.addRedirection(
|
|
529
|
+
await zimCreatorMutex.runExclusive(() => zimCreator.addRedirection(makeZimPath(from), redirectTitle, makeZimPath(to), { FRONT_ARTICLE: 1 }));
|
|
383
530
|
}
|
|
384
531
|
dump.status.redirects.written += 1;
|
|
385
532
|
}
|
|
@@ -391,7 +538,7 @@ async function execute(argv) {
|
|
|
391
538
|
const faviconIsRemote = customZimFavicon.includes('http');
|
|
392
539
|
let content;
|
|
393
540
|
if (faviconIsRemote) {
|
|
394
|
-
logger.
|
|
541
|
+
logger.info(`Downloading remote ZIM favicon from [${customZimFavicon}]`);
|
|
395
542
|
content = await Downloader.request({ url: customZimFavicon, method: 'GET', ...Downloader.arrayBufferRequestOptions })
|
|
396
543
|
.then((a) => a.data)
|
|
397
544
|
.catch(() => {
|
|
@@ -418,11 +565,18 @@ async function execute(argv) {
|
|
|
418
565
|
}
|
|
419
566
|
const parsedUrl = new URL(mwMetaData.logo, MediaWiki.baseUrl);
|
|
420
567
|
const logoUrl = parsedUrl.protocol ? mwMetaData.logo : MediaWiki.baseUrl.protocol + mwMetaData.logo;
|
|
421
|
-
|
|
422
|
-
|
|
568
|
+
try {
|
|
569
|
+
const { content } = await Downloader.downloadContent(logoUrl, 'image');
|
|
570
|
+
return sharp(content).resize(48, 48, resizeOptions).png().toBuffer();
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
logger.error(`Failed to fetch or resize logo declared in site info: ${logoUrl}`);
|
|
574
|
+
logger.error(`Please try specifying a customZimFavicon (--customZimFavicon=./path/to/your/file.ico)`);
|
|
575
|
+
throw err;
|
|
576
|
+
}
|
|
423
577
|
}
|
|
424
578
|
async function saveFavicon(zimCreator, data) {
|
|
425
|
-
logger.
|
|
579
|
+
logger.info('Saving favicon.png...');
|
|
426
580
|
try {
|
|
427
581
|
return zimCreator.addItem(new StringItem(`${config.output.dirs.res}/favicon.png`, 'image/png', null, { FRONT_ARTICLE: 0 }, data));
|
|
428
582
|
}
|
|
@@ -432,33 +586,33 @@ async function execute(argv) {
|
|
|
432
586
|
}
|
|
433
587
|
async function getIndexPath() {
|
|
434
588
|
for (const candidate of config.candidateIndexPath) {
|
|
435
|
-
if (!(await RedisStore.
|
|
589
|
+
if (!(await RedisStore.pagesStore.exists(candidate)) && !(await RedisStore.redirectsStore.exists(candidate))) {
|
|
436
590
|
return candidate;
|
|
437
591
|
}
|
|
438
592
|
}
|
|
439
|
-
throw new Error('All candidate main page paths are already used by
|
|
593
|
+
throw new Error('All candidate main page paths are already used by a page or a redirect');
|
|
440
594
|
}
|
|
441
595
|
/**
|
|
442
|
-
* Create a custom index page used as main page, listing all
|
|
596
|
+
* Create a custom index page used as main page, listing all pages in the ZIM
|
|
443
597
|
* Mainly used for selections which do not have a proper main page to use
|
|
444
598
|
* Supports a dry-run mode where main page is not really created, to be used before we
|
|
445
|
-
* fetch all
|
|
599
|
+
* fetch all pages. Real index creation must be done after we have fetched all pages,
|
|
446
600
|
* should some have failed we do not want to add them to the index
|
|
447
601
|
*/
|
|
448
602
|
async function createIndexPage(dump, zimCreator, dryrun) {
|
|
449
603
|
const indexPagePath = await getIndexPath();
|
|
450
|
-
const doc = domino.createDocument(
|
|
451
|
-
.replace('</head>', genHeaderCSSLink(config, 'mobile_main_page',
|
|
604
|
+
const doc = domino.createDocument(pageListHomeTemplate
|
|
605
|
+
.replace('</head>', genHeaderCSSLink(config, 'mobile_main_page', indexPagePath, config.output.dirs.res) +
|
|
452
606
|
'\n' +
|
|
453
|
-
genHeaderCSSLink(config, 'style',
|
|
607
|
+
genHeaderCSSLink(config, 'style', indexPagePath, config.output.dirs.res) +
|
|
454
608
|
'\n' +
|
|
455
|
-
genHeaderScript(config, 'images_loaded.min',
|
|
609
|
+
genHeaderScript(config, 'images_loaded.min', indexPagePath, config.output.dirs.res) +
|
|
456
610
|
'\n' +
|
|
457
|
-
genHeaderScript(config, 'masonry.min',
|
|
611
|
+
genHeaderScript(config, 'masonry.min', indexPagePath, config.output.dirs.res) +
|
|
458
612
|
'\n' +
|
|
459
|
-
genHeaderScript(config, '
|
|
613
|
+
genHeaderScript(config, 'page_list_home', indexPagePath, config.output.dirs.res) +
|
|
460
614
|
'\n' +
|
|
461
|
-
genCanonicalLink(config, dump.mwMetaData.webUrl,
|
|
615
|
+
genCanonicalLink(config, dump.mwMetaData.webUrl, indexPagePath) +
|
|
462
616
|
'\n' +
|
|
463
617
|
'\n</head>')
|
|
464
618
|
.replace(/__ASSETS_DIR__/g, config.output.dirs.assets)
|
|
@@ -466,33 +620,30 @@ async function execute(argv) {
|
|
|
466
620
|
.replace(/__MW_DIR__/g, config.output.dirs.mediawiki)
|
|
467
621
|
.replace(/__RELATIVE_FILE_PATH__/g, getRelativeFilePath(indexPagePath, '')));
|
|
468
622
|
doc.querySelector('title').innerHTML = sanitizeString(dump.mwMetaData.title) || sanitizeString(dump.opts.customZimTitle);
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
for (const
|
|
472
|
-
let articleId = articleTitle.replace(/ /g, '_');
|
|
623
|
+
const pagesWithImages = [];
|
|
624
|
+
const allPages = [];
|
|
625
|
+
for (const page of pages) {
|
|
473
626
|
// check if this is a redirect
|
|
474
|
-
const redirect = await
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
articlesWithImages.push(articleDetail);
|
|
483
|
-
if (articlesWithImages.length >= 100) {
|
|
627
|
+
const redirect = await RedisStore.redirectsStore.get(page);
|
|
628
|
+
const entryTitle = redirect ? redirect.to : page;
|
|
629
|
+
const pageDetail = await RedisStore.pagesStore.get(entryTitle);
|
|
630
|
+
if (pageDetail) {
|
|
631
|
+
allPages.push(pageDetail);
|
|
632
|
+
if (pageDetail.thumbnail && pageDetail.internalThumbnailUrl) {
|
|
633
|
+
pagesWithImages.push(pageDetail);
|
|
634
|
+
if (pagesWithImages.length >= 100) {
|
|
484
635
|
break;
|
|
485
636
|
}
|
|
486
637
|
}
|
|
487
638
|
}
|
|
488
639
|
}
|
|
489
|
-
if (
|
|
490
|
-
const
|
|
491
|
-
doc.body.innerHTML = `<div id='container'><div id='content'>${
|
|
640
|
+
if (pagesWithImages.length > MIN_IMAGE_THRESHOLD_INDEX_PAGE) {
|
|
641
|
+
const pagesWithImagesEl = pagesWithImages.map((page) => makePageImageTile(dump, page)).join('\n');
|
|
642
|
+
doc.body.innerHTML = `<div id='container'><div id='content'>${pagesWithImagesEl}</div></div>`;
|
|
492
643
|
}
|
|
493
644
|
else {
|
|
494
|
-
const
|
|
495
|
-
doc.body.innerHTML = `<ul id='list'>${
|
|
645
|
+
const pagesWithoutImagesEl = allPages.map((page) => makePageListItem(dump, page)).join('\n');
|
|
646
|
+
doc.body.innerHTML = `<ul id='list'>${pagesWithoutImagesEl}</ul>`;
|
|
496
647
|
}
|
|
497
648
|
/* Write the static html file */
|
|
498
649
|
if (!dryrun) {
|
|
@@ -501,44 +652,41 @@ async function execute(argv) {
|
|
|
501
652
|
}
|
|
502
653
|
return indexPagePath;
|
|
503
654
|
}
|
|
504
|
-
async function
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
async function updateArticleThumbnail(articleDetail, articleId) {
|
|
508
|
-
const imageUrl = articleDetail.thumbnail;
|
|
655
|
+
async function updatePageThumbnail(pageDetail, pageTitle) {
|
|
656
|
+
const imageUrl = pageDetail.thumbnail;
|
|
509
657
|
const { width: oldWidth } = getSizeFromUrl(imageUrl.source);
|
|
510
658
|
const suitableResUrl = imageUrl.source.replace(`/${oldWidth}px-`, '/500px-').replace(`-${oldWidth}px-`, '-500px-');
|
|
511
659
|
const { mult, width } = getSizeFromUrl(suitableResUrl);
|
|
512
660
|
const path = getMediaBase(suitableResUrl, false);
|
|
513
|
-
|
|
661
|
+
pageDetail.internalThumbnailUrl = getRelativeFilePath('Main_Page', getMediaBase(suitableResUrl, true));
|
|
514
662
|
await Promise.all([
|
|
515
|
-
|
|
516
|
-
|
|
663
|
+
FileManager.addFileToProcess(path, { url: urlHelper.serializeUrl(suitableResUrl), mult, width, kind: 'image' }),
|
|
664
|
+
RedisStore.pagesStore.set(pageTitle, pageDetail),
|
|
517
665
|
]);
|
|
518
666
|
}
|
|
519
667
|
async function getThumbnailsData() {
|
|
520
|
-
if (customMainPage || !
|
|
668
|
+
if (customMainPage || !pageList || pages.length <= MIN_IMAGE_THRESHOLD_INDEX_PAGE)
|
|
521
669
|
return;
|
|
522
|
-
logger.
|
|
523
|
-
let
|
|
524
|
-
let
|
|
525
|
-
while (
|
|
526
|
-
const
|
|
527
|
-
|
|
670
|
+
logger.info('Updating page thumbnails for pages');
|
|
671
|
+
let pageIndex = 0;
|
|
672
|
+
let pagesWithImages = 0;
|
|
673
|
+
while (pageIndex < pages.length && pagesWithImages < 100) {
|
|
674
|
+
const pageTitle = pages[pageIndex];
|
|
675
|
+
pageIndex++;
|
|
528
676
|
try {
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
677
|
+
const pageDetail = await RedisStore.pagesStore.get(pageTitle);
|
|
678
|
+
if (!pageDetail || !pageDetail.thumbnail)
|
|
531
679
|
continue;
|
|
532
|
-
await
|
|
533
|
-
|
|
680
|
+
await updatePageThumbnail(pageDetail, pageTitle);
|
|
681
|
+
pagesWithImages++;
|
|
534
682
|
}
|
|
535
683
|
catch {
|
|
536
|
-
logger.warn(`Failed to parse thumbnail for [${
|
|
684
|
+
logger.warn(`Failed to parse thumbnail for [${pageTitle}], skipping...`);
|
|
537
685
|
}
|
|
538
686
|
}
|
|
539
687
|
}
|
|
540
688
|
MediaWiki.reset();
|
|
541
|
-
RedisStore.close();
|
|
689
|
+
await RedisStore.close();
|
|
542
690
|
return dumps;
|
|
543
691
|
}
|
|
544
692
|
export { execute };
|