download.go

 1package spellcheck
 2
 3import (
 4	"context"
 5	"fmt"
 6	"io"
 7	"net/http"
 8	"os"
 9	"path/filepath"
10	"strings"
11
12	"github.com/floatpane/matcha/internal/httpclient"
13)
14
15// DefaultLanguage is the language code installed automatically the first
16// time the composer opens.
17const DefaultLanguage = "en"
18
19// DictURLTemplate is the URL used to fetch Hunspell .dic files. It is a
20// variable to allow tests and the CLI to override the source.
21var DictURLTemplate = "https://raw.githubusercontent.com/wooorm/dictionaries/main/dictionaries/%s/index.dic"
22
23// Download fetches the dictionary for lang from DictURLTemplate and writes
24// it atomically to the dicts directory.
25func Download(lang string) (string, error) {
26	if lang == "" {
27		return "", fmt.Errorf("empty language code")
28	}
29	dest, err := DictPath(lang)
30	if err != nil {
31		return "", err
32	}
33
34	url := fmt.Sprintf(DictURLTemplate, urlPathLang(lang))
35	client := httpclient.New(httpclient.InstallTimeout)
36	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
37	if err != nil {
38		return "", fmt.Errorf("build request: %w", err)
39	}
40	resp, err := client.Do(req)
41	if err != nil {
42		return "", fmt.Errorf("download %s: %w", lang, err)
43	}
44	defer resp.Body.Close() //nolint:errcheck
45
46	if resp.StatusCode != http.StatusOK {
47		return "", fmt.Errorf("download %s: status %d", lang, resp.StatusCode)
48	}
49
50	tmp, err := os.CreateTemp(filepath.Dir(dest), ".dl-*")
51	if err != nil {
52		return "", fmt.Errorf("create temp: %w", err)
53	}
54	tmpPath := tmp.Name()
55	defer os.Remove(tmpPath) //nolint:errcheck
56
57	if _, err := io.Copy(tmp, resp.Body); err != nil {
58		tmp.Close() //nolint:errcheck,gosec
59		return "", fmt.Errorf("write dict: %w", err)
60	}
61	if err := tmp.Close(); err != nil {
62		return "", fmt.Errorf("close dict: %w", err)
63	}
64	if err := os.Rename(tmpPath, dest); err != nil {
65		return "", fmt.Errorf("install dict: %w", err)
66	}
67	return dest, nil
68}
69
70// EnsureDefault downloads the default English dictionary if it is not
71// already installed and returns the language code that is available.
72func EnsureDefault() (string, error) {
73	if DictInstalled(DefaultLanguage) {
74		return DefaultLanguage, nil
75	}
76	_, err := Download(DefaultLanguage)
77	if err != nil {
78		return "", err
79	}
80	return DefaultLanguage, nil
81}
82
83// urlPathLang converts a language code into the directory name used by the
84// wooorm/dictionaries repository ("en", "en-GB", "de", ...). The code is
85// passed through after normalising the region separator.
86func urlPathLang(lang string) string {
87	lang = strings.TrimSpace(lang)
88	lang = strings.ReplaceAll(lang, "_", "-")
89	return lang
90}