const DEFAULT_FILE_NAME = 'data' const DEFAULT_MIME_TYPE = 'application/octet-stream' const encoder = new TextEncoder() const decoder = new TextDecoder() const normalizeSuffix = (suffix) => { const value = String(suffix || '').trim() if (!value) throw new Error('INVALID_SUFFIX') return value.startsWith('.') ? value : `.${value}` } const sanitizeFileNamePart = (value) => { const cleaned = String(value || '') .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .trim() return cleaned || DEFAULT_FILE_NAME } const formatTimestamp = (date = new Date()) => { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hour = String(date.getHours()).padStart(2, '0') const minute = String(date.getMinutes()).padStart(2, '0') const second = String(date.getSeconds()).padStart(2, '0') return `${year}${month}${day}-${hour}${minute}${second}` } const encodeData = (data) => { const json = JSON.stringify(data) return encoder.encode(json) } const decodeData = (bytes) => { const text = decoder.decode(bytes) return JSON.parse(text) } const downloadBlob = (blob, fileName) => { const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = fileName document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } const pickFile = (accept) => { return new Promise((resolve, reject) => { const input = document.createElement('input') input.type = 'file' input.accept = accept input.style.display = 'none' const cleanup = () => { input.removeEventListener('change', handleChange) input.remove() } const handleChange = () => { const file = input.files?.[0] || null cleanup() if (!file) { reject(new Error('FILE_NOT_SELECTED')) return } resolve(file) } input.addEventListener('change', handleChange, { once: true }) document.body.appendChild(input) input.click() }) } export const exportData = async (data, suffix, options = {}) => { const normalizedSuffix = normalizeSuffix(suffix) const bytes = encodeData(data) const blob = new Blob([bytes], { type: options.mimeType || DEFAULT_MIME_TYPE }) const baseName = sanitizeFileNamePart(options.fileName) const fileName = `${baseName}-${formatTimestamp()}${normalizedSuffix}` const shouldDownload = options.download !== false if (shouldDownload) { downloadBlob(blob, fileName) } return { blob, fileName, bytes } } export const importData = async (suffix) => { const normalizedSuffix = normalizeSuffix(suffix) const file = await pickFile(normalizedSuffix) const fileName = String(file.name || '') if (!fileName.toLowerCase().endsWith(normalizedSuffix.toLowerCase())) { throw new Error('INVALID_FILE_SUFFIX') } const bytes = new Uint8Array(await file.arrayBuffer()) return decodeData(bytes) }