// Создано: 2026-04-25 // source.go — handler для GET /v1/namespaces/{namespace}/functions/{name}/source // Возвращает список файлов исходного кода функции из tar.gz контекста сборки (S3). // Dockerfile исключается — он сгенерирован builder-ом, не является кодом пользователя. // Если код ещё не загружен (S3Key пустой) — возвращает пустой список. package handler import ( "archive/tar" "compress/gzip" "encoding/base64" "fmt" "io" "net/http" "strings" "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1" ) // sourceFileEntry — один файл из исходного кода функции. type sourceFileEntry struct { Name string `json:"name"` Content string `json:"content"` // UTF-8 текст; бинарные файлы — base64 encoded Binary bool `json:"binary"` // true если файл бинарный (base64 в content) } // GetSource — GET /v1/namespaces/{namespace}/functions/{name}/source // Скачивает tar.gz контекст сборки из S3 и возвращает пользовательские файлы (без Dockerfile). func (h *Handler) GetSource(w http.ResponseWriter, r *http.Request) { ns := namespace(r) name := pathVar(r, "name") fn := &slessv1alpha1.Function{} if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fn); err != nil { if errors.IsNotFound(err) { writeJSON(w, http.StatusNotFound, errResp("function not found")) return } writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } // Код ещё не был загружен — возвращаем пустой список без ошибки if fn.Spec.S3Key == "" { writeJSON(w, http.StatusOK, []sourceFileEntry{}) return } rc, err := h.S3.Download(r.Context(), fn.Spec.S3Key) if err != nil { writeJSON(w, http.StatusInternalServerError, errResp("download from S3: "+err.Error())) return } defer rc.Close() files, err := extractSourceFilesFromTarGz(rc) if err != nil { writeJSON(w, http.StatusInternalServerError, errResp("extract source: "+err.Error())) return } writeJSON(w, http.StatusOK, files) } // extractSourceFilesFromTarGz читает tar.gz и возвращает пользовательские файлы. // Dockerfile пропускается — он генерируется builder-ом автоматически. // Бинарные файлы (содержат null bytes) возвращаются в base64 с binary=true. func extractSourceFilesFromTarGz(r io.Reader) ([]sourceFileEntry, error) { gr, err := gzip.NewReader(r) if err != nil { return nil, fmt.Errorf("gzip: %w", err) } defer gr.Close() tr := tar.NewReader(gr) var result []sourceFileEntry for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return nil, fmt.Errorf("tar next: %w", err) } // Dockerfile сгенерирован builder-ом — не показываем пользователю if hdr.Name == "Dockerfile" || hdr.FileInfo().IsDir() { continue } data, err := io.ReadAll(tr) if err != nil { return nil, fmt.Errorf("read %s: %w", hdr.Name, err) } // Бинарные файлы — base64, текстовые — как есть content := string(data) isBinary := strings.ContainsRune(content, '\x00') if isBinary { content = base64.StdEncoding.EncodeToString(data) } result = append(result, sourceFileEntry{ Name: hdr.Name, Content: content, Binary: isBinary, }) } if result == nil { result = []sourceFileEntry{} } return result, nil }