md-fileserver
Version:
Locally view markdown files in a browser
159 lines (137 loc) • 3.73 kB
JavaScript
const fs = require('fs')
const WebSocket = require('ws')
const mw = require('./middlewares.js')
const { uri2filename, filename2uri } = require('./utils.js')
const MARKDOWNEXT = mw.MARKDOWNEXT
const WATCHOPTS = { persistent: true, recursive: false }
const M = {}
// list of connected browser pages
let connects = []
const watchedFiles = new Set()
const fileWatchers = new Map()
const changedFiles = new Map()
/**
* parse string to object
* @private
* @param {String} msg
* @return {Object}
*/
const parse = (msg) => {
try {
return JSON.parse(msg)
} catch (e) {
console.error(e)
return {}
}
}
/**
* broadcast msg to all connected clients
* @private
* @param {Object} msg
*/
const broadcast = (msg, origin) => {
const message = JSON.stringify(msg)
connects = connects.filter((connection) => {
const isOpen = connection.readyState === connection.OPEN
if (isOpen && connection !== origin) {
connection.send(message)
return true
}
return isOpen
})
}
/**
* create websockets
* @param {http.Server} server
*/
M.websocket = function (server) {
// create the server
const wsServer = new WebSocket.Server({ server })
// WebSocket server
wsServer.on('connection', function (connection, req) {
if (/http:\/\/(localhost|127.0.0.1):/.test(req.headers.origin)) {
connection.on('message', (message) => {
const msg = parse(message)
console.log(msg)
broadcast(msg, connection)
})
connection.on('close', () => {
connects = connects.filter((conn) => conn !== connection)
})
connection.on('error', (error) => {
console.error('WebSocket error:', error)
connection.terminate()
})
connects.push(connection)
} else {
connection.terminate()
}
})
}
const lastchange = () => (Date.now() / 1000) | 0
/**
* callback of fs.watch
* @param {String} file - filename which has changed
*/
function onChange(file) {
let debounceTimer
return function (event) {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
const currentTime = lastchange()
if (changedFiles.get(file) !== currentTime) {
const change = filename2uri(file)
broadcast({ change })
changedFiles.set(file, currentTime)
}
if (event === 'rename') {
if (fileWatchers.has(file)) {
fileWatchers.get(file).close()
fileWatchers.delete(file)
watchedFiles.delete(file)
}
try {
const watcher = fs.watch(file, WATCHOPTS, onChange(file))
fileWatchers.set(file, watcher)
watchedFiles.add(file)
// eslint-disable-next-line no-unused-vars
} catch (e) {
// file may not be present after renaming...
}
}
}, 100)
}
}
/**
* middleware which registers file for watching
* @param {Object} req
* @param {Object} res
* @param {Function} next
*/
M.watch = function (req, res, next) {
const file = uri2filename(req.url)
if (!req.isDirectory && MARKDOWNEXT.test(file)) {
if (!watchedFiles.has(file)) {
const watcher = fs.watch(file, WATCHOPTS, onChange(file))
watcher.on('close', () => {
watchedFiles.delete(file)
fileWatchers.delete(file)
})
fileWatchers.set(file, watcher)
watchedFiles.add(file)
}
changedFiles.set(file, lastchange())
}
next()
}
// Cleanup function for tests/server shutdown
M.cleanup = function () {
console.log('cleanup')
fileWatchers.forEach((watcher) => watcher.close())
fileWatchers.clear()
watchedFiles.clear()
changedFiles.clear()
connects.forEach((conn) => conn.terminate())
connects = []
}
module.exports = M