Describe the issue/error/question
After trying the Local File Trigger to watch changes on Excel files on Windows, the problem I’m facing is that every app I use to modify excel files (such as Microsoft Excel, OpenOffice, etc.) create a temp file to lock the file for other programs. Then, when the file is saved, the file that is being watched by chokidar in the Local File Trigger node is the temp file, and there is no way to avoid that.
I have tried to add a glob pattern into the “Ignore” field to avoid watching events from files that don’t have ‘.xlsx’, ‘.xls’ or ‘.csv’ extension, but then the node never gets triggered. This is the glob pattern I have used in the Ignore field:
**/!(*.xlsx|*.csv|*.xls)
After this, I have also tried to create a custom node copying the code from Local File Trigger node and updating it to the following code:
import { ITriggerFunctions } from 'n8n-core';
import { IDataObject, INodeType, INodeTypeDescription, ITriggerResponse } from 'n8n-workflow';
import { watch } from 'chokidar';
export class ExcelWatcherTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Excel Watcher Trigger',
name: 'excelWatcherTrigger',
icon: 'fa:folder-open',
group: ['trigger'],
version: 1,
subtitle: '=Path: {{$parameter["path"]}}',
description: 'Triggers a workflow on file system changes (specific for excel files)',
eventTriggerDescription: '',
defaults: {
name: 'Excel Watcher Trigger',
color: '#404040',
},
inputs: [],
outputs: ['main'],
properties: [
{
displayName: 'Trigger On',
name: 'triggerOn',
type: 'options',
options: [
{
name: 'Changes to a Specific File',
value: 'file',
},
{
name: 'Changes Involving a Specific Folder',
value: 'folder',
},
],
required: true,
default: '',
},
{
displayName: 'File to Watch',
name: 'path',
type: 'string',
displayOptions: {
show: {
triggerOn: ['file'],
},
},
default: '',
placeholder: '/data/invoices/1.xlsx',
},
{
displayName: 'Folder to Watch',
name: 'path',
type: 'string',
displayOptions: {
show: {
triggerOn: ['folder'],
},
},
default: '',
placeholder: '/data/invoices',
},
{
displayName: 'Watch for',
name: 'events',
type: 'multiOptions',
displayOptions: {
show: {
triggerOn: ['folder'],
},
},
options: [
{
name: 'File Added',
value: 'add',
description: 'Triggers whenever a new file was added',
},
{
name: 'File Changed',
value: 'change',
description: 'Triggers whenever a file was changed',
},
{
name: 'File Deleted',
value: 'unlink',
description: 'Triggers whenever a file was deleted',
},
{
name: 'Folder Added',
value: 'addDir',
description: 'Triggers whenever a new folder was added',
},
{
name: 'Folder Deleted',
value: 'unlinkDir',
description: 'Triggers whenever a folder was deleted',
},
],
required: true,
default: [],
description: 'The events to listen to',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Include Linked Files/Folders',
name: 'followSymlinks',
type: 'boolean',
default: true,
description:
'Whether linked files/folders will also be watched (this includes symlinks, aliases on MacOS and shortcuts on Windows). Otherwise only the links themselves will be monitored).',
},
{
displayName: 'Excel extensions',
name: 'extensions',
type: 'string',
default: '',
placeholder: 'xls,xlsx,csv',
description:
'Excel extensions to be watched. If not set, all extensions will be watched. Without dot and separated by comma.',
},
// eslint-disable-next-line n8n-nodes-base/node-param-default-missing
{
displayName: 'Max Folder Depth',
name: 'depth',
type: 'options',
options: [
{
name: '1 Levels Down',
value: 1,
},
{
name: '2 Levels Down',
value: 2,
},
{
name: '3 Levels Down',
value: 3,
},
{
name: '4 Levels Down',
value: 4,
},
{
name: '5 Levels Down',
value: 5,
},
{
name: 'Top Folder Only',
value: 0,
},
{
name: 'Unlimited',
value: -1,
},
],
default: -1,
description: 'How deep into the folder structure to watch for changes',
},
],
},
],
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const triggerOn = this.getNodeParameter('triggerOn') as string;
const path = this.getNodeParameter('path') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
let events: string[];
if (triggerOn === 'file') {
events = ['change'];
} else {
events = this.getNodeParameter('events', []) as string[];
}
const watcher = watch(path, {
persistent: true,
ignoreInitial: true,
followSymlinks:
options.followSymlinks === undefined ? true : (options.followSymlinks as boolean),
depth: [-1, undefined].includes(options.depth as number)
? undefined
: (options.depth as number),
});
const executeTrigger = (event: string, path: string) => {
this.emit([this.helpers.returnJsonArray([{ event, path }])]);
};
const isExcel = (path: string): boolean => {
const extensions = options.extensions as string;
if (extensions === undefined || extensions === '') {
return true;
}
const extensionArray = path.split(',')
let isExcel = false;
for (let i = 0; i < extensionArray.length; i++) {
if (path.endsWith(extensionArray[i])) {
isExcel = true;
break;
}
}
return isExcel;
}
for (const eventName of events) {
watcher.on(eventName, (path: string, stats) => {
if (isExcel(path)) {
executeTrigger(eventName, path)
}
});
}
function closeFunction() {
return watcher.close();
}
return {
closeFunction,
};
}
}
I need to get the path from the xlsx file instead of the temp file in order to read the binary content of the file with the Read Binary File node and then pass this binary data to the Spreadsheet File to process it.
If I provide the temp file path to the Read Binary File node then I get an error saying the file doesn’t exist. Also a curious thing is that the path gotten by n8n (the temp’s file path) is not the same apparently as the file I get in the bash (for example, the hidden file shown in bash is called ‘~$onetab_backup.xlsx’ and the path provided by chokidar in the node is ‘BC8D4A10’.
Also as a side note, I have created a node script that does exactly what I need to do without n8n using chokidar, and it receives two events when a xlsx is saved in Excel, first the temp file and second the original file, which it works for me because then I only need to discard the first event. This is the code I’m using:
import { watch } from 'chokidar';
import { join } from 'path';
import { isExcel, parseExcelToJson } from './lib/excel';
import { saveJsonToFile } from './lib/json';
export function startWatchingFiles(path: string): void {
// Initialize watcher. Use env var for absolute path
const watcher = watch(path, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
// React on file change
watcher.on('change', (path, stats) => {
if (stats && isExcel(path)) {
// Here we can parse it with different templates
const result = parseExcelToJson(path);
saveJsonToFile(join(__dirname, '..', 'data', 'output', 'output.json'), result);
}
});
}
// Place files inside data/input folder
startWatchingFiles(join(__dirname, '..', 'data', 'input'));
Information on your n8n setup
- n8n version: 0.197.1
- Database you’re using (default: SQLite): SQLite
- Running n8n with the execution process [own(default), main]: own
- Running n8n via [Docker, npm, n8n.cloud, desktop app]: npm