Files
2026-07-13 12:32:25 +08:00

246 lines
6.9 KiB
Go

// Copyright 2025 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
const (
query = `extension:sql pg_dump`
downloadCount = 110
)
// RepoName simply contains the name of the repository.
type RepoName struct {
FullName string `json:"full_name"`
}
// Item is a SQL file (hopefully) containing a pg_dump.
type Item struct {
Name string `json:"name"`
Path string `json:"path"`
HtmlURL string `json:"html_url"`
ContentsURL string `json:"url"`
Repository RepoName `json:"repository"`
}
// CodeSearchResult contains the result of a code search.
type CodeSearchResult struct {
TotalCount int `json:"total_count"`
IncompleteResults bool `json:"incomplete_results"`
Items []Item `json:"items"`
Message string `json:"message"` // Only used when there's an error
}
// ContentFile is all of the information about a SQL file, including how to retrieve it.
type ContentFile struct {
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
SHA string `json:"sha"`
Size int64 `json:"size"`
HTMLURL string `json:"html_url"`
DownloadURL string `json:"download_url"`
}
func main() {
ctx := context.Background()
httpClient := &http.Client{Timeout: 30 * time.Second}
token := os.Getenv("GITHUB_TOKEN")
if len(token) == 0 {
fmt.Println("Must provide a GITHUB_TOKEN as an environment variable")
os.Exit(1)
}
_, currentFileLocation, _, ok := runtime.Caller(0)
if !ok {
fmt.Println("Unable to find the folder where this file is located")
os.Exit(1)
}
dumpsFolder := filepath.Clean(filepath.Join(filepath.Dir(currentFileLocation), "../sql"))
var saved int
page := 1
OuterLoop:
for {
remaining := downloadCount - saved
items, err := SearchCode(ctx, httpClient, token, page, min(50, remaining))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if len(items) == 0 {
break
}
for _, item := range items {
cf, err := GetContent(ctx, httpClient, token, item.ContentsURL)
if err != nil {
fmt.Printf("warn: %s/%s: %v\n", item.Repository.FullName, item.Path, err)
continue
}
if cf.Type != "file" || cf.DownloadURL == "" {
continue
}
dest := filepath.Join(dumpsFolder, SanitizePath(item.Repository.FullName)+filepath.Ext(cf.Path))
if _, err = os.Stat(dest); err == nil {
continue
}
if err = DownloadFile(ctx, httpClient, item, cf.DownloadURL, dest); err != nil {
fmt.Printf("download error: %s -> %v\n", dest, err)
continue
}
fmt.Printf("saved: %s (%d bytes)\n", dest, cf.Size)
saved++
if saved >= downloadCount {
break OuterLoop
}
time.Sleep(6500 * time.Millisecond) // We sleep to mitigate rate limits
}
page++
}
}
// SearchCode executes the query against the API, returning all items that were found.
func SearchCode(ctx context.Context, hc *http.Client, token string, page int, perPage int) ([]Item, error) {
params := url.Values{}
params.Set("q", query)
params.Set("page", strconv.Itoa(page))
params.Set("per_page", strconv.Itoa(perPage))
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/search/code?"+params.Encode(), nil)
SetHeaders(req, token)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if HandleRate(resp) {
return SearchCode(ctx, hc, token, page, perPage)
}
var sr CodeSearchResult
if err = json.NewDecoder(resp.Body).Decode(&sr); err != nil {
return nil, err
}
if resp.StatusCode != 200 {
if sr.Message != "" {
return nil, fmt.Errorf("search error: %s (HTTP %d)", sr.Message, resp.StatusCode)
}
return nil, fmt.Errorf("search error: HTTP %d", resp.StatusCode)
}
return sr.Items, nil
}
// GetContent gets the ContentFile from the given URL.
func GetContent(ctx context.Context, hc *http.Client, token string, contentsURL string) (*ContentFile, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", contentsURL, nil)
SetHeaders(req, token)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if HandleRate(resp) {
return GetContent(ctx, hc, token, contentsURL)
}
if resp.StatusCode != 200 {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("contents error: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
var cf ContentFile
if err = json.NewDecoder(resp.Body).Decode(&cf); err != nil {
return nil, err
}
return &cf, nil
}
// DownloadFile downloads the given SQL file to the destination.
func DownloadFile(ctx context.Context, hc *http.Client, item Item, rawURL string, dest string) error {
req, _ := http.NewRequestWithContext(ctx, "GET", rawURL, nil)
req.Header.Set("User-Agent", "gh-pg-dump-finder/1.0")
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("download HTTP %d", resp.StatusCode)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, _ = io.WriteString(out, fmt.Sprintf("-- Downloaded from: %s\n", item.HtmlURL))
_, err = io.Copy(out, resp.Body)
return err
}
// SetHeaders sets the appropriate headers for a request.
func SetHeaders(req *http.Request, token string) {
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "gh-pg-dump-finder/1.0")
req.Header.Set("Authorization", "Bearer "+token)
}
// HandleRate handles potential rate limits.
func HandleRate(resp *http.Response) bool {
if resp.StatusCode == 403 {
if ra := resp.Header.Get("Retry-After"); ra != "" {
if secs, _ := strconv.Atoi(ra); secs > 0 {
sleepTime := time.Duration(secs) * time.Second
fmt.Printf("rate limited (%s), retrying\n", sleepTime.String())
time.Sleep(sleepTime)
return true
}
}
if reset := resp.Header.Get("X-RateLimit-Reset"); reset != "" {
if ts, _ := strconv.ParseInt(reset, 10, 64); ts > 0 {
wait := time.Until(time.Unix(ts+5, 0))
if wait > 0 && wait < 5*time.Minute {
fmt.Printf("rate limited (%s), retrying\n", wait.String())
time.Sleep(wait)
return true
}
}
}
}
return false
}
// SanitizePath removes potentially invalid file system characters.
func SanitizePath(s string) string {
illegal := `<>:"\|/?*`
for _, r := range illegal {
s = strings.ReplaceAll(s, string(r), "_")
}
return s
}