diff --git a/api/repos.go b/api/repos.go index 96dd8e9b..3050f028 100644 --- a/api/repos.go +++ b/api/repos.go @@ -4,10 +4,13 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/smira/aptly/aptly" + "github.com/smira/aptly/database" "github.com/smira/aptly/deb" + "github.com/smira/aptly/query" "github.com/smira/aptly/utils" "os" "path/filepath" + "sort" ) // GET /api/repos @@ -175,17 +178,128 @@ func apiReposPackagesShow(c *gin.Context) { return } - c.JSON(200, repo.RefList().Strings()) + queryS := c.Request.URL.Query().Get("q") + if queryS != "" { + q, err := query.Parse(queryS) + if err != nil { + c.Fail(400, err) + return + } + + list, err := deb.NewPackageListFromRefList(repo.RefList(), context.CollectionFactory().PackageCollection(), nil) + if err != nil { + c.Fail(500, err) + return + } + + list.PrepareIndex() + + withDeps := c.Request.URL.Query().Get("withDeps") == "1" + architecturesList := []string{} + + if withDeps { + if len(context.ArchitecturesList()) > 0 { + architecturesList = context.ArchitecturesList() + } else { + architecturesList = list.Architectures(false) + } + + sort.Strings(architecturesList) + + if len(architecturesList) == 0 { + c.Fail(400, fmt.Errorf("unable to determine list of architectures, please specify explicitly")) + return + } + } + + result, err := list.Filter([]deb.PackageQuery{q}, withDeps, + nil, context.DependencyOptions(), architecturesList) + if err != nil { + c.Fail(500, err) + return + } + + c.JSON(200, result.Strings()) + } else { + c.JSON(200, repo.RefList().Strings()) + } +} + +// Handler for both add and delete +func apiReposPackagesAddDelete(c *gin.Context, cb func(list *deb.PackageList, p *deb.Package) error) { + var b struct { + PackageRefs []string + } + + if !c.Bind(&b) { + return + } + + collection := context.CollectionFactory().LocalRepoCollection() + collection.Lock() + defer collection.Unlock() + + repo, err := collection.ByName(c.Params.ByName("name")) + if err != nil { + c.Fail(404, err) + return + } + + err = collection.LoadComplete(repo) + if err != nil { + c.Fail(500, err) + return + } + + list, err := deb.NewPackageListFromRefList(repo.RefList(), context.CollectionFactory().PackageCollection(), nil) + if err != nil { + c.Fail(500, err) + return + } + + // verify package refs and build package list + for _, ref := range b.PackageRefs { + p, err := context.CollectionFactory().PackageCollection().ByKey([]byte(ref)) + if err != nil { + if err == database.ErrNotFound { + c.Fail(404, fmt.Errorf("package %s: %s", ref, err)) + } else { + c.Fail(500, err) + } + return + } + err = cb(list, p) + if err != nil { + c.Fail(400, err) + return + } + } + + repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list)) + + err = context.CollectionFactory().LocalRepoCollection().Update(repo) + if err != nil { + c.Fail(500, fmt.Errorf("unable to save: %s", err)) + return + } + + c.JSON(200, repo) + } // POST /repos/:name/packages func apiReposPackagesAdd(c *gin.Context) { - c.JSON(400, gin.H{}) + apiReposPackagesAddDelete(c, func(list *deb.PackageList, p *deb.Package) error { + return list.Add(p) + }) } // DELETE /repos/:name/packages func apiReposPackagesDelete(c *gin.Context) { - c.JSON(400, gin.H{}) + apiReposPackagesAddDelete(c, func(list *deb.PackageList, p *deb.Package) error { + list.Remove(p) + return nil + }) } // POST /repos/:name/file/:dir/:file @@ -219,6 +333,12 @@ func apiReposPackageFromDir(c *gin.Context) { return } + err = collection.LoadComplete(repo) + if err != nil { + c.Fail(500, err) + return + } + verifier := &utils.GpgVerifier{} var ( diff --git a/system/api_lib.py b/system/api_lib.py index fbbee77f..f9646c7c 100644 --- a/system/api_lib.py +++ b/system/api_lib.py @@ -48,6 +48,11 @@ class APITest(BaseTest): return requests.post("http://%s%s" % (self.base_url, uri), *args, **kwargs) def delete(self, uri, *args, **kwargs): + if "json" in kwargs: + kwargs["data"] = json.dumps(kwargs.pop("json")) + if not "headers" in kwargs: + kwargs["headers"] = {} + kwargs["headers"]["Content-Type"] = "application/json" return requests.delete("http://%s%s" % (self.base_url, uri), *args, **kwargs) def upload(self, uri, *filenames, **kwargs): diff --git a/system/t12_api/repos.py b/system/t12_api/repos.py index 2f66fbc3..678c5073 100644 --- a/system/t12_api/repos.py +++ b/system/t12_api/repos.py @@ -139,3 +139,119 @@ class ReposAPITestAddFile(APITest): ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378']) self.check_not_exists("upload/" + d) + + +class ReposAPITestShowQuery(APITest): + """ + GET /api/repos/:name/packages?q=query + """ + def check(self): + repo_name = self.random_name() + + self.check_equal(self.post("/api/repos", json={"Name": repo_name, "Comment": "fun repo"}).status_code, 201) + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb", "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + self.check_equal(self.post("/api/repos/" + repo_name + "/file/" + d).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages", params={"q": "pyspi"}).json()), + ['Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e', 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89']) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages", params={"q": "Version (> 0.6.1-1.4)"}).json()), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378', 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89']) + + resp = self.get("/api/repos/" + repo_name + "/packages", params={"q": "pyspi)"}) + self.check_equal(resp.status_code, 400) + self.check_equal(resp.json()[0]["error"], u'parsing failed: unexpected token ): expecting end of query') + + +class ReposAPITestAddMultiple(APITest): + """ + POST /api/repos/:name/file/:dir/:file multiple + """ + def check(self): + repo_name = self.random_name() + + self.check_equal(self.post("/api/repos", json={"Name": repo_name, "Comment": "fun repo"}).status_code, 201) + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb", "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + + self.check_equal(self.post("/api/repos/" + repo_name + "/file/" + d + "/pyspi_0.6.1-1.3.dsc", + params={"noRemove": 1}).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages").json()), + ['Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e']) + + self.check_equal(self.post("/api/repos/" + repo_name + "/file/" + d + "/pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages").json()), + ['Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e', 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89']) + + +class ReposAPITestPackagesAddDelete(APITest): + """ + POST/DELETE /api/repos/:name/packages + """ + def check(self): + repo_name = self.random_name() + + self.check_equal(self.post("/api/repos", json={"Name": repo_name, "Comment": "fun repo"}).status_code, 201) + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb", "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + + self.check_equal(self.post("/api/repos/" + repo_name + "/file/" + d).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages").json()), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378', + 'Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e', + 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89']) + + self.check_equal(self.post("/api/repos/" + repo_name + "/packages/", + json={"PackageRefs": ['Psource pyspi 0.6.1-1.4 f8f1daa806004e89']}).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages").json()), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378', + 'Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e', + 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89']) + + self.check_equal(self.post("/api/repos/" + repo_name + "/packages/", + json={"PackageRefs": ['Psource pyspi 0.6.1-1.4 f8f1daa806004e89', + 'Psource no-such-package 0.6.1-1.4 f8f1daa806004e89']}).status_code, 404) + + self.check_equal(self.delete("/api/repos/" + repo_name + "/packages/", + json={"PackageRefs": ['Psource pyspi 0.6.1-1.4 f8f1daa806004e89']}).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages").json()), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378', + 'Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e']) + + self.check_equal(self.post("/api/repos/" + repo_name + "/packages/", + json={"PackageRefs": ['Psource pyspi 0.6.1-1.4 f8f1daa806004e89']}).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name + "/packages").json()), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378', + 'Psource pyspi 0.6.1-1.3 3a8b37cbd9a3559e', + 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89']) + + repo_name2 = self.random_name() + + self.check_equal(self.post("/api/repos", json={"Name": repo_name2, "Comment": "fun repo"}).status_code, 201) + + self.check_equal(self.post("/api/repos/" + repo_name2 + "/packages/", + json={"PackageRefs": ['Psource pyspi 0.6.1-1.4 f8f1daa806004e89', + 'Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378']}).status_code, 200) + + self.check_equal(sorted(self.get("/api/repos/" + repo_name2 + "/packages").json()), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378', + 'Psource pyspi 0.6.1-1.4 f8f1daa806004e89'])