155 lines
4.2 KiB
Go
155 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const maxDiagnosticBundleBytes = int64(256 * 1024 * 1024)
|
|
|
|
var diagnosticBundleID = regexp.MustCompile(`^[a-f0-9]{16,64}$`)
|
|
|
|
func (s *Server) handleWorkspaceDiagnostics(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if !diagnosticBundleID.MatchString(id) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid diagnostic bundle id"})
|
|
return
|
|
}
|
|
root := filepath.Join(s.procMgr.WorkDir(), ".kandev", "diagnostics")
|
|
if err := os.MkdirAll(root, 0o700); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to prepare diagnostics directory"})
|
|
return
|
|
}
|
|
_ = os.Chmod(root, 0o700)
|
|
cleanupExpiredDiagnostics(root, time.Now().Add(-24*time.Hour))
|
|
file, err := os.CreateTemp(root, ".upload-*.tmp")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create diagnostic file"})
|
|
return
|
|
}
|
|
tempPath := file.Name()
|
|
defer func() { _ = os.Remove(tempPath) }()
|
|
_ = file.Chmod(0o600)
|
|
written, copyErr := io.CopyBuffer(
|
|
file,
|
|
io.LimitReader(c.Request.Body, maxDiagnosticBundleBytes+1),
|
|
make([]byte, 1024*1024),
|
|
)
|
|
closeErr := file.Close()
|
|
if copyErr != nil || closeErr != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write diagnostic file"})
|
|
return
|
|
}
|
|
if written > maxDiagnosticBundleBytes {
|
|
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "diagnostic bundle is too large"})
|
|
return
|
|
}
|
|
destination := filepath.Join(root, id+".zip")
|
|
if err := os.Rename(tempPath, destination); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit diagnostic file"})
|
|
return
|
|
}
|
|
_ = os.Chmod(destination, 0o600)
|
|
ensureDiagnosticsIgnored(s.procMgr.WorkDir())
|
|
time.AfterFunc(24*time.Hour, func() {
|
|
info, err := os.Lstat(destination)
|
|
if err == nil && info.Mode().IsRegular() && time.Since(info.ModTime()) >= 24*time.Hour {
|
|
_ = os.Remove(destination)
|
|
}
|
|
})
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"path": filepath.ToSlash(filepath.Join(".kandev", "diagnostics", id+".zip")),
|
|
"bytes": written,
|
|
})
|
|
}
|
|
|
|
func cleanupExpiredDiagnostics(root string, cutoff time.Time) {
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".zip") {
|
|
continue
|
|
}
|
|
info, err := entry.Info()
|
|
if err == nil && info.Mode().IsRegular() && info.ModTime().Before(cutoff) {
|
|
_ = os.Remove(filepath.Join(root, entry.Name()))
|
|
}
|
|
}
|
|
}
|
|
|
|
func ensureDiagnosticsIgnored(workDir string) {
|
|
excludePath, err := diagnosticsExcludePath(workDir)
|
|
if err != nil || excludePath == "" {
|
|
return
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(excludePath), 0o700); err != nil {
|
|
return
|
|
}
|
|
content, err := os.ReadFile(excludePath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return
|
|
}
|
|
if strings.Contains(string(content), "/.kandev/") {
|
|
return
|
|
}
|
|
file, err := os.OpenFile(excludePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
_, _ = file.WriteString("\n/.kandev/\n")
|
|
}
|
|
|
|
func diagnosticsExcludePath(workDir string) (string, error) {
|
|
gitPath := filepath.Join(workDir, ".git")
|
|
info, err := os.Stat(gitPath)
|
|
if err == nil && info.IsDir() {
|
|
return filepath.Join(gitPath, "info", "exclude"), nil
|
|
}
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
data, err := os.ReadFile(gitPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
const prefix = "gitdir:"
|
|
line := strings.TrimSpace(string(data))
|
|
if !strings.HasPrefix(line, prefix) {
|
|
return "", nil
|
|
}
|
|
gitDir := strings.TrimSpace(strings.TrimPrefix(line, prefix))
|
|
if gitDir == "" {
|
|
return "", nil
|
|
}
|
|
if !filepath.IsAbs(gitDir) {
|
|
gitDir = filepath.Join(workDir, gitDir)
|
|
}
|
|
gitDir = filepath.Clean(gitDir)
|
|
commonData, commonErr := os.ReadFile(filepath.Join(gitDir, "commondir"))
|
|
if commonErr == nil {
|
|
commonDir := strings.TrimSpace(string(commonData))
|
|
if commonDir != "" {
|
|
if !filepath.IsAbs(commonDir) {
|
|
commonDir = filepath.Join(gitDir, commonDir)
|
|
}
|
|
gitDir = filepath.Clean(commonDir)
|
|
}
|
|
} else if !os.IsNotExist(commonErr) {
|
|
return "", commonErr
|
|
}
|
|
return filepath.Join(gitDir, "info", "exclude"), nil
|
|
}
|