Skip to content

Instantly share code, notes, and snippets.

@alissonbrunosa
Last active December 3, 2020 11:09
Show Gist options
  • Save alissonbrunosa/853d99f86854bddfc2a2bf51ad7a95f6 to your computer and use it in GitHub Desktop.
Save alissonbrunosa/853d99f86854bddfc2a2bf51ad7a95f6 to your computer and use it in GitHub Desktop.
A small program to set your timezone based on your IP
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
var httpClient = &http.Client{
Timeout: 30 * time.Second,
Transport: http.DefaultTransport,
}
type Services []string
var services = Services{
"https://ipapi.co/json",
"http://ip-api.com/json",
"https://freegeoip.app/json/",
"http://worldtimeapi.org/api/ip",
}
const (
DefaulLocalTimePath = "/etc/localtime"
DefaultZoneInfoPath = "/usr/share/zoneinfo"
)
type response struct {
Timezone string `json:"timezone"`
}
func main() {
ctx := context.Background()
resp, err := getTimezone(ctx, services)
if err != nil {
fmt.Printf("Could not retrieve any timezone from the services: %v\n", err)
os.Exit(1)
}
if err := setTimezone(resp.Timezone); err != nil {
fmt.Fprintln(os.Stderr, err)
if errors.Is(err, os.ErrPermission) {
fmt.Fprintln(os.Stderr, "Are you root?")
}
os.Exit(1)
}
fmt.Printf("Timezone updated to %s\n", resp.Timezone)
os.Exit(0)
}
func getTimezone(ctx context.Context, pool Services) (*response, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var (
resp *response
lastErr error
mu sync.Mutex
)
for _, service := range pool {
go func(s string) {
r, err := lookup(ctx, s)
switch err {
case nil:
cancel()
mu.Lock()
defer mu.Unlock()
resp = r
default:
mu.Lock()
defer mu.Unlock()
lastErr = err
}
}(service)
}
<-ctx.Done()
if err := ctx.Err(); err == context.DeadlineExceeded {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("cannot reach any service: %w", lastErr)
}
return resp, nil
}
func setTimezone(timezone string) error {
tzPath := filepath.Join(DefaultZoneInfoPath, timezone)
if _, err := os.Stat(tzPath); os.IsNotExist(err) {
return fmt.Errorf("Timezone %s not supported", timezone)
} else if err != nil {
return fmt.Errorf("unexpected error: %w", err)
}
if err := os.Remove(DefaulLocalTimePath); err != nil {
return fmt.Errorf("could not remove current timezone: %w", err)
}
if err := os.Symlink(tzPath, DefaulLocalTimePath); err != nil {
return fmt.Errorf("could not create symlink: %w", err)
}
return nil
}
func lookup(ctx context.Context, service string) (*response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, service, nil)
if err != nil {
return nil, fmt.Errorf("could not create a new request: %w", err)
}
res, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer res.Body.Close()
response := new(response)
if err := json.NewDecoder(res.Body).Decode(response); err != nil {
return nil, fmt.Errorf("unable to decode JSON response: %w", err)
}
return response, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment