mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(editor): Optimize the logic of replacing illegal characters in download workflow naming (#16260)
This commit is contained in:
@@ -60,6 +60,7 @@ import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
|||||||
import { type BaseTextKey, useI18n } from '@n8n/i18n';
|
import { type BaseTextKey, useI18n } from '@n8n/i18n';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||||
|
import { sanitizeFilename } from '@/utils/fileUtils';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -466,7 +467,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
|||||||
});
|
});
|
||||||
|
|
||||||
let name = props.name || 'unsaved_workflow';
|
let name = props.name || 'unsaved_workflow';
|
||||||
name = name.replace(/[^a-z0-9]/gi, '_');
|
name = sanitizeFilename(name);
|
||||||
|
|
||||||
telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
||||||
saveAs(blob, name + '.json');
|
saveAs(blob, name + '.json');
|
||||||
|
|||||||
94
packages/frontend/editor-ui/src/utils/fileUtils.test.ts
Normal file
94
packages/frontend/editor-ui/src/utils/fileUtils.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { sanitizeFilename } from './fileUtils';
|
||||||
|
|
||||||
|
describe('sanitizeFilename', () => {
|
||||||
|
it('should return normal filenames unchanged', () => {
|
||||||
|
expect(sanitizeFilename('normalfile')).toBe('normalfile');
|
||||||
|
expect(sanitizeFilename('my-file_v2')).toBe('my-file_v2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty and invalid inputs', () => {
|
||||||
|
expect(sanitizeFilename('')).toBe('untitled');
|
||||||
|
expect(sanitizeFilename(null as unknown as string)).toBe('untitled');
|
||||||
|
expect(sanitizeFilename(undefined as unknown as string)).toBe('untitled');
|
||||||
|
expect(sanitizeFilename('filename ')).toBe('filename');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace forbidden characters', () => {
|
||||||
|
expect(sanitizeFilename('hello:world')).toBe('hello_world');
|
||||||
|
expect(sanitizeFilename('file<name>')).toBe('file_name_');
|
||||||
|
expect(sanitizeFilename('file/name')).toBe('file_name');
|
||||||
|
expect(sanitizeFilename('file|name')).toBe('file_name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Unicode characters', () => {
|
||||||
|
expect(sanitizeFilename('file\u200Bname')).toBe('filename'); // Zero-width space
|
||||||
|
expect(sanitizeFilename('file\u00A0name')).toBe('file name'); // Non-breaking space
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases', () => {
|
||||||
|
expect(sanitizeFilename('.')).toBe('untitled');
|
||||||
|
expect(sanitizeFilename('..')).toBe('untitled');
|
||||||
|
expect(sanitizeFilename(' ... ')).toBe('untitled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle length limits', () => {
|
||||||
|
const longName = 'a'.repeat(250);
|
||||||
|
const result = sanitizeFilename(longName, 50);
|
||||||
|
expect(result.length).toBeLessThanOrEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 15 most complex world languages (by writing system complexity)
|
||||||
|
it('should support complex writing systems', () => {
|
||||||
|
// 1. Arabic - Right-to-left, complex ligatures
|
||||||
|
expect(sanitizeFilename('سير العمل الخاص بي')).toBe('سير العمل الخاص بي');
|
||||||
|
|
||||||
|
// 2. Burmese - Complex script with stacked characters
|
||||||
|
expect(sanitizeFilename('ကျွန်ုပ်၏ လုပ်ငန်းစဉ်')).toBe('ကျွန်ုပ်၏ လုပ်ငန်းစဉ်');
|
||||||
|
|
||||||
|
// 3. Thai - Complex script, no word separators
|
||||||
|
expect(sanitizeFilename('เวิร์กโฟลว์ของฉัน')).toBe('เวิร์กโฟลว์ของฉัน');
|
||||||
|
|
||||||
|
// 4. Hindi - Devanagari script with complex conjuncts
|
||||||
|
expect(sanitizeFilename('मेरा वर्कफ़्लो')).toBe('मेरा वर्कफ़्लो');
|
||||||
|
|
||||||
|
// 5. Bengali - Complex script with conjunct consonants
|
||||||
|
expect(sanitizeFilename('আমার ওয়ার্কফ্লো')).toBe('আমার ওয়ার্কফ্লো');
|
||||||
|
|
||||||
|
// 6. Urdu - Right-to-left, Arabic-based script
|
||||||
|
expect(sanitizeFilename('میرا ورک فلو')).toBe('میرا ورک فلو');
|
||||||
|
|
||||||
|
// 7. Chinese - Logographic writing system
|
||||||
|
expect(sanitizeFilename('我的工作流')).toBe('我的工作流');
|
||||||
|
|
||||||
|
// 8. Japanese - Mixed scripts (Hiragana, Katakana, Kanji)
|
||||||
|
expect(sanitizeFilename('私のワークフロー')).toBe('私のワークフロー');
|
||||||
|
|
||||||
|
// 9. Korean - Hangul syllabic blocks
|
||||||
|
expect(sanitizeFilename('내 워크플로우')).toBe('내 워크플로우');
|
||||||
|
|
||||||
|
// 10. Russian - Cyrillic script
|
||||||
|
expect(sanitizeFilename('Мой рабочий процесс')).toBe('Мой рабочий процесс');
|
||||||
|
|
||||||
|
// 11. Tamil - Complex script with vowel marks
|
||||||
|
expect(sanitizeFilename('எனது பணிப்பாய்வு')).toBe('எனது பணிப்பாய்வு');
|
||||||
|
|
||||||
|
// 12. Telugu - Complex script with conjunct consonants
|
||||||
|
expect(sanitizeFilename('నా వర్క్ఫ్లో')).toBe('నా వర్క్ఫ్లో');
|
||||||
|
|
||||||
|
// 13. Marathi - Devanagari script
|
||||||
|
expect(sanitizeFilename('माझा वर्कफ्लो')).toBe('माझा वर्कफ्लो');
|
||||||
|
|
||||||
|
// 14. Gujarati - Complex script with vowel modifications
|
||||||
|
expect(sanitizeFilename('મારો વર્કફ્લો')).toBe('મારો વર્કફ્લો');
|
||||||
|
|
||||||
|
// 15. Punjabi - Gurmukhi script
|
||||||
|
expect(sanitizeFilename('ਮੇਰਾ ਵਰਕਫਲੋ')).toBe('ਮੇਰਾ ਵਰਕਫਲੋ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed complex scripts with special characters', () => {
|
||||||
|
expect(sanitizeFilename('工作流程/ワークフロー')).toBe('工作流程_ワークフロー');
|
||||||
|
expect(sanitizeFilename('वर्कफ्लो:العمل')).toBe('वर्कफ्लो_العمل');
|
||||||
|
expect(sanitizeFilename('프로세스|процесс')).toBe('프로세스_процесс');
|
||||||
|
});
|
||||||
|
});
|
||||||
89
packages/frontend/editor-ui/src/utils/fileUtils.ts
Normal file
89
packages/frontend/editor-ui/src/utils/fileUtils.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Filename sanitization utilities
|
||||||
|
* For handling cross-platform filename compatibility issues
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Constants definition
|
||||||
|
const INVALID_CHARS_REGEX = /[<>:"/\\|?*\u0000-\u001F\u007F-\u009F]/g;
|
||||||
|
const ZERO_WIDTH_CHARS_REGEX = /[\u200B-\u200D\u2060\uFEFF]/g;
|
||||||
|
const UNICODE_SPACES_REGEX = /[\u00A0\u2000-\u200A]/g;
|
||||||
|
const LEADING_TRAILING_DOTS_SPACES_REGEX = /^[\s.]+|[\s.]+$/g;
|
||||||
|
const WINDOWS_RESERVED_NAMES = new Set([
|
||||||
|
'CON',
|
||||||
|
'PRN',
|
||||||
|
'AUX',
|
||||||
|
'NUL',
|
||||||
|
'COM1',
|
||||||
|
'COM2',
|
||||||
|
'COM3',
|
||||||
|
'COM4',
|
||||||
|
'COM5',
|
||||||
|
'COM6',
|
||||||
|
'COM7',
|
||||||
|
'COM8',
|
||||||
|
'COM9',
|
||||||
|
'LPT1',
|
||||||
|
'LPT2',
|
||||||
|
'LPT3',
|
||||||
|
'LPT4',
|
||||||
|
'LPT5',
|
||||||
|
'LPT6',
|
||||||
|
'LPT7',
|
||||||
|
'LPT8',
|
||||||
|
'LPT9',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DEFAULT_FALLBACK_NAME = 'untitled';
|
||||||
|
const MAX_FILENAME_LENGTH = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a filename to be compatible with Mac, Linux, and Windows file systems
|
||||||
|
*
|
||||||
|
* Main features:
|
||||||
|
* - Replace invalid characters (e.g. ":" in hello:world)
|
||||||
|
* - Handle Windows reserved names
|
||||||
|
* - Limit filename length
|
||||||
|
* - Normalize Unicode characters
|
||||||
|
*
|
||||||
|
* @param filename - The filename to sanitize (without extension)
|
||||||
|
* @param maxLength - Maximum filename length (default: 200)
|
||||||
|
* @returns A sanitized filename (without extension)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* sanitizeFilename('hello:world') // returns 'hello_world'
|
||||||
|
* sanitizeFilename('CON') // returns '_CON'
|
||||||
|
* sanitizeFilename('') // returns 'untitled'
|
||||||
|
*/
|
||||||
|
export const sanitizeFilename = (
|
||||||
|
filename: string,
|
||||||
|
maxLength: number = MAX_FILENAME_LENGTH,
|
||||||
|
): string => {
|
||||||
|
// Input validation
|
||||||
|
if (!filename) {
|
||||||
|
return DEFAULT_FALLBACK_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseName = filename
|
||||||
|
.trim()
|
||||||
|
.replace(INVALID_CHARS_REGEX, '_')
|
||||||
|
.replace(ZERO_WIDTH_CHARS_REGEX, '')
|
||||||
|
.replace(UNICODE_SPACES_REGEX, ' ')
|
||||||
|
.replace(LEADING_TRAILING_DOTS_SPACES_REGEX, '');
|
||||||
|
|
||||||
|
// Handle empty or invalid filenames after cleaning
|
||||||
|
if (!baseName) {
|
||||||
|
baseName = DEFAULT_FALLBACK_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Windows reserved names
|
||||||
|
if (WINDOWS_RESERVED_NAMES.has(baseName.toUpperCase())) {
|
||||||
|
baseName = `_${baseName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate if too long
|
||||||
|
if (baseName.length > maxLength) {
|
||||||
|
baseName = baseName.slice(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseName;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user