Skip to content

Instantly share code, notes, and snippets.

@marcin-gryszkalis
Created July 25, 2026 11:05
Show Gist options
  • Select an option

  • Save marcin-gryszkalis/4023b18c3d3bf81a093554de3555ae47 to your computer and use it in GitHub Desktop.

Select an option

Save marcin-gryszkalis/4023b18c3d3bf81a093554de3555ae47 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Build a JSON mapping {id: md5_hash} where the hash is computed from each
file's mtime, reconstructed as the original ISO-8601 millisecond timestamp
string (e.g. "2021-12-04T13:00:54.074Z") and then MD5-hashed.
Assumes filenames end with a numeric id immediately before the .gpx extension,
e.g. "track_1638622854123.gpx" or "1638622854123.gpx".
"""
import os
import re
import json
import hashlib
import argparse
from datetime import datetime, timezone, timedelta
ID_RE = re.compile(r'(\d+)\.gpx$', re.IGNORECASE)
def mtime_to_timestamp_str(mtime: float) -> str:
"""Convert epoch seconds -> 'YYYY-MM-DDTHH:MM:SS.mmmZ' (UTC, ms precision)."""
dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
ms = round(dt.microsecond / 1000)
if ms == 1000:
dt = dt.replace(microsecond=0) + timedelta(seconds=1)
ms = 0
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{ms:03d}Z"
def hash_from_mtime(mtime: float) -> tuple[str, str]:
ts = mtime_to_timestamp_str(mtime)
return ts, hashlib.md5(ts.encode()).hexdigest()
def build_mapping(directory: str, verbose: bool = False) -> dict:
mapping = {}
skipped = []
for entry in sorted(os.scandir(directory), key=lambda e: e.name):
if not entry.is_file():
continue
match = ID_RE.search(entry.name)
if not match:
skipped.append(entry.name)
continue
file_id = match.group(1)
st = entry.stat()
# Sanity check: warn if sub-second info looks like it was lost
sub_second_ns = st.st_mtime_ns % 1_000_000_000
if sub_second_ns == 0 and verbose:
print(f" warning: {entry.name} has zero sub-second mtime "
f"(nanosecond part is 0) - hash may not match original")
ts, h = hash_from_mtime(st.st_mtime)
if file_id in mapping:
print(f" warning: duplicate id {file_id} "
f"(file {entry.name}) - overwriting previous entry")
mapping[file_id] = h
if verbose:
print(f"{entry.name} -> id={file_id} mtime_ts={ts} hash={h}")
if skipped:
print(f"\nSkipped {len(skipped)} file(s) with no matching id pattern:")
for name in skipped:
print(f" {name}")
return mapping
def main():
parser = argparse.ArgumentParser(
description="Map file id -> md5(mtime timestamp) for .gpx files."
)
parser.add_argument("directory", help="Directory containing the .gpx files")
parser.add_argument("-o", "--output", default="id_hash_map.json",
help="Output JSON file (default: id_hash_map.json)")
parser.add_argument("-v", "--verbose", action="store_true",
help="Print per-file details")
args = parser.parse_args()
if not os.path.isdir(args.directory):
raise SystemExit(f"Not a directory: {args.directory}")
mapping = build_mapping(args.directory, verbose=args.verbose)
with open(args.output, "w") as f:
json.dump(mapping, f, indent=2, sort_keys=True)
print(f"\nWrote {len(mapping)} entries to {args.output}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment