Add frontend #1
@@ -0,0 +1,105 @@
|
|||||||
|
package gitprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unzip extracts the zip at src into the dest directory,
|
||||||
|
// guarding against zip-slip path traversal attacks.
|
||||||
|
func unzip(src, dest string) error {
|
||||||
|
r, err := zip.OpenReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
destClean := filepath.Clean(dest) + string(os.PathSeparator)
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
target := filepath.Join(dest, filepath.Clean("/"+f.Name))
|
||||||
|
if !strings.HasPrefix(target, destClean) {
|
||||||
|
return fmt.Errorf("zip slip attempt: %q resolves outside destination", f.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
if err = os.MkdirAll(target, f.Mode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writeZipEntry(f, target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeZipEntry(f *zip.File, target string) error {
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, rc)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyDir recursively copies src to dst, preserving file modes.
|
||||||
|
// Used as a cross-device fallback when os.Rename fails.
|
||||||
|
func copyDir(src, dst string) error {
|
||||||
|
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(src, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := filepath.Join(dst, rel)
|
||||||
|
|
||||||
|
if d.IsDir() {
|
||||||
|
return os.MkdirAll(target, 0o755)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := d.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return copyFile(path, target, info.Mode())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string, mode fs.FileMode) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -13,6 +13,12 @@ func NewProvider(serverType, protocol, baseUrl, authToken string) (GitProvider,
|
|||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
}, nil
|
}, nil
|
||||||
|
case "gitea":
|
||||||
|
return &GiteaProvider{
|
||||||
|
protocol: protocol,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
authToken: authToken,
|
||||||
|
}, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported git provider: %s", serverType)
|
return nil, fmt.Errorf("unsupported git provider: %s", serverType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package gitprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GiteaProvider struct {
|
||||||
|
protocol string
|
||||||
|
baseUrl string
|
||||||
|
authToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ GitProvider = (*GiteaProvider)(nil)
|
||||||
|
|
||||||
|
func (p *GiteaProvider) FetchAndDeployBranch(owner, repo, branch, destPath string) (*DeployResult, error) {
|
||||||
|
apiUrl := fmt.Sprintf("%s://%s/api/v1/", p.protocol, strings.TrimSuffix(p.baseUrl, "/"))
|
||||||
|
|
||||||
|
result, err := fetchGiteaBranchHead(apiUrl, owner, repo, branch, p.authToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching branch head: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = downloadAndExtractGitea(apiUrl, owner, repo, branch, p.authToken, destPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGiteaBranchHead(apiUrl, owner, repo, branch, token string) (*DeployResult, error) {
|
||||||
|
url := fmt.Sprintf("%srepos/%s/%s/branches/%s", apiUrl, owner, repo, branch)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "token "+token)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching branch: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("Gitea returned %s for %s", resp.Status, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /repos/{owner}/{repo}/branches/{branch} returns a Branch object
|
||||||
|
// whose commit field is a PayloadCommit.
|
||||||
|
var payload struct {
|
||||||
|
Commit struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"commit"`
|
||||||
|
}
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
return nil, fmt.Errorf("decoding branch response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DeployResult{
|
||||||
|
CommitHash: payload.Commit.ID,
|
||||||
|
CommitMessage: strings.SplitN(payload.Commit.Message, "\n", 2)[0],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadAndExtractGitea(apiUrl, owner, repo, branch, token, destDir string) error {
|
||||||
|
// GET /repos/{owner}/{repo}/archive/{sha}.zip
|
||||||
|
archiveURL := fmt.Sprintf("%srepos/%s/%s/archive/%s.zip", apiUrl, owner, repo, branch)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, archiveURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "token "+token)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetching archive: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("Gitea returned %s for %s", resp.Status, archiveURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
storageRoot := filepath.Dir(destDir)
|
||||||
|
|
||||||
|
tmpZip, err := os.CreateTemp(storageRoot, "giteabranch-*.zip")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating temp zip: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpZip.Name())
|
||||||
|
|
||||||
|
if _, err = io.Copy(tmpZip, resp.Body); err != nil {
|
||||||
|
tmpZip.Close()
|
||||||
|
return fmt.Errorf("writing zip: %w", err)
|
||||||
|
}
|
||||||
|
tmpZip.Close()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp(storageRoot, "giteabranch-unpack-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating temp dir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if err = unzip(tmpZip.Name(), tmpDir); err != nil {
|
||||||
|
return fmt.Errorf("unzipping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gitea wraps everything in {repo}-{branch}/ (all lowercase).
|
||||||
|
entries, err := os.ReadDir(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading temp dir: %w", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 1 || !entries[0].IsDir() {
|
||||||
|
return fmt.Errorf("unexpected archive layout: expected a single root directory")
|
||||||
|
}
|
||||||
|
extractedRoot := filepath.Join(tmpDir, entries[0].Name())
|
||||||
|
|
||||||
|
if err = os.MkdirAll(filepath.Dir(destDir), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("creating destination parent dirs: %w", err)
|
||||||
|
}
|
||||||
|
os.RemoveAll(destDir)
|
||||||
|
|
||||||
|
if err = os.Rename(extractedRoot, destDir); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = copyDir(extractedRoot, destDir); err != nil {
|
||||||
|
return fmt.Errorf("cross-device copy to destination: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package gitprovider
|
package gitprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -149,97 +147,3 @@ func downloadAndExtract(repoOwner, repoName, branch, pat, destDir string) error
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// unzip extracts the zip at src into the dest directory,
|
|
||||||
// guarding against zip-slip path traversal attacks.
|
|
||||||
func unzip(src, dest string) error {
|
|
||||||
r, err := zip.OpenReader(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
destClean := filepath.Clean(dest) + string(os.PathSeparator)
|
|
||||||
|
|
||||||
for _, f := range r.File {
|
|
||||||
target := filepath.Join(dest, filepath.Clean("/"+f.Name))
|
|
||||||
if !strings.HasPrefix(target, destClean) {
|
|
||||||
return fmt.Errorf("zip slip attempt: %q resolves outside destination", f.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
|
||||||
if err = os.MkdirAll(target, f.Mode()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = writeZipEntry(f, target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeZipEntry(f *zip.File, target string) error {
|
|
||||||
rc, err := f.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
|
|
||||||
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(out, rc)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyDir recursively copies src to dst, preserving file modes.
|
|
||||||
// Used as a cross-device fallback when os.Rename fails.
|
|
||||||
func copyDir(src, dst string) error {
|
|
||||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rel, err := filepath.Rel(src, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
target := filepath.Join(dst, rel)
|
|
||||||
|
|
||||||
if d.IsDir() {
|
|
||||||
return os.MkdirAll(target, 0o755)
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := d.Info()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return copyFile(path, target, info.Mode())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(src, dst string, mode fs.FileMode) error {
|
|
||||||
in, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user