aboutsummaryrefslogtreecommitdiff
path: root/mountain_pass/fetch.go
diff options
context:
space:
mode:
Diffstat (limited to 'mountain_pass/fetch.go')
-rw-r--r--mountain_pass/fetch.go83
1 files changed, 76 insertions, 7 deletions
diff --git a/mountain_pass/fetch.go b/mountain_pass/fetch.go
index 4be2d87..94b0b5e 100644
--- a/mountain_pass/fetch.go
+++ b/mountain_pass/fetch.go
@@ -1,12 +1,15 @@
package mountain_pass
import (
+ "crypto/md5"
+ "encoding/hex"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
+ "strings"
"github.com/schollz/progressbar/v3"
)
@@ -14,12 +17,19 @@ import (
const francePBFRUL = "https://download.geofabrik.de/europe/france-latest.osm.pbf"
// FetchFrancePBF downloads the full France OSM PBF to destPath, resuming a
-// partial download when the file already exists and skipping the download
-// entirely when it is already complete.
+// partial download when the file already exists, skipping the download
+// entirely when it is already complete, and verifying the result against
+// geofabrik's published MD5 checksum.
func FetchFrancePBF(destPath string) error {
- client := &http.Client{}
+ return fetchFrancePBF(&http.Client{}, francePBFRUL, destPath)
+}
+
+// fetchFrancePBF is the testable core of FetchFrancePBF. baseURL is the
+// server URL whose .md5 sibling carries the published checksum.
+func fetchFrancePBF(client *http.Client, baseURL, destPath string) error {
+ checksumURL := baseURL + ".md5"
- head, err := client.Head(francePBFRUL)
+ head, err := client.Head(baseURL)
if err != nil {
return fmt.Errorf("failed to check remote size: %w", err)
}
@@ -35,7 +45,7 @@ func FetchFrancePBF(destPath string) error {
offset = info.Size()
if offset >= total {
slog.Info("Already downloaded", "file", destPath, "bytes", offset)
- return nil
+ return verifyPBF(client, checksumURL, destPath)
}
if offset > 0 {
slog.Info("Resuming download", "file", destPath, "bytes", offset, "remaining", total-offset)
@@ -44,7 +54,7 @@ func FetchFrancePBF(destPath string) error {
return fmt.Errorf("failed to stat %s: %w", destPath, statErr)
}
- req, err := http.NewRequest(http.MethodGet, francePBFRUL, nil)
+ req, err := http.NewRequest(http.MethodGet, baseURL, nil)
if err != nil {
return err
}
@@ -89,10 +99,69 @@ func FetchFrancePBF(destPath string) error {
}
_, err = io.Copy(io.MultiWriter(file, bar), res.Body)
if err != nil {
- return fmt.Errorf("failed to download %s: %w", francePBFRUL, err)
+ return fmt.Errorf("failed to download %s: %w", baseURL, err)
}
if size, err := file.Stat(); err == nil && size.Size() != total {
return fmt.Errorf("download incomplete: got %d bytes, want %d", size.Size(), total)
}
+ return verifyPBF(client, checksumURL, destPath)
+}
+
+// verifyPBF verifies destPath against the published checksum served at
+// checksumURL. If the checksum endpoint is unreachable it logs a warning and
+// proceeds unverified rather than blocking the download; on mismatch it
+// returns an error without deleting the corrupt file so the user can inspect
+// it and remove it manually before re-fetching.
+func verifyPBF(client *http.Client, checksumURL, destPath string) error {
+ want, err := expectedPBFMD5(client, checksumURL)
+ if err != nil {
+ slog.Warn("Could not fetch expected checksum, skipping verification", "url", checksumURL, "err", err)
+ return nil
+ }
+ got, err := pbfMD5(destPath)
+ if err != nil {
+ return fmt.Errorf("failed to compute MD5 of %s: %w", destPath, err)
+ }
+ if !strings.EqualFold(got, want) {
+ return fmt.Errorf("checksum mismatch on %s: local md5 %s != expected %s; keeping the corrupt file for inspection, remove it manually before re-fetching",
+ destPath, got, want)
+ }
+ slog.Info("Checksum verified", "file", destPath, "md5", got)
return nil
}
+
+// expectedPBFMD5 fetches checksumURL and returns its first whitespace-separated
+// token. Geofabrik's .md5 files follow the "<hex> <filename>" format.
+func expectedPBFMD5(client *http.Client, checksumURL string) (string, error) {
+ res, err := client.Get(checksumURL)
+ if err != nil {
+ return "", err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("unexpected status code: %d", res.StatusCode)
+ }
+ body, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
+ if err != nil {
+ return "", err
+ }
+ fields := strings.Fields(string(body))
+ if len(fields) == 0 {
+ return "", errors.New("empty checksum file")
+ }
+ return fields[0], nil
+}
+
+// pbfMD5 returns the lowercase hex MD5 digest of the file at path.
+func pbfMD5(path string) (string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+ h := md5.New()
+ if _, err := io.Copy(h, f); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(h.Sum(nil)), nil
+}