diff --git a/docs/config.rst b/docs/config.rst index cb84e46..e2849cb 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -37,6 +37,7 @@ Example: "pbfAlias": "pbf", "serveAllFonts": false, "serveAllStyles": false, + "watchMbtiles": false, "serveStaticMaps": true, "allowRemoteMarkerIcons": true, "allowInlineMarkerImages": true, @@ -171,6 +172,13 @@ If this option is enabled, all the styles from the ``paths.styles`` will be serv The process will also watch for changes in this directory and remove/add more styles dynamically. It is recommended to also use the ``serveAllFonts`` option when using this option. +``watchMbtiles`` +------------------------ + +If this option is enabled, all the opened Mbtiles are watched for changes and automatically reloaded. +The new data is then severed immediately. There is a small downtime for rendered endpoints. +Mbtiles have to be replaced atomically. i.e. moving the file from the same filesystem. Modifying an existing file will crash the server. + ``serveStaticMaps`` ------------------------ diff --git a/src/main.js b/src/main.js index 7523aa9..ec18298 100644 --- a/src/main.js +++ b/src/main.js @@ -111,6 +111,7 @@ const startWithInputFile = async (inputFile) => { const config = { options: { + watchMbtiles: true, paths: { root: styleDir, fonts: 'fonts', diff --git a/src/serve_data.js b/src/serve_data.js index 1936da6..03c7dd7 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -365,6 +365,9 @@ export const serve_data = { return app; }, + remove: (repo, id) => { + delete repo[id]; + }, add: async (options, repo, params, id, publicUrl) => { let inputFile; let inputType; diff --git a/src/serve_rendered.js b/src/serve_rendered.js index 3e5c94e..15f55eb 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -883,6 +883,7 @@ export const serve_rendered = { renderersStatic: [], sources: {}, sourceTypes: {}, + styleFile: '', }; let styleJSON; @@ -1080,6 +1081,7 @@ export const serve_rendered = { }; const styleFile = params.style; + map.styleFile = styleFile; const styleJSONPath = path.resolve(options.paths.styles, styleFile); try { styleJSON = JSON.parse(await fsp.readFile(styleJSONPath)); @@ -1300,6 +1302,9 @@ export const serve_rendered = { item.map.renderersStatic.forEach((pool) => { pool.close(); }); + Object.entries(item.map.sources).forEach(([key, source]) => { + delete item.map.sources[key]; + }); } delete repo[id]; }, diff --git a/src/server.js b/src/server.js index 39808e3..985c9f6 100644 --- a/src/server.js +++ b/src/server.js @@ -292,6 +292,69 @@ function start(opts) { startupPromises.push( serve_data.add(options, serving.data, item, id, opts.publicUrl), ); + + if (options.watchMbtiles) { + console.log(`Watching Mbtile "${item.mbtiles}" for changes...`); + + const watcher = chokidar.watch( + path.join(options.paths.mbtiles, item.mbtiles), + { + ignoreInitial: true, + // wait 10 seconds after the last change before updating. Otherwise, cases where a file is constantly replaced + // will create race conditions and can crash the server + awaitWriteFinish: { stabilityThreshold: 10000 }, + }, + ); + + watcher.on('all', (eventType, filename) => { + if (filename) { + if (eventType === 'add' || eventType === 'change') { + console.log(`MBTiles "${filename}" changed, updating...`); + + serve_data.remove(serving.data, id); + let newItem = { + mbtiles: filename, + }; + serve_data.add(options, serving.data, newItem, id, opts.publicUrl); + + if (!isLight) { + Object.entries(serving.rendered).forEach( + ([serving_key, serving_value]) => { + // check if source is used in serving + Object.values(serving_value.map.sources).forEach( + (source_value) => { + const newFileInode = fs.statSync(filename).ino; + // we check if the filename is the same and the inode has changed + // the inode check is necessary because it could be that multiple mbtiles + // were changed and we already changed to the new file. This can lead to race-conditions + if ( + source_value.filename === filename && + source_value._stat.ino !== newFileInode + ) { + // remove from serving and add back + serve_style.remove(serving.styles, serving_key); + serve_rendered.remove(serving.rendered, serving_key); + + const item = { + style: serving_value.map.styleFile, + }; + addStyle(serving_key, item, false, false); + } + }, + ); + }, + ); + } + } + // we intentionally don't handle the 'unlink' event here. If the file is deleted, the file descriptor is still valid, + // everything will continue to work + } + }); + + watcher.on('error', (error) => { + console.error(`Failed to watch file: ${error}`); + }); + } } if (options.serveAllStyles) {