2023-07-12 14:06:17 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-09-02 16:03:37 +01:00
|
|
|
"context"
|
2023-07-12 14:06:17 +01:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2024-09-02 16:03:37 +01:00
|
|
|
"log/slog"
|
2023-07-12 14:06:17 +01:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2024-02-08 17:39:18 +00:00
|
|
|
"slices"
|
2023-07-12 14:06:17 +01:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/AdguardTeam/golibs/errors"
|
2023-10-06 14:45:48 +01:00
|
|
|
"github.com/AdguardTeam/golibs/ioutil"
|
2024-09-02 16:03:37 +01:00
|
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
2023-07-12 14:06:17 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// download and save all translations.
|
2024-09-02 16:03:37 +01:00
|
|
|
func (c *twoskyClient) download(ctx context.Context, l *slog.Logger) (err error) {
|
2023-07-12 14:06:17 +01:00
|
|
|
var numWorker int
|
|
|
|
|
|
|
|
flagSet := flag.NewFlagSet("download", flag.ExitOnError)
|
|
|
|
flagSet.Usage = func() {
|
|
|
|
usage("download command error")
|
|
|
|
}
|
|
|
|
flagSet.IntVar(&numWorker, "n", 1, "number of concurrent downloads")
|
|
|
|
|
|
|
|
err = flagSet.Parse(os.Args[2:])
|
|
|
|
if err != nil {
|
|
|
|
// Don't wrap the error since it's informative enough as is.
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if numWorker < 1 {
|
|
|
|
usage("count must be positive")
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadURI := c.uri.JoinPath("download")
|
|
|
|
|
|
|
|
client := &http.Client{
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
}
|
|
|
|
|
|
|
|
wg := &sync.WaitGroup{}
|
|
|
|
failed := &sync.Map{}
|
|
|
|
uriCh := make(chan *url.URL, len(c.langs))
|
|
|
|
|
2024-04-04 13:52:39 +01:00
|
|
|
for range numWorker {
|
2023-07-12 14:06:17 +01:00
|
|
|
wg.Add(1)
|
2024-09-02 16:03:37 +01:00
|
|
|
go downloadWorker(ctx, l, wg, failed, client, uriCh)
|
2023-07-12 14:06:17 +01:00
|
|
|
}
|
|
|
|
|
2023-07-20 17:52:14 +01:00
|
|
|
for _, lang := range c.langs {
|
2023-07-12 14:06:17 +01:00
|
|
|
uri := translationURL(downloadURI, defaultBaseFile, c.projectID, lang)
|
|
|
|
|
|
|
|
uriCh <- uri
|
|
|
|
}
|
|
|
|
|
|
|
|
close(uriCh)
|
|
|
|
wg.Wait()
|
|
|
|
|
2024-09-02 16:03:37 +01:00
|
|
|
printFailedLocales(ctx, l, failed)
|
2023-07-12 14:06:17 +01:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// printFailedLocales prints sorted list of failed downloads, if any.
|
2024-09-02 16:03:37 +01:00
|
|
|
func printFailedLocales(ctx context.Context, l *slog.Logger, failed *sync.Map) {
|
2023-07-12 14:06:17 +01:00
|
|
|
keys := []string{}
|
|
|
|
failed.Range(func(k, _ any) bool {
|
|
|
|
s, ok := k.(string)
|
|
|
|
if !ok {
|
|
|
|
panic("unexpected type")
|
|
|
|
}
|
|
|
|
|
|
|
|
keys = append(keys, s)
|
|
|
|
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
if len(keys) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
slices.Sort(keys)
|
2024-09-02 16:03:37 +01:00
|
|
|
l.InfoContext(ctx, "failed", "locales", keys)
|
2023-07-12 14:06:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// downloadWorker downloads translations by received urls and saves them.
|
|
|
|
// Where failed is a map for storing failed downloads.
|
|
|
|
func downloadWorker(
|
2024-09-02 16:03:37 +01:00
|
|
|
ctx context.Context,
|
|
|
|
l *slog.Logger,
|
2023-07-12 14:06:17 +01:00
|
|
|
wg *sync.WaitGroup,
|
|
|
|
failed *sync.Map,
|
|
|
|
client *http.Client,
|
|
|
|
uriCh <-chan *url.URL,
|
|
|
|
) {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
for uri := range uriCh {
|
|
|
|
q := uri.Query()
|
|
|
|
code := q.Get("language")
|
|
|
|
|
2024-09-02 16:03:37 +01:00
|
|
|
err := saveToFile(ctx, l, client, uri, code)
|
2023-07-12 14:06:17 +01:00
|
|
|
if err != nil {
|
2024-09-02 16:03:37 +01:00
|
|
|
l.ErrorContext(ctx, "download worker", slogutil.KeyError, err)
|
2023-07-12 14:06:17 +01:00
|
|
|
failed.Store(code, struct{}{})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// saveToFile downloads translation by url and saves it to a file, or returns
|
|
|
|
// error.
|
2024-09-02 16:03:37 +01:00
|
|
|
func saveToFile(
|
|
|
|
ctx context.Context,
|
|
|
|
l *slog.Logger,
|
|
|
|
client *http.Client,
|
|
|
|
uri *url.URL,
|
|
|
|
code string,
|
|
|
|
) (err error) {
|
|
|
|
data, err := getTranslation(ctx, l, client, uri.String())
|
2023-07-12 14:06:17 +01:00
|
|
|
if err != nil {
|
2024-09-02 16:03:37 +01:00
|
|
|
return fmt.Errorf("getting translation %q: %s", code, err)
|
2023-07-12 14:06:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
name := filepath.Join(localesDir, code+".json")
|
|
|
|
err = os.WriteFile(name, data, 0o664)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println(name)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getTranslation returns received translation data and error. If err is not
|
|
|
|
// nil, data may contain a response from server for inspection.
|
2024-09-02 16:03:37 +01:00
|
|
|
func getTranslation(
|
|
|
|
ctx context.Context,
|
|
|
|
l *slog.Logger,
|
|
|
|
client *http.Client,
|
|
|
|
url string,
|
|
|
|
) (data []byte, err error) {
|
2023-07-12 14:06:17 +01:00
|
|
|
resp, err := client.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("requesting: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-09-02 16:03:37 +01:00
|
|
|
defer slogutil.CloseAndLog(ctx, l, resp.Body, slog.LevelError)
|
2023-07-12 14:06:17 +01:00
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
err = fmt.Errorf("url: %q; status code: %s", url, http.StatusText(resp.StatusCode))
|
|
|
|
|
|
|
|
// Go on and download the body for inspection.
|
|
|
|
}
|
|
|
|
|
2023-10-06 14:45:48 +01:00
|
|
|
limitReader := ioutil.LimitReader(resp.Body, readLimit)
|
2023-07-12 14:06:17 +01:00
|
|
|
|
|
|
|
data, readErr := io.ReadAll(limitReader)
|
|
|
|
|
|
|
|
return data, errors.WithDeferred(err, readErr)
|
|
|
|
}
|
|
|
|
|
|
|
|
// translationURL returns a new url.URL with provided query parameters.
|
|
|
|
func translationURL(oldURL *url.URL, baseFile, projectID string, lang langCode) (uri *url.URL) {
|
|
|
|
uri = &url.URL{}
|
|
|
|
*uri = *oldURL
|
|
|
|
|
|
|
|
q := uri.Query()
|
|
|
|
q.Set("format", "json")
|
|
|
|
q.Set("filename", baseFile)
|
|
|
|
q.Set("project", projectID)
|
|
|
|
q.Set("language", string(lang))
|
|
|
|
|
|
|
|
uri.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
return uri
|
|
|
|
}
|