Created
July 25, 2026 11:05
-
-
Save marcin-gryszkalis/4023b18c3d3bf81a093554de3555ae47 to your computer and use it in GitHub Desktop.
create hashes for https://github.com/timschneeb/KomootGPX/pull/37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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