diff --git a/api/graph.go b/api/graph.go new file mode 100644 index 00000000..66ed4444 --- /dev/null +++ b/api/graph.go @@ -0,0 +1,75 @@ +package api + +import ( + "bytes" + "fmt" + "github.com/gin-gonic/gin" + "github.com/smira/aptly/deb" + "io" + "mime" + "os" + "os/exec" +) + +// GET /api/graph.:ext +func apiGraph(c *gin.Context) { + var ( + err error + output []byte + ) + + ext := c.Params.ByName("ext") + + factory := context.CollectionFactory() + + factory.RemoteRepoCollection().RLock() + defer factory.RemoteRepoCollection().RUnlock() + factory.LocalRepoCollection().RLock() + defer factory.LocalRepoCollection().RUnlock() + factory.SnapshotCollection().RLock() + defer factory.SnapshotCollection().RUnlock() + factory.PublishedRepoCollection().RLock() + defer factory.PublishedRepoCollection().RUnlock() + + graph, err := deb.BuildGraph(factory) + if err != nil { + c.JSON(500, err) + return + } + + buf := bytes.NewBufferString(graph.String()) + + command := exec.Command("dot", "-T"+ext) + command.Stderr = os.Stderr + + stdin, err := command.StdinPipe() + if err != nil { + c.Fail(500, err) + return + } + + _, err = io.Copy(stdin, buf) + if err != nil { + c.Fail(500, err) + return + } + + err = stdin.Close() + if err != nil { + c.Fail(500, err) + return + } + + output, err = command.Output() + if err != nil { + c.Fail(500, fmt.Errorf("unable to execute dot: %s (is graphviz package installed?)", err)) + return + } + + mimeType := mime.TypeByExtension("." + ext) + if mimeType == "" { + mimeType = "application/octet-stream" + } + + c.Data(200, mimeType, output) +} diff --git a/api/router.go b/api/router.go index a34bdd10..1b378b8b 100644 --- a/api/router.go +++ b/api/router.go @@ -52,5 +52,9 @@ func Router(c *ctx.AptlyContext) http.Handler { root.DELETE("/publish/:prefix/:distribution", apiPublishDrop) } + { + root.GET("/graph.:ext", apiGraph) + } + return router } diff --git a/cmd/graph.go b/cmd/graph.go index 28bbc969..552066da 100644 --- a/cmd/graph.go +++ b/cmd/graph.go @@ -2,7 +2,6 @@ package cmd import ( "bytes" - "code.google.com/p/gographviz" "fmt" "github.com/smira/aptly/deb" "github.com/smira/commander" @@ -10,7 +9,6 @@ import ( "io/ioutil" "os" "os/exec" - "strings" ) func aptlyGraph(cmd *commander.Command, args []string) error { @@ -21,121 +19,11 @@ func aptlyGraph(cmd *commander.Command, args []string) error { return commander.ErrCommandError } - graph := gographviz.NewEscape() - graph.SetDir(true) - graph.SetName("aptly") - - existingNodes := map[string]bool{} - - fmt.Printf("Loading mirrors...\n") - - err = context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error { - err := context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo) - if err != nil { - return err - } - - graph.AddNode("aptly", repo.UUID, map[string]string{ - "shape": "Mrecord", - "style": "filled", - "fillcolor": "darkgoldenrod1", - "label": fmt.Sprintf("{Mirror %s|url: %s|dist: %s|comp: %s|arch: %s|pkgs: %d}", - repo.Name, repo.ArchiveRoot, repo.Distribution, strings.Join(repo.Components, ", "), - strings.Join(repo.Architectures, ", "), repo.NumPackages()), - }) - existingNodes[repo.UUID] = true - return nil - }) - - if err != nil { - return err - } - - fmt.Printf("Loading local repos...\n") - - err = context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error { - err := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo) - if err != nil { - return err - } - - graph.AddNode("aptly", repo.UUID, map[string]string{ - "shape": "Mrecord", - "style": "filled", - "fillcolor": "mediumseagreen", - "label": fmt.Sprintf("{Repo %s|comment: %s|pkgs: %d}", - repo.Name, repo.Comment, repo.NumPackages()), - }) - existingNodes[repo.UUID] = true - return nil - }) - - if err != nil { - return err - } - - fmt.Printf("Loading snapshots...\n") - - context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error { - existingNodes[snapshot.UUID] = true - return nil - }) - - err = context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error { - err := context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot) - if err != nil { - return err - } - - description := snapshot.Description - if snapshot.SourceKind == "repo" { - description = "Snapshot from repo" - } - - graph.AddNode("aptly", snapshot.UUID, map[string]string{ - "shape": "Mrecord", - "style": "filled", - "fillcolor": "cadetblue1", - "label": fmt.Sprintf("{Snapshot %s|%s|pkgs: %d}", snapshot.Name, description, snapshot.NumPackages()), - }) - - if snapshot.SourceKind == "repo" || snapshot.SourceKind == "local" || snapshot.SourceKind == "snapshot" { - for _, uuid := range snapshot.SourceIDs { - _, exists := existingNodes[uuid] - if exists { - graph.AddEdge(uuid, snapshot.UUID, true, nil) - } - } - } - return nil - }) - - if err != nil { - return err - } - - fmt.Printf("Loading published repos...\n") - - context.CollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error { - graph.AddNode("aptly", repo.UUID, map[string]string{ - "shape": "Mrecord", - "style": "filled", - "fillcolor": "darkolivegreen1", - "label": fmt.Sprintf("{Published %s/%s|comp: %s|arch: %s}", repo.Prefix, repo.Distribution, - strings.Join(repo.Components(), " "), strings.Join(repo.Architectures, ", ")), - }) - - for _, uuid := range repo.Sources { - _, exists := existingNodes[uuid] - if exists { - graph.AddEdge(uuid, repo.UUID, true, nil) - } - } - - return nil - }) - fmt.Printf("Generating graph...\n") + graph, err := deb.BuildGraph(context.CollectionFactory()) + if err != nil { + return err + } buf := bytes.NewBufferString(graph.String()) diff --git a/deb/graph.go b/deb/graph.go new file mode 100644 index 00000000..108b6d95 --- /dev/null +++ b/deb/graph.go @@ -0,0 +1,119 @@ +package deb + +import ( + "code.google.com/p/gographviz" + "fmt" + "strings" +) + +func BuildGraph(collectionFactory *CollectionFactory) (gographviz.Interface, error) { + var err error + + graph := gographviz.NewEscape() + graph.SetDir(true) + graph.SetName("aptly") + + existingNodes := map[string]bool{} + + err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *RemoteRepo) error { + err := collectionFactory.RemoteRepoCollection().LoadComplete(repo) + if err != nil { + return err + } + + graph.AddNode("aptly", repo.UUID, map[string]string{ + "shape": "Mrecord", + "style": "filled", + "fillcolor": "darkgoldenrod1", + "label": fmt.Sprintf("{Mirror %s|url: %s|dist: %s|comp: %s|arch: %s|pkgs: %d}", + repo.Name, repo.ArchiveRoot, repo.Distribution, strings.Join(repo.Components, ", "), + strings.Join(repo.Architectures, ", "), repo.NumPackages()), + }) + existingNodes[repo.UUID] = true + return nil + }) + + if err != nil { + return nil, err + } + + err = collectionFactory.LocalRepoCollection().ForEach(func(repo *LocalRepo) error { + err := collectionFactory.LocalRepoCollection().LoadComplete(repo) + if err != nil { + return err + } + + graph.AddNode("aptly", repo.UUID, map[string]string{ + "shape": "Mrecord", + "style": "filled", + "fillcolor": "mediumseagreen", + "label": fmt.Sprintf("{Repo %s|comment: %s|pkgs: %d}", + repo.Name, repo.Comment, repo.NumPackages()), + }) + existingNodes[repo.UUID] = true + return nil + }) + + if err != nil { + return nil, err + } + + collectionFactory.SnapshotCollection().ForEach(func(snapshot *Snapshot) error { + existingNodes[snapshot.UUID] = true + return nil + }) + + err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *Snapshot) error { + err := collectionFactory.SnapshotCollection().LoadComplete(snapshot) + if err != nil { + return err + } + + description := snapshot.Description + if snapshot.SourceKind == "repo" { + description = "Snapshot from repo" + } + + graph.AddNode("aptly", snapshot.UUID, map[string]string{ + "shape": "Mrecord", + "style": "filled", + "fillcolor": "cadetblue1", + "label": fmt.Sprintf("{Snapshot %s|%s|pkgs: %d}", snapshot.Name, description, snapshot.NumPackages()), + }) + + if snapshot.SourceKind == "repo" || snapshot.SourceKind == "local" || snapshot.SourceKind == "snapshot" { + for _, uuid := range snapshot.SourceIDs { + _, exists := existingNodes[uuid] + if exists { + graph.AddEdge(uuid, snapshot.UUID, true, nil) + } + } + } + return nil + }) + + if err != nil { + return nil, err + } + + collectionFactory.PublishedRepoCollection().ForEach(func(repo *PublishedRepo) error { + graph.AddNode("aptly", repo.UUID, map[string]string{ + "shape": "Mrecord", + "style": "filled", + "fillcolor": "darkolivegreen1", + "label": fmt.Sprintf("{Published %s/%s|comp: %s|arch: %s}", repo.Prefix, repo.Distribution, + strings.Join(repo.Components(), " "), strings.Join(repo.Architectures, ", ")), + }) + + for _, uuid := range repo.Sources { + _, exists := existingNodes[uuid] + if exists { + graph.AddEdge(uuid, repo.UUID, true, nil) + } + } + + return nil + }) + + return graph, nil +} diff --git a/system/t12_api/__init__.py b/system/t12_api/__init__.py index b09d27a0..c8ba5645 100644 --- a/system/t12_api/__init__.py +++ b/system/t12_api/__init__.py @@ -5,4 +5,5 @@ Testing aptly REST API from .repos import * from .files import * from .publish import * -from .version import * \ No newline at end of file +from .version import * +from .graph import * diff --git a/system/t12_api/graph.py b/system/t12_api/graph.py new file mode 100644 index 00000000..2f2a9f10 --- /dev/null +++ b/system/t12_api/graph.py @@ -0,0 +1,17 @@ +from api_lib import APITest + + +class GraphAPITest(APITest): + """ + GET /graph.:ext + """ + + def check(self): + resp = self.get("/api/graph.png") + self.check_equal(resp.headers["Content-Type"], "image/png") + self.check_equal(resp.content[:4], '\x89PNG') + + self.check_equal(self.post("/api/repos", json={"Name": "xyz", "Comment": "fun repo"}).status_code, 201) + resp = self.get("/api/graph.svg") + self.check_equal(resp.headers["Content-Type"], "image/svg+xml") + self.check_equal(resp.content[:4], '