Local File Trigger not working with Excel files in Windows

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

Hi @softwarexperts, welcome to the community!

You could try an expression such as {{ [ '**/*.tmp', '**/.~lock.*' ] }} in the ignore field. This worked for me on Windows 11 with OpenOffice 7.4 Calc:

Hello @MutedJam, thank you for your fast reply. I have tried the expression you have provided and it did not work as expected. Instead, what happened is: The first time I saved the file the trigger wasn’t fired, and after a second save, the trigger got fired but without the desired result. This is the outcome:

I’m working with Microsoft Excel and Windows 10. However after trying with another computer with OpenOffice it didn’t work either. Am I doing something wrong?

From your screenshot it seems you are using this as a plain text value rather than expression. Can you try converting your value into an expression like below?

image

You might also need to add another pattern inside the array handling the temporary File created by Excel.

It looks like it is bypassing the condition in the Ignore field…

It keeps throwing the wrong file and it doesn’t ignore it.

By the way, after activating the workflow I realized that it handles the temp file in the first execution and then the desired file in the next execution, however I have no way to prepare next nodes as they are dependent on the output data of the desired file and in the workflow creator I only get access to temp files and then data from the file is not available to get configured in following nodes. What can I do?

Hey @softwarexperts,

You are using /*.tmp in the pattern, It might be worth doing **/*.tmp instead

That’s a good point! Have copied the string from @MutedJam without realizing that. However after trying it keeps ignoring the Ignore field:

It happens that sometimes instead of showing the “.~lock.onetab_backup.xlsx#” version it shows the “whatever.tmp” version, and I’m using the exact same application to save the file (LibreOffice Calc) and the exact same node. Also sometimes I need to save twice to trigger the node.

What’s really weird here is that your n8n UI previews the result of this expression as null rather than as an array (like it does in my screenshot). Could you share your node data? Simply select it on the n8n canvas and press Ctrl+C, you can afterwards paste it here on the forum using Ctrl+V.

Here you have:

Mmmm ok got the error… After copy-pasting your text the quote was ‘ instead of '…
Thanks for your help, it worked now with LibreOffice!!

However, with excel it keeps showing the temp file as it is not in any of the patterns described in the Ignore field:

I suppose I have to add another glob pattern negating any file that don’t have an extension?

1 Like

Yes, adding another object to the array excluding the Excel temp file should do the job, though I am not sure how these filenames could look like.

I’ve blocked some time next week to play around with this and will let you know if I can find anything suitable here.

Understand. Basically they are 8 hexadecimal digits without any kind of extension, an example can be found in the previous image posted, something like this:

C:\Users\Usuario\Documents\Test\12D8FF10
1 Like

Thanks @softwarexperts, when quickly testing this the below expression did the job for me:

{{ [ '**/*.tmp', '**/.~lock.*', '**/????????' ] }}

There might be more elegant ways to specify a glob pattern for file names including 8 characters and no extension, but I am not exactly familiar with glob and the above works :slight_smile:

So perhaps you can give this a go on your end?

Thank you so much for taking your time writing the final solution. Maybe the best solution would be ignoring any file without extension, but yours did the work for me as well!
I really appreaciate your quick response since I posted the problem here, having an active community gives more confidence with your product.

1 Like