diff options
Diffstat (limited to 'mountain_pass/downloader.go')
| -rw-r--r-- | mountain_pass/downloader.go | 222 |
1 files changed, 0 insertions, 222 deletions
diff --git a/mountain_pass/downloader.go b/mountain_pass/downloader.go deleted file mode 100644 index 00ff586..0000000 --- a/mountain_pass/downloader.go +++ /dev/null @@ -1,222 +0,0 @@ -package mountain_pass - -import ( - "bytes" - "database/sql" - "encoding/csv" - "errors" - "fmt" - "io" - "log/slog" - "net/http" - "os" - "strconv" - "strings" - "time" - - "github.com/jftuga/geodist" - "github.com/martinlehoux/kagamigo/kcore" - "github.com/schollz/progressbar/v3" -) - -type MountainPass struct { - Name string - ExternalID string - CountryCode string - DepartmentCode string - Elevation int - Coord *geodist.Coord -} - -func parseMountainPasses(reader io.Reader) ([]MountainPass, error) { - mountainPasses := make([]MountainPass, 0) - csvReader := csv.NewReader(reader) - csvReader.Comma = '\t' - csvReader.LazyQuotes = true - rows, err := csvReader.ReadAll() - if err != nil { - return nil, kcore.Wrap(err, "Failed to read mountain pass CSV data") - } - rows = rows[1:] - for _, row := range rows { - if len(row) < 4 { - return nil, fmt.Errorf("unexpected row with %d fields: %v", len(row), row) - } - countryCode, departmentCode, err := splitCountryDepartment(row[0]) - if err != nil { - return nil, err - } - codes := strings.Split(row[1], " | ") - names := strings.Split(row[2], " | ") - altitudes := strings.Split(row[3], " | ") - if len(codes) != len(names) || len(codes) != len(altitudes) { - return nil, fmt.Errorf("inconsistent multi-pass row: %v", row) - } - for i := range codes { - elevation, err := strconv.Atoi(altitudes[i]) - if err != nil { - return nil, fmt.Errorf("failed to parse elevation %q: %w", altitudes[i], err) - } - mountainPasses = append(mountainPasses, MountainPass{ - ExternalID: "centcols/" + codes[i], - CountryCode: countryCode, - Name: names[i], - DepartmentCode: departmentCode, - Elevation: elevation, - }) - } - } - - return mountainPasses, nil -} - -func splitCountryDepartment(brevet string) (string, string, error) { - parts := strings.Split(brevet, "-") - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", fmt.Errorf("unexpected brevet code: %q", brevet) - } - return parts[0], parts[1], nil -} - -func getCSVData(departmentCode string) ([]byte, error) { - url := "https://www.centcols.org/membres/cols/csvbrevet.php?id=FR" + departmentCode + "&enc=UTF-8" - res, err := http.Get(url) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, statusError{statusCode: res.StatusCode} - } - return io.ReadAll(res.Body) -} - -type statusError struct { - statusCode int -} - -func (e statusError) Error() string { - return fmt.Sprintf("unexpected status code: %d", e.statusCode) -} - -const downloadRetries = 3 - -func departmentCacheFile(departmentCode string) string { - return fmt.Sprintf("debug_department_%s.csv", departmentCode) -} - -func loadCachedDepartment(filename string) ([]MountainPass, bool, error) { - data, err := os.ReadFile(filename) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, false, nil - } - return nil, false, err - } - mountainPasses, err := parseMountainPasses(bytes.NewReader(data)) - if err != nil { - return nil, false, nil - } - return mountainPasses, true, nil -} - -func downloadDepartment(departmentCode string, resume bool) ([]MountainPass, error) { - filename := departmentCacheFile(departmentCode) - if resume { - mountainPasses, found, err := loadCachedDepartment(filename) - if err != nil { - return nil, err - } - if found { - return mountainPasses, nil - } - } - var lastErr error - backoff := time.Second - for attempt := 1; attempt <= downloadRetries; attempt++ { - data, err := getCSVData(departmentCode) - if err != nil { - lastErr = err - var statusErr statusError - if errors.As(err, &statusErr) && statusErr.statusCode == http.StatusTooManyRequests { - backoff = time.Minute - } - slog.Warn("Retrying department download", "department", departmentCode, "attempt", attempt, "error", err) - time.Sleep(backoff) - backoff *= 2 - continue - } - mountainPasses, err := parseMountainPasses(bytes.NewReader(data)) - if err != nil { - if writeErr := os.WriteFile(filename, data, 0o644); writeErr != nil { - return nil, kcore.Wrap(writeErr, "Failed to write dump file") - } - return nil, err - } - if writeErr := os.WriteFile(filename, data, 0o644); writeErr != nil { - return nil, kcore.Wrap(writeErr, "Failed to write dump file") - } - return mountainPasses, nil - } - return nil, lastErr -} - -func upsertMountainPasses(db *sql.DB, mountainPasses []MountainPass) error { - tx, err := db.Begin() - kcore.Expect(err, "Failed to begin transaction") - defer tx.Rollback() - stmt, err := tx.Prepare(` - INSERT INTO mountain_passes (external_id, name, country_code, department_code, elevation) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(external_id) DO UPDATE SET - name = excluded.name, - country_code = excluded.country_code, - department_code = excluded.department_code, - elevation = excluded.elevation - `) - kcore.Expect(err, "Failed to prepare statement") - defer stmt.Close() - - for _, mountainPass := range mountainPasses { - _, err := stmt.Exec(mountainPass.ExternalID, mountainPass.Name, mountainPass.CountryCode, mountainPass.DepartmentCode, mountainPass.Elevation) - kcore.Expect(err, "Failed to execute statement") - } - - return tx.Commit() -} - -func DownloadMountainPasses(db *sql.DB, delay time.Duration, resume bool) error { - mountainPasses := make([]MountainPass, 0) - bar := progressbar.Default(90) - ticker := time.NewTicker(delay) - for i := 1; i <= 90; i++ { - <-ticker.C - bar.Add(1) - departmentCode := fmt.Sprintf("%02d", i) - bar.Describe(fmt.Sprintf("Downloading mountain passes for department_code=%s", departmentCode)) - departmentMountainPasses, err := downloadDepartment(departmentCode, resume) - if err != nil { - return kcore.Wrap(err, "Failed to download mountain passes for department "+departmentCode) - } - mountainPasses = append(mountainPasses, departmentMountainPasses...) - } - - return upsertMountainPasses(db, mountainPasses) -} - -func ImportCachedDepartments(db *sql.DB, departmentCodes []string) (int, error) { - mountainPasses := make([]MountainPass, 0) - for _, departmentCode := range departmentCodes { - departmentMountainPasses, found, err := loadCachedDepartment(departmentCacheFile(departmentCode)) - if err != nil { - return 0, err - } - if !found { - slog.Warn("No cached department file", "department", departmentCode) - continue - } - slog.Info("Loaded cached department", "department", departmentCode, "passes", len(departmentMountainPasses)) - mountainPasses = append(mountainPasses, departmentMountainPasses...) - } - return len(mountainPasses), upsertMountainPasses(db, mountainPasses) -} |
