From 6d95ed9865fee5fd9956fbef6b0fc702b6a95e71 Mon Sep 17 00:00:00 2001 From: gtkirk Date: Sun, 25 Feb 2024 22:49:58 -0600 Subject: [PATCH] Handle zipped files --- package-lock.json | 11 +++++++ package.json | 1 + src/track.js | 82 ++++++++++++++++------------------------------- src/ui.js | 56 +++++++++++++++++++++++++++++--- 4 files changed, 90 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e074c3..b046cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "exif-js": "^2.3.0", "fast-xml-parser": "^4.3.2", + "fflate": "^0.8.2", "fit-file-parser": "^1.6.18", "leaflet": "^1.3.3", "leaflet-easybutton": "^2.3.0", @@ -943,6 +944,11 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -2359,6 +2365,11 @@ "strnum": "^1.0.5" } }, + "fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", diff --git a/package.json b/package.json index 5541c6d..78f63f9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "exif-js": "^2.3.0", "fast-xml-parser": "^4.3.2", + "fflate": "^0.8.2", "fit-file-parser": "^1.6.18", "leaflet": "^1.3.3", "leaflet-easybutton": "^2.3.0", diff --git a/src/track.js b/src/track.js index 4cf6833..0f6ca88 100644 --- a/src/track.js +++ b/src/track.js @@ -7,14 +7,19 @@ import { XMLParser } from 'fast-xml-parser'; import FitParser from 'fit-file-parser'; -import Pako from 'pako'; +import * as fflate from 'fflate'; -const parser = new XMLParser({ +const xmlParser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', attributesGroupName: '$', }); +const fitParser = new FitParser({ + force: true, + mode: 'list', +}); + function getSport(sport, name) { sport = sport?.toLowerCase(); @@ -190,65 +195,32 @@ function extractFITTracks(fit, name) { return points.length > 0 ? [{timestamp, points, name, sport}] : []; } -function readFile(file, encoding, isGzipped) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => { - const result = e.target.result; - try { - return resolve(isGzipped ? Pako.inflate(result) : result); - } catch (e) { - return reject(e); - } - }; - - if (encoding === 'binary') { - reader.readAsArrayBuffer(file); - } else { - reader.readAsText(file); - } - }); -} - -export default function extractTracks(file) { - const isGzipped = /\.gz$/i.test(file.name); - const strippedName = file.name.replace(/\.gz$/i, ''); - const format = strippedName.split('.').pop().toLowerCase(); - +export default function extractTracks(name, contents) { + const format = name.split('.').pop().toLowerCase(); switch (format) { - case 'gpx': - case 'tcx': /* Handle XML based file formats the same way */ - - return readFile(file, 'text', isGzipped) - .then(textContents => new Promise((resolve, reject) => { - const result = parser.parse(textContents); + case 'gpx': + case 'tcx': + return new Promise((resolve, reject) => { + const result = xmlParser.parse(fflate.strFromU8(contents)); if (result.gpx) { resolve(extractGPXTracks(result.gpx)); } else if (result.TrainingCenterDatabase) { - resolve(extractTCXTracks(result.TrainingCenterDatabase, strippedName)); + resolve(extractTCXTracks(result.TrainingCenterDatabase, name)); } else { reject(new Error('Invalid file type.')); } - })); - - case 'fit': - return readFile(file, 'binary', isGzipped) - .then(contents => new Promise((resolve, reject) => { - const parser = new FitParser({ - force: true, - mode: 'list', - }); - - parser.parse(contents, (err, result) => { - if (err) { - reject(err); - } else { - resolve(extractFITTracks(result, strippedName)); - } - }); - })); - - default: - throw `Unsupported file format: ${format}`; + }); + case 'fit': + return new Promise((resolve, reject) => { + fitParser.parse(contents, (err, result) => { + if (err) { + reject(err); + } else { + resolve(extractFITTracks(result, name)); + } + }); + }); + default: + throw `Unsupported file format: ${format}`; } } diff --git a/src/ui.js b/src/ui.js index 3c7f03f..70bd31c 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,6 +1,7 @@ import picoModal from 'picomodal'; import extractTracks from './track'; import Image from './image'; +import * as fflate from 'fflate'; const AVAILABLE_THEMES = [ 'CartoDB.DarkMatter', @@ -85,8 +86,8 @@ function handleFileSelect(map, evt) { modal.addSuccess(); }; - const handleTrackFile = async (file) => { - for (const track of await extractTracks(file)) { + const handleTrackFile = async (file, contents) => { + for (const track of await extractTracks(file, contents)) { track.filename = file.name; tracks.push(track); map.addTrack(track); @@ -94,12 +95,57 @@ function handleFileSelect(map, evt) { modal.addSuccess(); }; + async function readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const result = e.target.result; + try { + return resolve(result); + } catch (e) { + return reject(e); + } + }; + + reader.readAsArrayBuffer(file); + }); + } + + async function unzip(bytes) { + return new Promise ((resolve, reject) => { + fflate.unzip(new Uint8Array(bytes), + { filter(file) { return !file.name.endsWith('/'); } }, + (err, data) => { + if (err) { reject(err); } + resolve(data); + }); + }); + } + + const handleZipEntry = async entry => + { + return new Promise((resolve) => setTimeout(resolve, 0)) + .then(() => handleTrackFile(...entry)); + }; + + const handleZip = async file => { + return readFile(file) + .then(contents => unzip(contents)) + .then(unzipped => { + modal.addDirectoryEntries(Object.keys(unzipped).length); + return Promise.all(Object.entries(unzipped).map(handleZipEntry)); + }); + } + const handleFile = async file => { try { if (/\.jpe?g$/i.test(file.name)) { return await handleImage(file); } - return await handleTrackFile(file); + if (/\.zip$/i.test(file.name)) { + return await handleZip(file); + } + return await readFile(file).then((contents) => handleTrackFile(file.name, contents)); } catch (err) { console.error(err); modal.addFailure({name: file.name, error: err}); @@ -131,7 +177,7 @@ function handleFileSelect(map, evt) { }); }; - const readFile = async entry => { + const resolveFile = async entry => { return new Promise(resolve => { entry.file(resolve); }); @@ -141,7 +187,7 @@ function handleFileSelect(map, evt) { { if (entry.isFile) { - return await readFile(entry).then(handleFile); + return await resolveFile(entry).then(handleFile); } else if (entry.isDirectory) {