1
0

file.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. const {
  2. getWxApi,
  3. isCancelError
  4. } = require('../utils/base-utils.js')
  5. const {
  6. formatBytes
  7. } = require('../utils/binary-utils.js')
  8. function formatExportStamp(date = new Date()) {
  9. const pad = (value, length = 2) => String(value).padStart(length, '0')
  10. return [
  11. date.getFullYear(),
  12. pad(date.getMonth() + 1),
  13. pad(date.getDate()),
  14. '-',
  15. pad(date.getHours()),
  16. pad(date.getMinutes()),
  17. pad(date.getSeconds())
  18. ].join('')
  19. }
  20. function normalizeExtensions(extensions = []) {
  21. return extensions
  22. .map((extension) => String(extension || '').trim().replace(/^\./, '').toLowerCase())
  23. .filter(Boolean)
  24. }
  25. function escapeRegExp(text) {
  26. return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  27. }
  28. function getRuntimeInfo() {
  29. const wxApi = getWxApi()
  30. try {
  31. if (typeof wxApi.getDeviceInfo === 'function') return wxApi.getDeviceInfo() || {}
  32. } catch (error) {}
  33. try {
  34. if (typeof wxApi.getSystemInfoSync === 'function') return wxApi.getSystemInfoSync() || {}
  35. } catch (error) {}
  36. return {}
  37. }
  38. function getRuntimePlatform() {
  39. const info = getRuntimeInfo()
  40. return String(info.platform || info.system || '').toLowerCase()
  41. }
  42. function isPcRuntime() {
  43. const platform = getRuntimePlatform()
  44. return /windows|mac|devtools/.test(platform)
  45. }
  46. function getPathFileName(filePath) {
  47. const text = String(filePath || '').split(/[\\/]/).pop() || ''
  48. try {
  49. return decodeURIComponent(text)
  50. } catch (error) {
  51. return text
  52. }
  53. }
  54. function getFileName(file, fallback = '未命名文件') {
  55. const name = file && (file.name || file.fileName)
  56. if (name) return String(name)
  57. return getPathFileName(getFilePath(file)) || fallback
  58. }
  59. function getFilePath(file) {
  60. return file && (file.path || file.tempFilePath || file.filePath)
  61. ? (file.path || file.tempFilePath || file.filePath)
  62. : ''
  63. }
  64. function getFileSize(file) {
  65. const size = Number(file && file.size)
  66. return Number.isFinite(size) && size >= 0 ? size : 0
  67. }
  68. function normalizeSelectedFiles(result, fallbackName = '未命名文件') {
  69. const sourceFiles = Array.isArray(result && result.tempFiles)
  70. ? result.tempFiles
  71. : []
  72. const pathFiles = Array.isArray(result && result.tempFilePaths)
  73. ? result.tempFilePaths.map((filePath) => ({ path: filePath }))
  74. : []
  75. const directFile = result && (result.path || result.tempFilePath || result.filePath)
  76. ? [result]
  77. : []
  78. return sourceFiles.concat(pathFiles, directFile)
  79. .map((file, index) => {
  80. const path = getFilePath(file)
  81. return {
  82. file,
  83. name: getFileName(file, index === 0 ? fallbackName : `${fallbackName}-${index + 1}`),
  84. path,
  85. size: getFileSize(file)
  86. }
  87. })
  88. .filter((file) => !!file.path)
  89. }
  90. function getFirstSelectedFile(result, fallbackName = '未命名文件') {
  91. const files = normalizeSelectedFiles(result, fallbackName)
  92. const fileInfo = files[0]
  93. if (!fileInfo) throw new Error('没有选择文件')
  94. return fileInfo
  95. }
  96. function assertFileExtension(fileInfo, extensions = [], message = '文件格式不符') {
  97. const normalizedExtensions = normalizeExtensions(extensions)
  98. if (!normalizedExtensions.length) return
  99. const nameText = `${fileInfo && fileInfo.name ? fileInfo.name : ''} ${fileInfo && fileInfo.path ? fileInfo.path : ''}`
  100. const matched = normalizedExtensions.some((extension) => (
  101. new RegExp(`\\.${escapeRegExp(extension)}$`, 'i').test(nameText)
  102. ))
  103. if (!matched) throw new Error(message)
  104. }
  105. function chooseMessageFile(options = {}) {
  106. const wxApi = getWxApi()
  107. const extensions = normalizeExtensions(options.extensions || options.extension || [])
  108. const type = extensions.length ? 'file' : (options.type || 'all')
  109. return new Promise((resolve, reject) => {
  110. if (typeof wxApi.chooseMessageFile !== 'function') {
  111. reject(new Error('当前微信版本不支持从聊天记录选择文件,请升级微信或将文件转发到文件传输助手后重试'))
  112. return
  113. }
  114. const chooseOptions = {
  115. count: options.count || 1,
  116. type,
  117. success: resolve,
  118. fail: reject
  119. }
  120. if (extensions.length) chooseOptions.extension = extensions
  121. wxApi.chooseMessageFile(chooseOptions)
  122. })
  123. }
  124. function chooseLocalFile(options = {}) {
  125. const wxApi = getWxApi()
  126. const extensions = normalizeExtensions(options.extensions || options.extension || [])
  127. const type = extensions.length ? 'file' : (options.type || 'all')
  128. return new Promise((resolve, reject) => {
  129. if (typeof wxApi.chooseFile !== 'function') {
  130. reject(new Error(options.unsupportedMessage || '当前微信环境不支持本地文件选择,请将文件发送到文件传输助手后从聊天记录选择'))
  131. return
  132. }
  133. const chooseOptions = {
  134. count: options.count || 1,
  135. type,
  136. success: resolve,
  137. fail: reject
  138. }
  139. if (extensions.length) chooseOptions.extension = extensions
  140. wxApi.chooseFile(chooseOptions)
  141. })
  142. }
  143. async function chooseFile(source = 'message', options = {}) {
  144. const normalizedSource = source === 'local' || source === 'auto' ? source : 'message'
  145. // PC 微信更接近本地文件选择,移动端更稳定的是从聊天记录取文件。
  146. const preferLocal = normalizedSource === 'local' || (normalizedSource === 'auto' && isPcRuntime())
  147. const firstChooser = preferLocal ? chooseLocalFile : chooseMessageFile
  148. const fallbackChooser = preferLocal ? chooseMessageFile : chooseLocalFile
  149. try {
  150. return await firstChooser(options)
  151. } catch (error) {
  152. if (isCancelError(error) || options.fallback === false) throw error
  153. if (normalizedSource !== 'auto' && options.fallbackToOtherSource === false) throw error
  154. try {
  155. return await fallbackChooser(options)
  156. } catch (fallbackError) {
  157. if (isCancelError(fallbackError)) throw fallbackError
  158. const firstMessage = error && (error.errMsg || error.message || error)
  159. const fallbackMessage = fallbackError && (fallbackError.errMsg || fallbackError.message || fallbackError)
  160. throw new Error([
  161. firstMessage || '文件选择失败',
  162. fallbackMessage || ''
  163. ].filter(Boolean).join(';'))
  164. }
  165. }
  166. }
  167. function readFile(filePath, options = {}) {
  168. const wxApi = getWxApi()
  169. return new Promise((resolve, reject) => {
  170. if (typeof wxApi.getFileSystemManager !== 'function') {
  171. reject(new Error('当前微信版本不支持读取文件'))
  172. return
  173. }
  174. const fs = wxApi.getFileSystemManager()
  175. const readOptions = {
  176. filePath,
  177. success: (res) => resolve(res.data),
  178. fail: reject
  179. }
  180. if (options.encoding) readOptions.encoding = options.encoding
  181. fs.readFile(readOptions)
  182. })
  183. }
  184. function toUint8Array(data) {
  185. if (data instanceof Uint8Array) return data
  186. if (data instanceof ArrayBuffer) return new Uint8Array(data)
  187. if (ArrayBuffer.isView(data)) {
  188. return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
  189. }
  190. return new Uint8Array(data || new ArrayBuffer(0))
  191. }
  192. async function loadSelectedFile(source = 'message', options = {}) {
  193. const result = await chooseFile(source, options)
  194. const fileInfo = getFirstSelectedFile(result, options.fallbackName || '未命名文件')
  195. assertFileExtension(fileInfo, options.extensions || options.extension || [], options.extensionMessage || '文件格式不符')
  196. const data = await readFile(fileInfo.path, {
  197. encoding: options.encoding
  198. })
  199. const bytes = options.encoding ? null : toUint8Array(data)
  200. return {
  201. ...fileInfo,
  202. bytes,
  203. data,
  204. size: bytes ? bytes.length : (fileInfo.size || String(data || '').length),
  205. sizeText: formatBytes(bytes ? bytes.length : (fileInfo.size || String(data || '').length)),
  206. text: options.encoding ? String(data || '') : ''
  207. }
  208. }
  209. function getUserDataFilePath(fileName) {
  210. const wxApi = getWxApi()
  211. const userDataPath = wxApi.env && wxApi.env.USER_DATA_PATH
  212. if (!userDataPath) throw new Error('当前微信版本不支持生成文件')
  213. return `${userDataPath}/${fileName}`
  214. }
  215. function writeTextFile(filePath, data, encoding = 'utf8') {
  216. const wxApi = getWxApi()
  217. return new Promise((resolve, reject) => {
  218. if (typeof wxApi.getFileSystemManager !== 'function') {
  219. reject(new Error('当前微信版本不支持生成文件'))
  220. return
  221. }
  222. const fs = wxApi.getFileSystemManager()
  223. if (typeof fs.writeFileSync === 'function') {
  224. try {
  225. fs.writeFileSync(filePath, data, encoding)
  226. resolve(filePath)
  227. } catch (error) {
  228. reject(error)
  229. }
  230. return
  231. }
  232. if (typeof fs.writeFile !== 'function') {
  233. reject(new Error('当前微信版本不支持生成文件'))
  234. return
  235. }
  236. fs.writeFile({
  237. data,
  238. encoding,
  239. filePath,
  240. fail: reject,
  241. success: () => resolve(filePath)
  242. })
  243. })
  244. }
  245. function saveFileToDisk(filePath) {
  246. const wxApi = getWxApi()
  247. return new Promise((resolve, reject) => {
  248. if (typeof wxApi.saveFileToDisk !== 'function') {
  249. reject(new Error('当前微信环境不支持保存到电脑磁盘'))
  250. return
  251. }
  252. wxApi.saveFileToDisk({
  253. filePath,
  254. fail: reject,
  255. success: resolve
  256. })
  257. })
  258. }
  259. function shareFileToChat(filePath, fileName) {
  260. const wxApi = getWxApi()
  261. return new Promise((resolve, reject) => {
  262. if (typeof wxApi.shareFileMessage !== 'function') {
  263. reject(new Error('当前微信版本不支持发送文件到聊天'))
  264. return
  265. }
  266. wxApi.shareFileMessage({
  267. fileName,
  268. filePath,
  269. success: resolve,
  270. fail: reject
  271. })
  272. })
  273. }
  274. async function exportFile(filePath, fileName, options = {}) {
  275. const wxApi = getWxApi()
  276. const preferDisk = options.target === 'disk'
  277. || (options.target !== 'chat' && isPcRuntime() && typeof wxApi.saveFileToDisk === 'function')
  278. if (preferDisk && typeof wxApi.saveFileToDisk === 'function') {
  279. try {
  280. await saveFileToDisk(filePath)
  281. return {
  282. method: 'disk'
  283. }
  284. } catch (error) {
  285. if (isCancelError(error) || typeof wxApi.shareFileMessage !== 'function') throw error
  286. }
  287. }
  288. if (typeof wxApi.shareFileMessage === 'function') {
  289. await shareFileToChat(filePath, fileName)
  290. return {
  291. method: 'chat'
  292. }
  293. }
  294. if (typeof wxApi.saveFileToDisk === 'function') {
  295. await saveFileToDisk(filePath)
  296. return {
  297. method: 'disk'
  298. }
  299. }
  300. throw new Error('当前微信环境不支持导出文件,请升级微信后重试')
  301. }
  302. async function saveTextFileToChat(fileName, data) {
  303. const filePath = getUserDataFilePath(fileName)
  304. await writeTextFile(filePath, data, 'utf8')
  305. const result = await exportFile(filePath, fileName)
  306. return {
  307. filePath,
  308. ...result
  309. }
  310. }
  311. module.exports = {
  312. assertFileExtension,
  313. chooseFile,
  314. chooseLocalFile,
  315. chooseMessageFile,
  316. exportFile,
  317. formatBytes,
  318. formatExportStamp,
  319. getFirstSelectedFile,
  320. getRuntimePlatform,
  321. getUserDataFilePath,
  322. isCancelError,
  323. isPcRuntime,
  324. loadSelectedFile,
  325. normalizeSelectedFiles,
  326. readFile,
  327. saveFileToDisk,
  328. saveTextFileToChat,
  329. shareFileToChat,
  330. toUint8Array,
  331. writeTextFile
  332. }