Change watch node

I have a simple flow which monitors a directory and then pushes files across into a Telegram node to send pictures to Telegram.

My issue is that each folder that is watched is split up into years and months so for eg /pictures/2024/05. If I monitor all of /pictures/ is causes CPU to run quite high, so the solution is to only monitor the year or the month.

Problem with this approach is that the path needs to be manually changed. Is it possible to use the nodered API to modify the path when either the Month or Year changes? The watch node doesn't have an input so that it can be changed automatically :frowning:

[
    {
        "id": "086e6be5111544b7",
        "type": "watch",
        "z": "8907e2ab117014e1",
        "name": "",
        "files": "/pictures/2024",
        "recursive": true,
        "x": 350,
        "y": 540,
        "wires": [
            [
                "5a8e329bad4ac391"
            ]
        ]
    },
    {
        "id": "5a8e329bad4ac391",
        "type": "function",
        "z": "8907e2ab117014e1",
        "name": "Filter Files & Prepare Telegram Message",
        "func": "if (msg.event === 'update' &&\n    (msg.file.endsWith('.jpg') || msg.file.endsWith('.mp4')) &&\n    !msg.payload.includes('.ocTransferId')) {\n    var fileType = msg.file.endsWith('.jpg') ? 'document' : 'video';\n    return {\n        payload: {\n            content: msg.filename,\n            type: fileType,\n            chatId: '-1234567890'\n        }\n    };\n}\nreturn null;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 660,
        "y": 540,
        "wires": [
            [
                "acb4c27d6902f793"
            ]
        ]
    }
]

Unfortunately, the watch node doesn't allow override inputs. However, you could use a function node and install chokidar and write your own monitor.

Why not use one watch node on the picture folder, and check watch sub directories.
Then you can parse the filename to get the year and month path.

Are you sure about that? Watch should be efficient in Linux I think. Configure it to watch /pictures but do not wire anything to it's output and see if the CPU is high. That will check whether it is something in your flow causing the issue.

So I've done it using chokdir as you suggested, with a great lot of help from GPT4o

Anyways, in case this is helpful for anyone, the code is below.

This watches a folder structure for files automatically uploaded from the NextCloud app on an Android phone and then pushes the file out to a Telegram bot, in this case any pictures or videos taken on the same phone. It now changes the watched folder depending on the date as the Nextcloud app will alter the upload path for the Year and Month (likely also a good idea for organising photos)

This completes faster than the built in Watch node.

You'll need an Inject node to create the watcher, prob a good idea to make it run on start and then routinely at midnight or something. (maybe an overkill)

// Import required modules from the global context
const chokidar = global.get('chokidar');
const fs = global.get('fs');
const path = global.get('path');

// Constants
const CHAT_ID = "-123456789";
const WATCHER_ID = "uniqueWatcherId";
const DEBOUNCE_RATE_SECONDS = 1; // 1 second debounce rate
const TELEGRAM_RATE_LIMIT_MS = DEBOUNCE_RATE_SECONDS * 1000; // Convert to milliseconds

// Get current year and month
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');

// Set the path to monitor using the current year and month
const pathToWatch = `/pictures/${year}/${month}`;

// Function to get the list of existing files
function getExistingFiles(dirPath) {
    try {
        return fs.readdirSync(dirPath).map(file => path.join(dirPath, file));
    } catch (err) {
        node.error(`Error reading directory: ${err.message}`);
        return [];
    }
}

// Check if a watcher already exists with the specific ID
let existingWatcher = global.get(WATCHER_ID);
if (existingWatcher) {
    // Close the existing watcher
    existingWatcher.close();
    // Remove the existing watcher from the global context
    global.set(WATCHER_ID, null);
}

// Get the list of existing files
const existingFiles = getExistingFiles(pathToWatch);
global.set('existingFiles', existingFiles);

// Initialize watcher
const watcher = chokidar.watch(path.join(pathToWatch, '*.{jpg,mp4}'), {
    persistent: true,
    ignoreInitial: true // Ignore initial add events
});

// Function to determine the file type
function getFileType(filePath) {
    if (filePath.endsWith('.jpg')) {
        return 'document';
    } else if (filePath.endsWith('.mp4')) {
        return 'video';
    } else {
        return 'unknown';
    }
}

// Debounce mechanism to avoid rapid multiple events and respect Telegram rate limits
let debounceTimeout;
function debounceEvent(callback, delay = TELEGRAM_RATE_LIMIT_MS) {
    if (debounceTimeout) {
        clearTimeout(debounceTimeout);
    }
    debounceTimeout = setTimeout(callback, delay);
}

// Define event listeners
watcher
    .on('add', filePath => {
        debounceEvent(() => {
            const existingFiles = global.get('existingFiles');
            if (!existingFiles.includes(filePath)) {
                let msg = {};
                msg.payload = {
                    content: filePath,
                    chatId: CHAT_ID,
                    type: getFileType(filePath)
                };
                node.send(msg);
            }
        });
    })
    .on('change', filePath => {
        debounceEvent(() => {
            let msg = {};
            msg.payload = {
                content: filePath,
                chatId: CHAT_ID,
                type: getFileType(filePath)
            };
            node.send(msg);
        });
    })
    .on('unlink', filePath => {
        debounceEvent(() => {
            let msg = {};
            msg.payload = {
                content: filePath,
                chatId: CHAT_ID,
                type: getFileType(filePath)
            };
            node.send(msg);
        });
    })
    .on('error', error => {
        node.error(`Watcher error: ${error.message}`);
    });

// Save watcher to global context with the unique ID to prevent multiple watchers
global.set(WATCHER_ID, watcher);

return null;


This needs to be included into NodeRed's settings.js and then restart NodeRed

functionGlobalContext: {
    chokidar: require('chokidar'),
    fs: require('fs'),
    path: require('path')
},

It is usually very fast on native filing systems. But if it isn't a native FS, it can quickly get very slow.

:slight_smile:

Just at start-up should be fine, I've a few of those in my flows and they all seem very reliable.

That is not the only way with newer versions of Node-RED, you can allow function nodes to have their own requires. That method makes the module available to the function node directly as in this example:

Thanks for sharing with the forum, very useful.

Thanks, I've made those final tweaks! :slight_smile: