From 72aae8e3ecece834462544673d65b58a81320f03 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Wed, 25 Mar 2026 10:29:12 +0000 Subject: [PATCH] knotmirror,tapc,spindle: rename go files to snake_case Triggers my autism. Signed-off-by: Anirudh Oppiliappan --- tapc/simpleIndexer.go | 24 ------------------------ tapc/simple_indexer.go | 24 ++++++++++++++++++++++++ knotmirror/xrpc/git_getArchive.go | 106 ---------------------------------------------------------------------------------------------------------- knotmirror/xrpc/git_getBlob.go | 86 -------------------------------------------------------------------------------------- knotmirror/xrpc/git_getBranch.go | 85 ------------------------------------------------------------------------------------- knotmirror/xrpc/git_getTag.go | 92 -------------------------------------------------------------------------------------------- knotmirror/xrpc/git_getTree.go | 118 ---------------------------------------------------------------------------------------------------------------------- knotmirror/xrpc/git_get_archive.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_get_blob.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_get_branch.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_get_tag.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_get_tree.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_listBranches.go | 95 ----------------------------------------------------------------------------------------------- knotmirror/xrpc/git_listCommits.go | 95 ----------------------------------------------------------------------------------------------- knotmirror/xrpc/git_listLanguages.go | 86 -------------------------------------------------------------------------------------- knotmirror/xrpc/git_listTags.go | 98 -------------------------------------------------------------------------------------------------- knotmirror/xrpc/git_list_branches.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_list_commits.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_list_languages.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/git_list_tags.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ knotmirror/xrpc/sync_requestCrawl.go | 104 -------------------------------------------------------------------------------------------------------- knotmirror/xrpc/sync_request_crawl.go | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ spindle/xrpc/pipeline_cancelPipeline.go | 97 ------------------------------------------------------------------------------------------------- spindle/xrpc/pipeline_cancel_pipeline.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 24 file(s) changed, 1086 insertion(s)(+), 1086 deletion(s)(-) diff --git a/tapc/simpleIndexer.go b/tapc/simpleIndexer.go deleted file mode 100644 --- a/tapc/simpleIndexer.go +++ /dev/null @@ -1,24 +0,0 @@ -package tapc - -import "context" - -type SimpleIndexer struct { - EventHandler func(ctx context.Context, evt Event) error - ErrorHandler func(ctx context.Context, err error) -} - -var _ Handler = (*SimpleIndexer)(nil) - -func (i *SimpleIndexer) OnEvent(ctx context.Context, evt Event) error { - if i.EventHandler == nil { - return nil - } - return i.EventHandler(ctx, evt) -} - -func (i *SimpleIndexer) OnError(ctx context.Context, err error) { - if i.ErrorHandler == nil { - return - } - i.ErrorHandler(ctx, err) -} diff --git a/tapc/simple_indexer.go b/tapc/simple_indexer.go new file mode 100644 --- /dev/null +++ b/tapc/simple_indexer.go @@ -0,0 +1,24 @@ +package tapc + +import "context" + +type SimpleIndexer struct { + EventHandler func(ctx context.Context, evt Event) error + ErrorHandler func(ctx context.Context, err error) +} + +var _ Handler = (*SimpleIndexer)(nil) + +func (i *SimpleIndexer) OnEvent(ctx context.Context, evt Event) error { + if i.EventHandler == nil { + return nil + } + return i.EventHandler(ctx, evt) +} + +func (i *SimpleIndexer) OnError(ctx context.Context, err error) { + if i.ErrorHandler == nil { + return + } + i.ErrorHandler(ctx, err) +} diff --git a/knotmirror/xrpc/git_getArchive.go b/knotmirror/xrpc/git_getArchive.go deleted file mode 100644 --- a/knotmirror/xrpc/git_getArchive.go +++ /dev/null @@ -1,106 +0,0 @@ -package xrpc - -import ( - "compress/gzip" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/go-git/go-git/v5/plumbing" - "tangled.org/core/api/tangled" - "tangled.org/core/knotmirror/db" - "tangled.org/core/knotserver/git" -) - -func (x *Xrpc) GetArchive(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - ref = r.URL.Query().Get("ref") - format = r.URL.Query().Get("format") - prefix = r.URL.Query().Get("prefix") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - if format != "tar.gz" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "only tar.gz format is supported"}) - return - } - if format == "" { - format = "tar.gz" - } - - l := x.logger.With("repo", repo, "ref", ref, "format", format, "prefix", prefix) - ctx := r.Context() - - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - l.Error("failed to resolve repo at-uri", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to resolve repo"}) - return - } - - gr, err := git.Open(repoPath, ref) - if err != nil { - l.Error("failed to open git repo", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to open git repo"}) - return - } - - repoName, err := func() (string, error) { - r, err := db.GetRepoByAtUri(ctx, x.db, repo) - if err != nil { - return "", err - } - if r == nil { - return "", fmt.Errorf("repo not found: %s", repo) - } - return r.Name, nil - }() - if err != nil { - l.Error("failed to get repo name", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to retrieve repo name"}) - return - } - - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") - immutableLink := func() string { - params := url.Values{} - params.Set("repo", repo.String()) - params.Set("ref", gr.Hash().String()) - params.Set("format", format) - params.Set("prefix", prefix) - return fmt.Sprintf("%s/xrpc/%s?%s", x.cfg.BaseUrl(), tangled.GitTempGetArchiveNSID, params.Encode()) - }() - - filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) - w.Header().Set("Content-Type", "application/gzip") - w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) - - gw := gzip.NewWriter(w) - defer gw.Close() - - if err := gr.WriteTar(gw, prefix); err != nil { - // once we start writing to the body we can't report error anymore - // so we are only left with logging the error - l.Error("writing tar file", "err", err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err := gw.Flush(); err != nil { - // once we start writing to the body we can't report error anymore - // so we are only left with logging the error - l.Error("flushing", "err", err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/knotmirror/xrpc/git_getBlob.go b/knotmirror/xrpc/git_getBlob.go deleted file mode 100644 --- a/knotmirror/xrpc/git_getBlob.go +++ /dev/null @@ -1,86 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "io" - "net/http" - "slices" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/go-git/go-git/v5/plumbing/object" - "tangled.org/core/knotserver/git" -) - -func (x *Xrpc) GetBlob(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) - path = r.URL.Query().Get("path") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - l := x.logger.With("repo", repo, "ref", ref, "path", path) - - if path == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing path parameter"}) - return - } - - file, err := x.getFile(r.Context(), repo, ref, path) - if err != nil { - // TODO: better error return - l.Error("failed to get blob", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) - return - } - - reader, err := file.Reader() - if err != nil { - l.Error("failed to read blob", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) - return - } - defer reader.Close() - - w.Header().Set("Content-Type", "application/octet-stream") - if _, err := io.Copy(w, reader); err != nil { - l.Error("failed to serve the blob", "err", err) - } -} - -func (x *Xrpc) getFile(ctx context.Context, repo syntax.ATURI, ref, path string) (*object.File, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("resolving repo at-uri: %w", err) - } - - gr, err := git.Open(repoPath, ref) - if err != nil { - return nil, fmt.Errorf("opening git repo: %w", err) - } - - return gr.File(path) -} - -var textualMimeTypes = []string{ - "application/json", - "application/xml", - "application/yaml", - "application/x-yaml", - "application/toml", - "application/javascript", - "application/ecmascript", -} - -// isTextualMimeType returns true if the MIME type represents textual content -// that should be served as text/plain for security reasons -func isTextualMimeType(mimeType string) bool { - return slices.Contains(textualMimeTypes, mimeType) -} diff --git a/knotmirror/xrpc/git_getBranch.go b/knotmirror/xrpc/git_getBranch.go deleted file mode 100644 --- a/knotmirror/xrpc/git_getBranch.go +++ /dev/null @@ -1,85 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/api/tangled" - "tangled.org/core/knotserver/git" -) - -// TODO: maybe rename to `sh.tangled.repo.temp.getCommit`? -// then, we should ensure the given `ref` is valid -func (x *Xrpc) GetBranch(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - nameQuery = r.URL.Query().Get("name") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - if nameQuery == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing name parameter"}) - return - } - branchName, _ := url.PathUnescape(nameQuery) - - l := x.logger.With("repo", repo, "branch", branchName) - - out, err := x.getBranch(r.Context(), repo, branchName) - if err != nil { - // TODO: better error return - l.Error("failed to get branch", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get branch"}) - return - } - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) getBranch(ctx context.Context, repo syntax.ATURI, branchName string) (*tangled.GitTempGetBranch_Output, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) - } - - gr, err := git.PlainOpen(repoPath) - if err != nil { - return nil, fmt.Errorf("failed to open git repo: %w", err) - } - - ref, err := gr.Branch(branchName) - if err != nil { - return nil, fmt.Errorf("getting branch '%s': %w", branchName, err) - } - - commit, err := gr.Commit(ref.Hash()) - if err != nil { - return nil, fmt.Errorf("getting commit '%s': %w", ref.Hash(), err) - } - - out := tangled.GitTempGetBranch_Output{ - Name: ref.Name().Short(), - Hash: ref.Hash().String(), - When: commit.Author.When.Format(time.RFC3339), - Author: &tangled.GitTempDefs_Signature{ - Name: commit.Author.Name, - Email: commit.Author.Email, - When: commit.Author.When.Format(time.RFC3339), - }, - } - - if commit.Message != "" { - out.Message = &commit.Message - } - - return &out, nil -} diff --git a/knotmirror/xrpc/git_getTag.go b/knotmirror/xrpc/git_getTag.go deleted file mode 100644 --- a/knotmirror/xrpc/git_getTag.go +++ /dev/null @@ -1,92 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "net/http" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "tangled.org/core/knotserver/git" - "tangled.org/core/types" -) - -func (x *Xrpc) GetTag(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - tagName = r.URL.Query().Get("tag") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - if tagName == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing 'tag' parameter"}) - return - } - - l := x.logger.With("repo", repo, "tag", tagName) - - out, err := x.getTag(r.Context(), repo, tagName) - if err != nil { - // TODO: better error return - l.Error("failed to get tag", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tag"}) - return - } - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) getTag(ctx context.Context, repo syntax.ATURI, tagName string) (*types.RepoTagResponse, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) - } - - gr, err := git.PlainOpen(repoPath) - if err != nil { - return nil, fmt.Errorf("failed to open git repo: %w", err) - } - - // if this is not already formatted as refs/tags/v0.1.0, then format it - if !plumbing.ReferenceName(tagName).IsTag() { - tagName = plumbing.NewTagReferenceName(tagName).String() - } - - tag, err := func() (object.Tag, error) { - tags, err := gr.Tags(&git.TagsOptions{ - Pattern: tagName, - }) - if err != nil { - return object.Tag{}, err - } - if len(tags) != 1 { - return object.Tag{}, fmt.Errorf("expected 1 tag to be returned, got %d tags", len(tags)) - } - return tags[0], nil - }() - if err != nil { - return nil, fmt.Errorf("getting tag: %w", err) - } - - var target *object.Tag - if tag.Target != plumbing.ZeroHash { - target = &tag - } - - return &types.RepoTagResponse{ - Tag: &types.TagReference{ - Tag: target, - Reference: types.Reference{ - Name: tag.Name, - Hash: tag.Hash.String(), - }, - Message: tag.Message, - }, - }, nil -} diff --git a/knotmirror/xrpc/git_getTree.go b/knotmirror/xrpc/git_getTree.go deleted file mode 100644 --- a/knotmirror/xrpc/git_getTree.go +++ /dev/null @@ -1,118 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "net/http" - "path/filepath" - "time" - "unicode/utf8" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/api/tangled" - "tangled.org/core/appview/pages/markup" - "tangled.org/core/knotserver/git" -) - -func (x *Xrpc) GetTree(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) - path = r.URL.Query().Get("path") // path can be empty (defaults to root) - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - l := x.logger.With("repo", repo, "ref", ref, "path", path) - - out, err := x.getTree(r.Context(), repo, ref, path) - if err != nil { - // TODO: better error return - l.Error("failed to get tree", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tree"}) - return - } - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) getTree(ctx context.Context, repo syntax.ATURI, ref, path string) (*tangled.GitTempGetTree_Output, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) - } - - gr, err := git.Open(repoPath, ref) - if err != nil { - return nil, fmt.Errorf("opening git repo: %w", err) - } - - files, err := gr.FileTree(ctx, path) - if err != nil { - return nil, fmt.Errorf("reading file tree: %w", err) - } - - // if any of these files are a readme candidate, pass along its blob contents too - var readmeFileName string - var readmeContents string - for _, file := range files { - if markup.IsReadmeFile(file.Name) { - contents, err := gr.RawContent(filepath.Join(path, file.Name)) - if err != nil { - x.logger.Error("failed to read contents of file", "path", path, "file", file.Name) - } - - if utf8.Valid(contents) { - readmeFileName = file.Name - readmeContents = string(contents) - break - } - } - } - - // convert NiceTree -> tangled.RepoTempGetTree_TreeEntry - treeEntries := make([]*tangled.GitTempGetTree_TreeEntry, len(files)) - for i, file := range files { - entry := &tangled.GitTempGetTree_TreeEntry{ - Name: file.Name, - Mode: file.Mode, - Size: file.Size, - } - if file.LastCommit != nil { - entry.Last_commit = &tangled.GitTempGetTree_LastCommit{ - Hash: file.LastCommit.Hash.String(), - Message: file.LastCommit.Message, - When: file.LastCommit.When.Format(time.RFC3339), - } - } - treeEntries[i] = entry - } - - var parentPtr *string - if path != "" { - parentPtr = &path - } - - var dotdotPtr *string - if path != "" { - dotdot := filepath.Dir(path) - if dotdot != "." { - dotdotPtr = &dotdot - } - } - - return &tangled.GitTempGetTree_Output{ - Ref: ref, - Parent: parentPtr, - Dotdot: dotdotPtr, - Files: treeEntries, - Readme: &tangled.GitTempGetTree_Readme{ - Filename: readmeFileName, - Contents: readmeContents, - }, - }, nil -} diff --git a/knotmirror/xrpc/git_get_archive.go b/knotmirror/xrpc/git_get_archive.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_get_archive.go @@ -0,0 +1,106 @@ +package xrpc + +import ( + "compress/gzip" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing" + "tangled.org/core/api/tangled" + "tangled.org/core/knotmirror/db" + "tangled.org/core/knotserver/git" +) + +func (x *Xrpc) GetArchive(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + ref = r.URL.Query().Get("ref") + format = r.URL.Query().Get("format") + prefix = r.URL.Query().Get("prefix") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + if format != "tar.gz" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "only tar.gz format is supported"}) + return + } + if format == "" { + format = "tar.gz" + } + + l := x.logger.With("repo", repo, "ref", ref, "format", format, "prefix", prefix) + ctx := r.Context() + + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + l.Error("failed to resolve repo at-uri", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to resolve repo"}) + return + } + + gr, err := git.Open(repoPath, ref) + if err != nil { + l.Error("failed to open git repo", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to open git repo"}) + return + } + + repoName, err := func() (string, error) { + r, err := db.GetRepoByAtUri(ctx, x.db, repo) + if err != nil { + return "", err + } + if r == nil { + return "", fmt.Errorf("repo not found: %s", repo) + } + return r.Name, nil + }() + if err != nil { + l.Error("failed to get repo name", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to retrieve repo name"}) + return + } + + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") + immutableLink := func() string { + params := url.Values{} + params.Set("repo", repo.String()) + params.Set("ref", gr.Hash().String()) + params.Set("format", format) + params.Set("prefix", prefix) + return fmt.Sprintf("%s/xrpc/%s?%s", x.cfg.BaseUrl(), tangled.GitTempGetArchiveNSID, params.Encode()) + }() + + filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) + + gw := gzip.NewWriter(w) + defer gw.Close() + + if err := gr.WriteTar(gw, prefix); err != nil { + // once we start writing to the body we can't report error anymore + // so we are only left with logging the error + l.Error("writing tar file", "err", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if err := gw.Flush(); err != nil { + // once we start writing to the body we can't report error anymore + // so we are only left with logging the error + l.Error("flushing", "err", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/knotmirror/xrpc/git_get_blob.go b/knotmirror/xrpc/git_get_blob.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_get_blob.go @@ -0,0 +1,86 @@ +package xrpc + +import ( + "context" + "fmt" + "io" + "net/http" + "slices" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing/object" + "tangled.org/core/knotserver/git" +) + +func (x *Xrpc) GetBlob(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) + path = r.URL.Query().Get("path") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + l := x.logger.With("repo", repo, "ref", ref, "path", path) + + if path == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing path parameter"}) + return + } + + file, err := x.getFile(r.Context(), repo, ref, path) + if err != nil { + // TODO: better error return + l.Error("failed to get blob", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) + return + } + + reader, err := file.Reader() + if err != nil { + l.Error("failed to read blob", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) + return + } + defer reader.Close() + + w.Header().Set("Content-Type", "application/octet-stream") + if _, err := io.Copy(w, reader); err != nil { + l.Error("failed to serve the blob", "err", err) + } +} + +func (x *Xrpc) getFile(ctx context.Context, repo syntax.ATURI, ref, path string) (*object.File, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("resolving repo at-uri: %w", err) + } + + gr, err := git.Open(repoPath, ref) + if err != nil { + return nil, fmt.Errorf("opening git repo: %w", err) + } + + return gr.File(path) +} + +var textualMimeTypes = []string{ + "application/json", + "application/xml", + "application/yaml", + "application/x-yaml", + "application/toml", + "application/javascript", + "application/ecmascript", +} + +// isTextualMimeType returns true if the MIME type represents textual content +// that should be served as text/plain for security reasons +func isTextualMimeType(mimeType string) bool { + return slices.Contains(textualMimeTypes, mimeType) +} diff --git a/knotmirror/xrpc/git_get_branch.go b/knotmirror/xrpc/git_get_branch.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_get_branch.go @@ -0,0 +1,85 @@ +package xrpc + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" + "tangled.org/core/knotserver/git" +) + +// TODO: maybe rename to `sh.tangled.repo.temp.getCommit`? +// then, we should ensure the given `ref` is valid +func (x *Xrpc) GetBranch(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + nameQuery = r.URL.Query().Get("name") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + if nameQuery == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing name parameter"}) + return + } + branchName, _ := url.PathUnescape(nameQuery) + + l := x.logger.With("repo", repo, "branch", branchName) + + out, err := x.getBranch(r.Context(), repo, branchName) + if err != nil { + // TODO: better error return + l.Error("failed to get branch", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get branch"}) + return + } + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) getBranch(ctx context.Context, repo syntax.ATURI, branchName string) (*tangled.GitTempGetBranch_Output, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) + } + + gr, err := git.PlainOpen(repoPath) + if err != nil { + return nil, fmt.Errorf("failed to open git repo: %w", err) + } + + ref, err := gr.Branch(branchName) + if err != nil { + return nil, fmt.Errorf("getting branch '%s': %w", branchName, err) + } + + commit, err := gr.Commit(ref.Hash()) + if err != nil { + return nil, fmt.Errorf("getting commit '%s': %w", ref.Hash(), err) + } + + out := tangled.GitTempGetBranch_Output{ + Name: ref.Name().Short(), + Hash: ref.Hash().String(), + When: commit.Author.When.Format(time.RFC3339), + Author: &tangled.GitTempDefs_Signature{ + Name: commit.Author.Name, + Email: commit.Author.Email, + When: commit.Author.When.Format(time.RFC3339), + }, + } + + if commit.Message != "" { + out.Message = &commit.Message + } + + return &out, nil +} diff --git a/knotmirror/xrpc/git_get_tag.go b/knotmirror/xrpc/git_get_tag.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_get_tag.go @@ -0,0 +1,92 @@ +package xrpc + +import ( + "context" + "fmt" + "net/http" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "tangled.org/core/knotserver/git" + "tangled.org/core/types" +) + +func (x *Xrpc) GetTag(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + tagName = r.URL.Query().Get("tag") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + if tagName == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing 'tag' parameter"}) + return + } + + l := x.logger.With("repo", repo, "tag", tagName) + + out, err := x.getTag(r.Context(), repo, tagName) + if err != nil { + // TODO: better error return + l.Error("failed to get tag", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tag"}) + return + } + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) getTag(ctx context.Context, repo syntax.ATURI, tagName string) (*types.RepoTagResponse, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) + } + + gr, err := git.PlainOpen(repoPath) + if err != nil { + return nil, fmt.Errorf("failed to open git repo: %w", err) + } + + // if this is not already formatted as refs/tags/v0.1.0, then format it + if !plumbing.ReferenceName(tagName).IsTag() { + tagName = plumbing.NewTagReferenceName(tagName).String() + } + + tag, err := func() (object.Tag, error) { + tags, err := gr.Tags(&git.TagsOptions{ + Pattern: tagName, + }) + if err != nil { + return object.Tag{}, err + } + if len(tags) != 1 { + return object.Tag{}, fmt.Errorf("expected 1 tag to be returned, got %d tags", len(tags)) + } + return tags[0], nil + }() + if err != nil { + return nil, fmt.Errorf("getting tag: %w", err) + } + + var target *object.Tag + if tag.Target != plumbing.ZeroHash { + target = &tag + } + + return &types.RepoTagResponse{ + Tag: &types.TagReference{ + Tag: target, + Reference: types.Reference{ + Name: tag.Name, + Hash: tag.Hash.String(), + }, + Message: tag.Message, + }, + }, nil +} diff --git a/knotmirror/xrpc/git_get_tree.go b/knotmirror/xrpc/git_get_tree.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_get_tree.go @@ -0,0 +1,118 @@ +package xrpc + +import ( + "context" + "fmt" + "net/http" + "path/filepath" + "time" + "unicode/utf8" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" + "tangled.org/core/appview/pages/markup" + "tangled.org/core/knotserver/git" +) + +func (x *Xrpc) GetTree(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) + path = r.URL.Query().Get("path") // path can be empty (defaults to root) + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + l := x.logger.With("repo", repo, "ref", ref, "path", path) + + out, err := x.getTree(r.Context(), repo, ref, path) + if err != nil { + // TODO: better error return + l.Error("failed to get tree", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tree"}) + return + } + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) getTree(ctx context.Context, repo syntax.ATURI, ref, path string) (*tangled.GitTempGetTree_Output, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) + } + + gr, err := git.Open(repoPath, ref) + if err != nil { + return nil, fmt.Errorf("opening git repo: %w", err) + } + + files, err := gr.FileTree(ctx, path) + if err != nil { + return nil, fmt.Errorf("reading file tree: %w", err) + } + + // if any of these files are a readme candidate, pass along its blob contents too + var readmeFileName string + var readmeContents string + for _, file := range files { + if markup.IsReadmeFile(file.Name) { + contents, err := gr.RawContent(filepath.Join(path, file.Name)) + if err != nil { + x.logger.Error("failed to read contents of file", "path", path, "file", file.Name) + } + + if utf8.Valid(contents) { + readmeFileName = file.Name + readmeContents = string(contents) + break + } + } + } + + // convert NiceTree -> tangled.RepoTempGetTree_TreeEntry + treeEntries := make([]*tangled.GitTempGetTree_TreeEntry, len(files)) + for i, file := range files { + entry := &tangled.GitTempGetTree_TreeEntry{ + Name: file.Name, + Mode: file.Mode, + Size: file.Size, + } + if file.LastCommit != nil { + entry.Last_commit = &tangled.GitTempGetTree_LastCommit{ + Hash: file.LastCommit.Hash.String(), + Message: file.LastCommit.Message, + When: file.LastCommit.When.Format(time.RFC3339), + } + } + treeEntries[i] = entry + } + + var parentPtr *string + if path != "" { + parentPtr = &path + } + + var dotdotPtr *string + if path != "" { + dotdot := filepath.Dir(path) + if dotdot != "." { + dotdotPtr = &dotdot + } + } + + return &tangled.GitTempGetTree_Output{ + Ref: ref, + Parent: parentPtr, + Dotdot: dotdotPtr, + Files: treeEntries, + Readme: &tangled.GitTempGetTree_Readme{ + Filename: readmeFileName, + Contents: readmeContents, + }, + }, nil +} diff --git a/knotmirror/xrpc/git_listBranches.go b/knotmirror/xrpc/git_listBranches.go deleted file mode 100644 --- a/knotmirror/xrpc/git_listBranches.go +++ /dev/null @@ -1,95 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "net/http" - "path/filepath" - "strconv" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/knotserver/git" - "tangled.org/core/types" -) - -func (x *Xrpc) ListBranches(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - limitQuery = r.URL.Query().Get("limit") - cursorQuery = r.URL.Query().Get("cursor") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - limit := 50 - if limitQuery != "" { - limit, err = strconv.Atoi(limitQuery) - if err != nil || limit < 1 || limit > 1000 { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) - return - } - } - - var cursor int64 - if cursorQuery != "" { - cursor, err = strconv.ParseInt(cursorQuery, 10, 64) - if err != nil || cursor < 0 { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) - return - } - } - - l := x.logger.With("repo", repoQuery, "limit", limit, "cursor", cursor) - - out, err := x.listBranches(r.Context(), repo, limit, cursor) - if err != nil { - // TODO: better error return - l.Error("failed to list branches", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list branches"}) - return - } - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) listBranches(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoBranchesResponse, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("resolving repo at-uri: %w", err) - } - - gr, err := git.PlainOpen(repoPath) - if err != nil { - return nil, fmt.Errorf("opening git repo: %w", err) - } - - branches, err := gr.Branches(&git.BranchesOptions{ - Limit: limit, - Offset: int(cursor), - }) - if err != nil { - return nil, fmt.Errorf("listing git branches: %w", err) - } - - return &types.RepoBranchesResponse{ - // TODO: include default branch and cursor - Branches: branches, - }, nil -} - -func (x *Xrpc) makeRepoPath(ctx context.Context, repo syntax.ATURI) (string, error) { - id, err := x.resolver.ResolveIdent(ctx, repo.Authority().String()) - if err != nil { - return "", err - } - - return filepath.Join( - x.cfg.GitRepoBasePath, - id.DID.String(), - repo.RecordKey().String(), - ), nil -} diff --git a/knotmirror/xrpc/git_listCommits.go b/knotmirror/xrpc/git_listCommits.go deleted file mode 100644 --- a/knotmirror/xrpc/git_listCommits.go +++ /dev/null @@ -1,95 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/knotserver/git" - "tangled.org/core/types" -) - -func (x *Xrpc) ListCommits(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) - limitQuery = r.URL.Query().Get("limit") - cursorQuery = r.URL.Query().Get("cursor") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - limit := 50 - if limitQuery != "" { - limit, err = strconv.Atoi(limitQuery) - if err != nil || limit < 1 || limit > 1000 { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) - return - } - } - - var cursor int64 - if cursorQuery != "" { - cursor, err = strconv.ParseInt(cursorQuery, 10, 64) - if err != nil || cursor < 0 { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) - return - } - } - - l := x.logger.With("repo", repo, "ref", ref) - - out, err := x.listCommits(r.Context(), repo, ref, limit, cursor) - if err != nil { - // TODO: better error return - l.Error("failed to list commits", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list commits"}) - return - } - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) listCommits(ctx context.Context, repo syntax.ATURI, ref string, limit int, cursor int64) (*types.RepoLogResponse, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("resolving repo at-uri: %w", err) - } - - gr, err := git.Open(repoPath, ref) - if err != nil { - return nil, fmt.Errorf("opening git repo: %w", err) - } - - offset := int(cursor) - - commits, err := gr.Commits(offset, limit) - if err != nil { - return nil, fmt.Errorf("listing git commits: %w", err) - } - - tcommits := make([]types.Commit, len(commits)) - for i, c := range commits { - tcommits[i].FromGoGitCommit(c) - } - - total, err := gr.TotalCommits() - if err != nil { - return nil, fmt.Errorf("counting total commits: %w", err) - } - - return &types.RepoLogResponse{ - Commits: tcommits, - Ref: ref, - Page: (offset / limit) + 1, - PerPage: limit, - Total: total, - Log: true, - }, nil -} diff --git a/knotmirror/xrpc/git_listLanguages.go b/knotmirror/xrpc/git_listLanguages.go deleted file mode 100644 --- a/knotmirror/xrpc/git_listLanguages.go +++ /dev/null @@ -1,86 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "math" - "net/http" - "time" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/api/tangled" - "tangled.org/core/knotserver/git" -) - -func (x *Xrpc) ListLanguages(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - ref = r.URL.Query().Get("ref") - ) - l := x.logger.With("repo", repoQuery, "ref", ref) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - l.Error("invalid repo at-uri", "err", err) - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - out, err := x.listLanguages(r.Context(), repo, ref) - if err != nil { - l.Error("failed to list languages", "err", err) - writeErr(w, err) - return - } - - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) listLanguages(ctx context.Context, repo syntax.ATURI, ref string) (*tangled.GitTempListLanguages_Output, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("resolving repo at-uri: %w", err) - } - - gr, err := git.Open(repoPath, ref) - if err != nil { - return nil, &atclient.APIError{StatusCode: http.StatusNotFound, Name: "RepoNotFound", Message: "failed to find git repo"} - } - - ctx, cancel := context.WithTimeout(ctx, 1*time.Second) - defer cancel() - - sizes, err := gr.AnalyzeLanguages(ctx) - if err != nil { - return nil, fmt.Errorf("analyzing languages: %w", err) - } - - return &tangled.GitTempListLanguages_Output{ - Ref: ref, - Languages: sizesToLanguages(sizes), - }, nil -} - -func sizesToLanguages(sizes git.LangBreakdown) []*tangled.GitTempListLanguages_Language { - var apiLanguages []*tangled.GitTempListLanguages_Language - var totalSize int64 - for _, size := range sizes { - totalSize += size - } - - for name, size := range sizes { - percentagef64 := float64(size) / float64(totalSize) * 100 - percentage := math.Round(percentagef64) - - lang := &tangled.GitTempListLanguages_Language{ - Name: name, - Size: size, - Percentage: int64(percentage), - } - - apiLanguages = append(apiLanguages, lang) - } - - return apiLanguages -} diff --git a/knotmirror/xrpc/git_listTags.go b/knotmirror/xrpc/git_listTags.go deleted file mode 100644 --- a/knotmirror/xrpc/git_listTags.go +++ /dev/null @@ -1,98 +0,0 @@ -package xrpc - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "tangled.org/core/knotserver/git" - "tangled.org/core/types" -) - -func (x *Xrpc) ListTags(w http.ResponseWriter, r *http.Request) { - var ( - repoQuery = r.URL.Query().Get("repo") - limitQuery = r.URL.Query().Get("limit") - cursorQuery = r.URL.Query().Get("cursor") - ) - - repo, err := syntax.ParseATURI(repoQuery) - if err != nil || repo.RecordKey() == "" { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) - return - } - - limit := 50 - if limitQuery != "" { - limit, err = strconv.Atoi(limitQuery) - if err != nil || limit < 1 || limit > 1000 { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) - return - } - } - - var cursor int64 - if cursorQuery != "" { - cursor, err = strconv.ParseInt(cursorQuery, 10, 64) - if err != nil || cursor < 0 { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) - return - } - } - - l := x.logger.With("repo", repo, "limit", limit, "cursor", cursor) - - out, err := x.listTags(r.Context(), repo, limit, cursor) - if err != nil { - // TODO: better error return - l.Error("failed to list tags", "err", err) - writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list tags"}) - return - } - writeJson(w, http.StatusOK, out) -} - -func (x *Xrpc) listTags(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoTagsResponse, error) { - repoPath, err := x.makeRepoPath(ctx, repo) - if err != nil { - return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) - } - - gr, err := git.PlainOpen(repoPath) - if err != nil { - return nil, fmt.Errorf("failed to open git repo: %w", err) - } - - tags, err := gr.Tags(&git.TagsOptions{ - Limit: limit, - Offset: int(cursor), - }) - if err != nil { - return nil, fmt.Errorf("failed to get git tags: %w", err) - } - - rtags := make([]*types.TagReference, len(tags)) - for i, tag := range tags { - var target *object.Tag - if tag.Target != plumbing.ZeroHash { - target = &tag - } - rtags[i] = &types.TagReference{ - Reference: types.Reference{ - Name: tag.Name, - Hash: tag.Hash.String(), - }, - Tag: target, - Message: tag.Message, - } - } - - return &types.RepoTagsResponse{ - Tags: rtags, - }, nil -} diff --git a/knotmirror/xrpc/git_list_branches.go b/knotmirror/xrpc/git_list_branches.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_list_branches.go @@ -0,0 +1,95 @@ +package xrpc + +import ( + "context" + "fmt" + "net/http" + "path/filepath" + "strconv" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/knotserver/git" + "tangled.org/core/types" +) + +func (x *Xrpc) ListBranches(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + limitQuery = r.URL.Query().Get("limit") + cursorQuery = r.URL.Query().Get("cursor") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + limit := 50 + if limitQuery != "" { + limit, err = strconv.Atoi(limitQuery) + if err != nil || limit < 1 || limit > 1000 { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) + return + } + } + + var cursor int64 + if cursorQuery != "" { + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) + if err != nil || cursor < 0 { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) + return + } + } + + l := x.logger.With("repo", repoQuery, "limit", limit, "cursor", cursor) + + out, err := x.listBranches(r.Context(), repo, limit, cursor) + if err != nil { + // TODO: better error return + l.Error("failed to list branches", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list branches"}) + return + } + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) listBranches(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoBranchesResponse, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("resolving repo at-uri: %w", err) + } + + gr, err := git.PlainOpen(repoPath) + if err != nil { + return nil, fmt.Errorf("opening git repo: %w", err) + } + + branches, err := gr.Branches(&git.BranchesOptions{ + Limit: limit, + Offset: int(cursor), + }) + if err != nil { + return nil, fmt.Errorf("listing git branches: %w", err) + } + + return &types.RepoBranchesResponse{ + // TODO: include default branch and cursor + Branches: branches, + }, nil +} + +func (x *Xrpc) makeRepoPath(ctx context.Context, repo syntax.ATURI) (string, error) { + id, err := x.resolver.ResolveIdent(ctx, repo.Authority().String()) + if err != nil { + return "", err + } + + return filepath.Join( + x.cfg.GitRepoBasePath, + id.DID.String(), + repo.RecordKey().String(), + ), nil +} diff --git a/knotmirror/xrpc/git_list_commits.go b/knotmirror/xrpc/git_list_commits.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_list_commits.go @@ -0,0 +1,95 @@ +package xrpc + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/knotserver/git" + "tangled.org/core/types" +) + +func (x *Xrpc) ListCommits(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) + limitQuery = r.URL.Query().Get("limit") + cursorQuery = r.URL.Query().Get("cursor") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + limit := 50 + if limitQuery != "" { + limit, err = strconv.Atoi(limitQuery) + if err != nil || limit < 1 || limit > 1000 { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) + return + } + } + + var cursor int64 + if cursorQuery != "" { + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) + if err != nil || cursor < 0 { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) + return + } + } + + l := x.logger.With("repo", repo, "ref", ref) + + out, err := x.listCommits(r.Context(), repo, ref, limit, cursor) + if err != nil { + // TODO: better error return + l.Error("failed to list commits", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list commits"}) + return + } + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) listCommits(ctx context.Context, repo syntax.ATURI, ref string, limit int, cursor int64) (*types.RepoLogResponse, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("resolving repo at-uri: %w", err) + } + + gr, err := git.Open(repoPath, ref) + if err != nil { + return nil, fmt.Errorf("opening git repo: %w", err) + } + + offset := int(cursor) + + commits, err := gr.Commits(offset, limit) + if err != nil { + return nil, fmt.Errorf("listing git commits: %w", err) + } + + tcommits := make([]types.Commit, len(commits)) + for i, c := range commits { + tcommits[i].FromGoGitCommit(c) + } + + total, err := gr.TotalCommits() + if err != nil { + return nil, fmt.Errorf("counting total commits: %w", err) + } + + return &types.RepoLogResponse{ + Commits: tcommits, + Ref: ref, + Page: (offset / limit) + 1, + PerPage: limit, + Total: total, + Log: true, + }, nil +} diff --git a/knotmirror/xrpc/git_list_languages.go b/knotmirror/xrpc/git_list_languages.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_list_languages.go @@ -0,0 +1,86 @@ +package xrpc + +import ( + "context" + "fmt" + "math" + "net/http" + "time" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" + "tangled.org/core/knotserver/git" +) + +func (x *Xrpc) ListLanguages(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + ref = r.URL.Query().Get("ref") + ) + l := x.logger.With("repo", repoQuery, "ref", ref) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + l.Error("invalid repo at-uri", "err", err) + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + out, err := x.listLanguages(r.Context(), repo, ref) + if err != nil { + l.Error("failed to list languages", "err", err) + writeErr(w, err) + return + } + + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) listLanguages(ctx context.Context, repo syntax.ATURI, ref string) (*tangled.GitTempListLanguages_Output, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("resolving repo at-uri: %w", err) + } + + gr, err := git.Open(repoPath, ref) + if err != nil { + return nil, &atclient.APIError{StatusCode: http.StatusNotFound, Name: "RepoNotFound", Message: "failed to find git repo"} + } + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + sizes, err := gr.AnalyzeLanguages(ctx) + if err != nil { + return nil, fmt.Errorf("analyzing languages: %w", err) + } + + return &tangled.GitTempListLanguages_Output{ + Ref: ref, + Languages: sizesToLanguages(sizes), + }, nil +} + +func sizesToLanguages(sizes git.LangBreakdown) []*tangled.GitTempListLanguages_Language { + var apiLanguages []*tangled.GitTempListLanguages_Language + var totalSize int64 + for _, size := range sizes { + totalSize += size + } + + for name, size := range sizes { + percentagef64 := float64(size) / float64(totalSize) * 100 + percentage := math.Round(percentagef64) + + lang := &tangled.GitTempListLanguages_Language{ + Name: name, + Size: size, + Percentage: int64(percentage), + } + + apiLanguages = append(apiLanguages, lang) + } + + return apiLanguages +} diff --git a/knotmirror/xrpc/git_list_tags.go b/knotmirror/xrpc/git_list_tags.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/git_list_tags.go @@ -0,0 +1,98 @@ +package xrpc + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "tangled.org/core/knotserver/git" + "tangled.org/core/types" +) + +func (x *Xrpc) ListTags(w http.ResponseWriter, r *http.Request) { + var ( + repoQuery = r.URL.Query().Get("repo") + limitQuery = r.URL.Query().Get("limit") + cursorQuery = r.URL.Query().Get("cursor") + ) + + repo, err := syntax.ParseATURI(repoQuery) + if err != nil || repo.RecordKey() == "" { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) + return + } + + limit := 50 + if limitQuery != "" { + limit, err = strconv.Atoi(limitQuery) + if err != nil || limit < 1 || limit > 1000 { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) + return + } + } + + var cursor int64 + if cursorQuery != "" { + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) + if err != nil || cursor < 0 { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) + return + } + } + + l := x.logger.With("repo", repo, "limit", limit, "cursor", cursor) + + out, err := x.listTags(r.Context(), repo, limit, cursor) + if err != nil { + // TODO: better error return + l.Error("failed to list tags", "err", err) + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list tags"}) + return + } + writeJson(w, http.StatusOK, out) +} + +func (x *Xrpc) listTags(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoTagsResponse, error) { + repoPath, err := x.makeRepoPath(ctx, repo) + if err != nil { + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) + } + + gr, err := git.PlainOpen(repoPath) + if err != nil { + return nil, fmt.Errorf("failed to open git repo: %w", err) + } + + tags, err := gr.Tags(&git.TagsOptions{ + Limit: limit, + Offset: int(cursor), + }) + if err != nil { + return nil, fmt.Errorf("failed to get git tags: %w", err) + } + + rtags := make([]*types.TagReference, len(tags)) + for i, tag := range tags { + var target *object.Tag + if tag.Target != plumbing.ZeroHash { + target = &tag + } + rtags[i] = &types.TagReference{ + Reference: types.Reference{ + Name: tag.Name, + Hash: tag.Hash.String(), + }, + Tag: target, + Message: tag.Message, + } + } + + return &types.RepoTagsResponse{ + Tags: rtags, + }, nil +} diff --git a/knotmirror/xrpc/sync_requestCrawl.go b/knotmirror/xrpc/sync_requestCrawl.go deleted file mode 100644 --- a/knotmirror/xrpc/sync_requestCrawl.go +++ /dev/null @@ -1,104 +0,0 @@ -package xrpc - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/xrpc" - "tangled.org/core/api/tangled" - "tangled.org/core/knotmirror/db" - "tangled.org/core/knotmirror/hostutil" - "tangled.org/core/knotmirror/models" -) - -func (x *Xrpc) RequestCrawl(w http.ResponseWriter, r *http.Request) { - var input tangled.SyncRequestCrawl_Input - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"}) - return - } - - ctx := r.Context() - - l := x.logger.With("input", input) - - hostname, noSSL, err := hostutil.ParseHostname(input.Hostname) - if err != nil { - l.Error("invalid hostname", "err", err) - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("hostname field empty or invalid: %s", input.Hostname)}) - return - } - - // TODO: check if host is Knot with knot.describeServer - - // store given repoAt to db - // this will allow knotmirror to ingest repo creation event bypassing tap. - // this step won't be needed once we introduce did-for-repo - // TODO(boltless): remove this section - if input.EnsureRepo != nil { - repoAt, err := syntax.ParseATURI(*input.EnsureRepo) - if err != nil { - l.Error("invalid repo at-uri", "err", err) - writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", *input.EnsureRepo)}) - return - } - owner, err := x.resolver.ResolveIdent(ctx, repoAt.Authority().String()) - if err != nil || owner.Handle.IsInvalidHandle() { - l.Error("failed to resolve ident", "err", err, "owner", repoAt.Authority().String()) - writeErr(w, fmt.Errorf("failed to resolve repo owner")) - return - } - xrpcc := xrpc.Client{Host: owner.PDSEndpoint()} - out, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) - if err != nil { - l.Error("failed to get repo record", "err", err, "repo", repoAt) - writeErr(w, fmt.Errorf("failed to get repo record")) - return - } - record := out.Value.Val.(*tangled.Repo) - - knotUrl := record.Knot - if !strings.Contains(record.Knot, "://") { - if noSSL { - knotUrl = "http://" + knotUrl - } else { - knotUrl = "https://" + knotUrl - } - } - - repo := &models.Repo{ - Did: owner.DID, - Rkey: repoAt.RecordKey(), - Cid: (*syntax.CID)(out.Cid), - Name: record.Name, - KnotDomain: knotUrl, - State: models.RepoStatePending, - ErrorMsg: "", - RetryAfter: 0, - RetryCount: 0, - } - - if err := db.UpsertRepo(ctx, x.db, repo); err != nil { - l.Error("failed to upsert repo", "err", err) - writeErr(w, err) - return - } - } - - // subscribe to requested host - if !x.ks.CheckIfSubscribed(hostname) { - if err := x.ks.SubscribeHost(ctx, hostname, noSSL); err != nil { - // TODO(boltless): return HostBanned on banned hosts - l.Error("failed to subscribe host", "err", err) - writeErr(w, err) - return - } - } - - w.WriteHeader(http.StatusOK) -} diff --git a/knotmirror/xrpc/sync_request_crawl.go b/knotmirror/xrpc/sync_request_crawl.go new file mode 100644 --- /dev/null +++ b/knotmirror/xrpc/sync_request_crawl.go @@ -0,0 +1,104 @@ +package xrpc + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" + "tangled.org/core/api/tangled" + "tangled.org/core/knotmirror/db" + "tangled.org/core/knotmirror/hostutil" + "tangled.org/core/knotmirror/models" +) + +func (x *Xrpc) RequestCrawl(w http.ResponseWriter, r *http.Request) { + var input tangled.SyncRequestCrawl_Input + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"}) + return + } + + ctx := r.Context() + + l := x.logger.With("input", input) + + hostname, noSSL, err := hostutil.ParseHostname(input.Hostname) + if err != nil { + l.Error("invalid hostname", "err", err) + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("hostname field empty or invalid: %s", input.Hostname)}) + return + } + + // TODO: check if host is Knot with knot.describeServer + + // store given repoAt to db + // this will allow knotmirror to ingest repo creation event bypassing tap. + // this step won't be needed once we introduce did-for-repo + // TODO(boltless): remove this section + if input.EnsureRepo != nil { + repoAt, err := syntax.ParseATURI(*input.EnsureRepo) + if err != nil { + l.Error("invalid repo at-uri", "err", err) + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", *input.EnsureRepo)}) + return + } + owner, err := x.resolver.ResolveIdent(ctx, repoAt.Authority().String()) + if err != nil || owner.Handle.IsInvalidHandle() { + l.Error("failed to resolve ident", "err", err, "owner", repoAt.Authority().String()) + writeErr(w, fmt.Errorf("failed to resolve repo owner")) + return + } + xrpcc := xrpc.Client{Host: owner.PDSEndpoint()} + out, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) + if err != nil { + l.Error("failed to get repo record", "err", err, "repo", repoAt) + writeErr(w, fmt.Errorf("failed to get repo record")) + return + } + record := out.Value.Val.(*tangled.Repo) + + knotUrl := record.Knot + if !strings.Contains(record.Knot, "://") { + if noSSL { + knotUrl = "http://" + knotUrl + } else { + knotUrl = "https://" + knotUrl + } + } + + repo := &models.Repo{ + Did: owner.DID, + Rkey: repoAt.RecordKey(), + Cid: (*syntax.CID)(out.Cid), + Name: record.Name, + KnotDomain: knotUrl, + State: models.RepoStatePending, + ErrorMsg: "", + RetryAfter: 0, + RetryCount: 0, + } + + if err := db.UpsertRepo(ctx, x.db, repo); err != nil { + l.Error("failed to upsert repo", "err", err) + writeErr(w, err) + return + } + } + + // subscribe to requested host + if !x.ks.CheckIfSubscribed(hostname) { + if err := x.ks.SubscribeHost(ctx, hostname, noSSL); err != nil { + // TODO(boltless): return HostBanned on banned hosts + l.Error("failed to subscribe host", "err", err) + writeErr(w, err) + return + } + } + + w.WriteHeader(http.StatusOK) +} diff --git a/spindle/xrpc/pipeline_cancelPipeline.go b/spindle/xrpc/pipeline_cancelPipeline.go deleted file mode 100644 --- a/spindle/xrpc/pipeline_cancelPipeline.go +++ /dev/null @@ -1,97 +0,0 @@ -package xrpc - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/xrpc" - securejoin "github.com/cyphar/filepath-securejoin" - "tangled.org/core/api/tangled" - "tangled.org/core/rbac" - "tangled.org/core/spindle/models" - xrpcerr "tangled.org/core/xrpc/errors" -) - -func (x *Xrpc) CancelPipeline(w http.ResponseWriter, r *http.Request) { - l := x.Logger - fail := func(e xrpcerr.XrpcError) { - l.Error("failed", "kind", e.Tag, "error", e.Message) - writeError(w, e, http.StatusBadRequest) - } - l.Debug("cancel pipeline") - - actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) - if !ok { - fail(xrpcerr.MissingActorDidError) - return - } - - var input tangled.PipelineCancelPipeline_Input - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - fail(xrpcerr.GenericError(err)) - return - } - - aturi := syntax.ATURI(input.Pipeline) - wid := models.WorkflowId{ - PipelineId: models.PipelineId{ - Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"), - Rkey: aturi.RecordKey().String(), - }, - Name: input.Workflow, - } - l.Debug("cancel pipeline", "wid", wid) - - // unfortunately we have to resolve repo-at here - repoAt, err := syntax.ParseATURI(input.Repo) - if err != nil { - fail(xrpcerr.InvalidRepoError(input.Repo)) - return - } - - ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) - if err != nil || ident.Handle.IsInvalidHandle() { - fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) - return - } - - xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} - resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) - if err != nil { - fail(xrpcerr.GenericError(err)) - return - } - - repo := resp.Value.Val.(*tangled.Repo) - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) - if err != nil { - fail(xrpcerr.GenericError(err)) - return - } - - // TODO: fine-grained role based control - isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo) - if err != nil || !isRepoOwner { - fail(xrpcerr.AccessControlError(actorDid.String())) - return - } - for _, engine := range x.Engines { - l.Debug("destorying workflow", "wid", wid) - err = engine.DestroyWorkflow(r.Context(), wid) - if err != nil { - fail(xrpcerr.GenericError(fmt.Errorf("failed to destroy workflow: %w", err))) - return - } - err = x.Db.StatusCancelled(wid, "User canceled the workflow", -1, x.Notifier) - if err != nil { - fail(xrpcerr.GenericError(fmt.Errorf("failed to emit status failed: %w", err))) - return - } - } - - w.WriteHeader(http.StatusOK) -} diff --git a/spindle/xrpc/pipeline_cancel_pipeline.go b/spindle/xrpc/pipeline_cancel_pipeline.go new file mode 100644 --- /dev/null +++ b/spindle/xrpc/pipeline_cancel_pipeline.go @@ -0,0 +1,97 @@ +package xrpc + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" + securejoin "github.com/cyphar/filepath-securejoin" + "tangled.org/core/api/tangled" + "tangled.org/core/rbac" + "tangled.org/core/spindle/models" + xrpcerr "tangled.org/core/xrpc/errors" +) + +func (x *Xrpc) CancelPipeline(w http.ResponseWriter, r *http.Request) { + l := x.Logger + fail := func(e xrpcerr.XrpcError) { + l.Error("failed", "kind", e.Tag, "error", e.Message) + writeError(w, e, http.StatusBadRequest) + } + l.Debug("cancel pipeline") + + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) + if !ok { + fail(xrpcerr.MissingActorDidError) + return + } + + var input tangled.PipelineCancelPipeline_Input + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + aturi := syntax.ATURI(input.Pipeline) + wid := models.WorkflowId{ + PipelineId: models.PipelineId{ + Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"), + Rkey: aturi.RecordKey().String(), + }, + Name: input.Workflow, + } + l.Debug("cancel pipeline", "wid", wid) + + // unfortunately we have to resolve repo-at here + repoAt, err := syntax.ParseATURI(input.Repo) + if err != nil { + fail(xrpcerr.InvalidRepoError(input.Repo)) + return + } + + ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) + if err != nil || ident.Handle.IsInvalidHandle() { + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) + return + } + + xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} + resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) + if err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + repo := resp.Value.Val.(*tangled.Repo) + didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) + if err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + // TODO: fine-grained role based control + isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo) + if err != nil || !isRepoOwner { + fail(xrpcerr.AccessControlError(actorDid.String())) + return + } + for _, engine := range x.Engines { + l.Debug("destorying workflow", "wid", wid) + err = engine.DestroyWorkflow(r.Context(), wid) + if err != nil { + fail(xrpcerr.GenericError(fmt.Errorf("failed to destroy workflow: %w", err))) + return + } + err = x.Db.StatusCancelled(wid, "User canceled the workflow", -1, x.Notifier) + if err != nil { + fail(xrpcerr.GenericError(fmt.Errorf("failed to emit status failed: %w", err))) + return + } + } + + w.WriteHeader(http.StatusOK) +} -- tangled.sh