261 lines
7.3 KiB
Go
261 lines
7.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const githubBlobMaxBytes = 100 * 1024 * 1024
|
|
|
|
func collectDirFiles(rootDir string) ([]gitPathFile, []string) {
|
|
cleanRoot := filepath.Clean(rootDir)
|
|
byPath := map[string]gitPathFile{}
|
|
var warnings []string
|
|
|
|
_ = filepath.Walk(cleanRoot, func(path string, info os.FileInfo, walkErr error) error {
|
|
if walkErr != nil {
|
|
warnings = append(warnings, path+": "+walkErr.Error())
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
if shouldSkipBundledPath(filepath.Base(path)) && path != cleanRoot {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(cleanRoot, path)
|
|
if err != nil {
|
|
warnings = append(warnings, path+": "+err.Error())
|
|
return nil
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
if rel == "" || strings.HasPrefix(rel, "..") || shouldSkipBundledPath(rel) {
|
|
return nil
|
|
}
|
|
if info.Size() > githubBlobMaxBytes {
|
|
warnings = append(warnings, rel+": file quá lớn (>100MB), bỏ qua")
|
|
return nil
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
warnings = append(warnings, rel+": "+err.Error())
|
|
return nil
|
|
}
|
|
byPath[rel] = gitPathFile{path: rel, content: data}
|
|
return nil
|
|
})
|
|
|
|
out := make([]gitPathFile, 0, len(byPath))
|
|
for _, f := range byPath {
|
|
out = append(out, f)
|
|
}
|
|
return out, warnings
|
|
}
|
|
|
|
type gitPathFile struct {
|
|
path string
|
|
content []byte
|
|
}
|
|
|
|
func githubCreateBlob(token, owner, repo string, content []byte) (string, error) {
|
|
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/blobs", owner, repo)
|
|
body := map[string]string{
|
|
"content": base64.StdEncoding.EncodeToString(content),
|
|
"encoding": "base64",
|
|
}
|
|
var resp struct {
|
|
SHA string `json:"sha"`
|
|
}
|
|
if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
if resp.SHA == "" {
|
|
return "", errors.New("GitHub không trả blob sha")
|
|
}
|
|
return resp.SHA, nil
|
|
}
|
|
|
|
func githubCreateTree(token, owner, repo, baseTreeSHA string, entries []map[string]string) (string, error) {
|
|
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees", owner, repo)
|
|
body := map[string]any{"tree": entries}
|
|
if baseTreeSHA != "" {
|
|
body["base_tree"] = baseTreeSHA
|
|
}
|
|
var resp struct {
|
|
SHA string `json:"sha"`
|
|
}
|
|
if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
if resp.SHA == "" {
|
|
return "", errors.New("GitHub không trả tree sha")
|
|
}
|
|
return resp.SHA, nil
|
|
}
|
|
|
|
func githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message string) (string, error) {
|
|
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits", owner, repo)
|
|
body := map[string]any{
|
|
"message": message,
|
|
"tree": treeSHA,
|
|
}
|
|
if parentSHA != "" {
|
|
body["parents"] = []string{parentSHA}
|
|
}
|
|
var resp struct {
|
|
SHA string `json:"sha"`
|
|
}
|
|
if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
if resp.SHA == "" {
|
|
return "", errors.New("GitHub không trả commit sha")
|
|
}
|
|
return resp.SHA, nil
|
|
}
|
|
|
|
func githubUpdateBranchRef(token, owner, repo, branch, commitSHA string) error {
|
|
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs/heads/%s", owner, repo, branch)
|
|
body := map[string]any{"sha": commitSHA, "force": false}
|
|
err := githubJSON(token, http.MethodPatch, apiURL, body, nil)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
createURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs", owner, repo)
|
|
createBody := map[string]string{
|
|
"ref": "refs/heads/" + branch,
|
|
"sha": commitSHA,
|
|
}
|
|
return githubJSON(token, http.MethodPost, createURL, createBody, nil)
|
|
}
|
|
|
|
func githubBranchTip(token, owner, repo, branch string) (commitSHA, treeSHA string, err error) {
|
|
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/ref/heads/%s", owner, repo, branch)
|
|
var refResp struct {
|
|
Object struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"object"`
|
|
}
|
|
if err := githubJSON(token, http.MethodGet, apiURL, nil, &refResp); err != nil {
|
|
return "", "", err
|
|
}
|
|
commitSHA = refResp.Object.SHA
|
|
if commitSHA == "" {
|
|
return "", "", errors.New("không lấy được commit hiện tại")
|
|
}
|
|
commitURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits/%s", owner, repo, commitSHA)
|
|
var commitResp struct {
|
|
Tree struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"tree"`
|
|
}
|
|
if err := githubJSON(token, http.MethodGet, commitURL, nil, &commitResp); err != nil {
|
|
return "", "", err
|
|
}
|
|
return commitSHA, commitResp.Tree.SHA, nil
|
|
}
|
|
|
|
func githubJSON(token, method, apiURL string, body any, out any) error {
|
|
var reader io.Reader
|
|
if body != nil {
|
|
payload, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader = bytes.NewReader(payload)
|
|
}
|
|
req, err := http.NewRequest(method, apiURL, reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 300 {
|
|
var errBody struct {
|
|
Message string `json:"message"`
|
|
}
|
|
_ = json.Unmarshal(raw, &errBody)
|
|
if errBody.Message != "" {
|
|
return errors.New(errBody.Message)
|
|
}
|
|
return fmt.Errorf("GitHub API lỗi (%d)", resp.StatusCode)
|
|
}
|
|
if out != nil && len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, out); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// githubPushFiles — đẩy danh sách file lên repo; file lỗi được bỏ qua.
|
|
func githubPushFiles(token, owner, repo, branch string, files []gitPathFile, message string) (string, []string, error) {
|
|
warnings := []string{}
|
|
if len(files) == 0 {
|
|
return "", warnings, errors.New("không có file để đẩy")
|
|
}
|
|
|
|
parentSHA, baseTreeSHA, tipErr := githubBranchTip(token, owner, repo, branch)
|
|
if tipErr != nil {
|
|
parentSHA = ""
|
|
baseTreeSHA = ""
|
|
}
|
|
|
|
entries := make([]map[string]string, 0, len(files))
|
|
for _, f := range files {
|
|
blobSHA, err := githubCreateBlob(token, owner, repo, f.content)
|
|
if err != nil {
|
|
warnings = append(warnings, f.path+": "+err.Error()+" (bỏ qua)")
|
|
continue
|
|
}
|
|
entries = append(entries, map[string]string{
|
|
"path": f.path,
|
|
"mode": "100644",
|
|
"type": "blob",
|
|
"sha": blobSHA,
|
|
})
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
return "", warnings, errors.New("không đẩy được file nào lên GitHub")
|
|
}
|
|
|
|
treeSHA, err := githubCreateTree(token, owner, repo, baseTreeSHA, entries)
|
|
if err != nil {
|
|
return "", warnings, err
|
|
}
|
|
commitSHA, err := githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message)
|
|
if err != nil {
|
|
return "", warnings, err
|
|
}
|
|
if err := githubUpdateBranchRef(token, owner, repo, branch, commitSHA); err != nil {
|
|
return "", warnings, err
|
|
}
|
|
return fmt.Sprintf("https://github.com/%s/%s/tree/%s", owner, repo, branch), warnings, nil
|
|
}
|
|
|
|
// githubPushDirectory — đẩy file lên repo; file lỗi được bỏ qua, không chặn cả lần push.
|
|
func githubPushDirectory(token, owner, repo, branch, localRoot, message string) (string, []string, error) {
|
|
files, collectWarn := collectDirFiles(localRoot)
|
|
warnings := append([]string{}, collectWarn...)
|
|
url, pushWarn, err := githubPushFiles(token, owner, repo, branch, files, message)
|
|
return url, append(warnings, pushWarn...), err
|
|
}
|