refactor(editor): Optimize the logic of replacing illegal characters in download workflow naming (#16260)

This commit is contained in:
luka
2025-06-17 23:15:16 +08:00
committed by GitHub
parent 7415125da5
commit c64ccf74a2
3 changed files with 185 additions and 1 deletions

View File

@@ -60,6 +60,7 @@ import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { type BaseTextKey, useI18n } from '@n8n/i18n';
import { ProjectTypes } from '@/types/projects.types';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
import { sanitizeFilename } from '@/utils/fileUtils';
const props = defineProps<{
readOnly?: boolean;
@@ -466,7 +467,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
});
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 });
saveAs(blob, name + '.json');

View 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('프로세스_процесс');
});
});

View 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;
};