Support uploading FIT files (and zipped exports) alongside GPX
Maptool::Fit reads Garmin's binary FIT format directly rather than adding a gem — the one Ruby option depends on activesupport, unmaintained since 2022, for a single-purpose binary parser. Only "record" messages are read (position + altitude; everything else is skipped by byte length, never interpreted); field numbers and scale/offset are Garmin's own (fit-python-sdk's profile.py), and the framing plus all extracted values were checked byte-for-byte against 5 real device files from python-fitparse's test suite before this shipped. A .zip upload (Garmin's bulk/FIT export format) is expanded into its .gpx/.fit entries via rubyzip — that one's an actual dependency, since ZIP's own format isn't the kind of narrow, single-purpose parsing FIT above is.
This commit is contained in:
1 parent
8d3c126a14
commit
fc174d4267
8 files changed
+338
-29
No files matched your search
@@ -2,7 +2,10 @@ require "sinatra/base"
|
||||
require "sinatra/json"
|
||||
require "oauth2"
|
||||
require "securerandom"
|
||||
require "zip"
|
||||
require "stringio"
|
||||
require_relative "lib/maptool/gpx"
|
||||
require_relative "lib/maptool/fit"
|
||||
require_relative "lib/maptool/gpx_store"
|
||||
require_relative "lib/maptool/gpsd_client"
|
||||
require_relative "lib/maptool/livetrack_store"
|
||||
@@ -69,6 +72,18 @@ module Maptool
|
||||
def callback_url
|
||||
"#{request.base_url}/auth/callback"
|
||||
end
|
||||
|
||||
def save_zip_entries(folder:, io:)
|
||||
paths = []
|
||||
Zip::File.open_buffer(io) do |zip|
|
||||
zip.each do |entry|
|
||||
next unless entry.file? && GpxStore::TRACK_EXTENSIONS.any? { |ext| entry.name.downcase.end_with?(ext) }
|
||||
|
||||
paths << GPX_STORE.save(folder: folder, filename: File.basename(entry.name), io: StringIO.new(entry.get_input_stream.read))
|
||||
end
|
||||
end
|
||||
paths
|
||||
end
|
||||
end
|
||||
|
||||
get "/login" do
|
||||
@@ -149,18 +164,27 @@ module Maptool
|
||||
json GPX_STORE.tree
|
||||
end
|
||||
|
||||
# A .zip (Garmin's bulk/FIT export format) is expanded into its
|
||||
# .gpx/.fit entries instead of being stored itself — nothing downstream
|
||||
# (tree listing, map rendering) knows what to do with a zip.
|
||||
post "/api/gpx" do
|
||||
require_login!
|
||||
file = params[:file]
|
||||
halt 400, json(error: "file is required") unless file && file[:tempfile]
|
||||
|
||||
begin
|
||||
path = GPX_STORE.save(folder: params[:folder].to_s, filename: file[:filename], io: file[:tempfile])
|
||||
paths = if file[:filename].to_s.downcase.end_with?(".zip")
|
||||
save_zip_entries(folder: params[:folder].to_s, io: file[:tempfile])
|
||||
else
|
||||
[GPX_STORE.save(folder: params[:folder].to_s, filename: file[:filename], io: file[:tempfile])]
|
||||
end
|
||||
rescue GpxStore::InvalidPath
|
||||
halt 400, json(error: "invalid folder")
|
||||
rescue Zip::Error
|
||||
halt 400, json(error: "invalid zip file")
|
||||
end
|
||||
|
||||
json path: path, tree: GPX_STORE.tree
|
||||
json paths: paths, tree: GPX_STORE.tree
|
||||
end
|
||||
|
||||
get "/api/gpx/file/*" do
|
||||
@@ -172,9 +196,10 @@ module Maptool
|
||||
halt 404 unless File.file?(path)
|
||||
|
||||
begin
|
||||
json Gpx.to_geojson(path)
|
||||
rescue Gpx::ParseError
|
||||
halt 422, json(error: "could not parse GPX file")
|
||||
geojson = path.downcase.end_with?(".fit") ? Fit.to_geojson(path) : Gpx.to_geojson(path)
|
||||
json geojson
|
||||
rescue Gpx::ParseError, Fit::ParseError
|
||||
halt 422, json(error: "could not parse file")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in new issue
Block a user