118 lines
3.0 KiB
JavaScript
118 lines
3.0 KiB
JavaScript
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)
|
|
}
|