diff --git a/api/api_test.go b/api/api_test.go index 27e1cb5f0..8c62b31df 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -3,6 +3,7 @@ package api import ( "bytes" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -38,7 +39,7 @@ func createTestConfig() *os.File { return nil } jsonString, err := json.Marshal(gin.H{ - "architectures": []string{}, + "architectures": []string{}, "enableMetricsEndpoint": true, }) if err != nil { @@ -48,10 +49,12 @@ func createTestConfig() *os.File { return file } -func (s *ApiSuite) SetUpSuite(c *C) { +func (s *ApiSuite) setupContext() error { aptly.Version = "testVersion" file := createTestConfig() - c.Assert(file, NotNil) + if nil == file { + return fmt.Errorf("unable to create the test configuration file") + } s.configFile = file flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError) @@ -62,10 +65,19 @@ func (s *ApiSuite) SetUpSuite(c *C) { s.flags = flags context, err := ctx.NewContext(s.flags) - c.Assert(err, IsNil) + if nil != err { + return err + } s.context = context s.router = Router(context) + + return nil +} + +func (s *ApiSuite) SetUpSuite(c *C) { + err := s.setupContext() + c.Assert(err, IsNil) } func (s *ApiSuite) TearDownSuite(c *C) { @@ -98,7 +110,7 @@ func (s *ApiSuite) TestGetVersion(c *C) { response, err := s.HTTPRequest("GET", "/api/version", nil) c.Assert(err, IsNil) c.Check(response.Code, Equals, 200) - c.Check(response.Body.String(), Matches, "{\"Version\":\"" + aptly.Version + "\"}") + c.Check(response.Body.String(), Matches, "{\"Version\":\""+aptly.Version+"\"}") } func (s *ApiSuite) TestGetReadiness(c *C) { diff --git a/api/repos.go b/api/repos.go index 6f7d435cd..fac990a5e 100644 --- a/api/repos.go +++ b/api/repos.go @@ -5,6 +5,7 @@ import ( "net/http" "os" "path/filepath" + "sort" "strings" "text/template" @@ -414,6 +415,145 @@ func apiReposPackageFromDir(c *gin.Context) { }) } +// POST /repos/:name/copy/:src/:file +func apiReposCopyPackage(c *gin.Context) { + dstRepoName := c.Params.ByName("name") + srcRepoName := c.Params.ByName("src") + + jsonBody := struct { + WithDeps bool `json:"with-deps,omitempty"` + DryRun bool `json:"dry-run,omitempty"` + }{ + WithDeps: false, + DryRun: false, + } + + err := c.Bind(&jsonBody) + if err != nil { + return + } + + collectionFactory := context.NewCollectionFactory() + dstRepo, err := collectionFactory.LocalRepoCollection().ByName(dstRepoName) + if err != nil { + AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("dest repo error: %s", err)) + return + } + + err = collectionFactory.LocalRepoCollection().LoadComplete(dstRepo) + if err != nil { + AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("dest repo error: %s", err)) + return + } + + var ( + srcRefList *deb.PackageRefList + srcRepo *deb.LocalRepo + ) + + srcRepo, err = collectionFactory.LocalRepoCollection().ByName(srcRepoName) + if err != nil { + AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("src repo error: %s", err)) + return + } + + if srcRepo.UUID == dstRepo.UUID { + AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("dest and source are identical")) + return + } + + err = collectionFactory.LocalRepoCollection().LoadComplete(srcRepo) + if err != nil { + AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("src repo error: %s", err)) + return + } + + srcRefList = srcRepo.RefList() + taskName := fmt.Sprintf("Copy packages from repo %s to repo %s", srcRepoName, dstRepoName) + resources := []string{string(dstRepo.Key()), string(srcRepo.Key())} + + maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { + reporter := &aptly.RecordingResultReporter{ + Warnings: []string{}, + AddedLines: []string{}, + RemovedLines: []string{}, + } + + dstList, err := deb.NewPackageListFromRefList(dstRepo.RefList(), collectionFactory.PackageCollection(), context.Progress()) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages in dest: %s", err) + + } + + srcList, err := deb.NewPackageListFromRefList(srcRefList, collectionFactory.PackageCollection(), context.Progress()) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages in src: %s", err) + } + + srcList.PrepareIndex() + + var architecturesList []string + + if jsonBody.WithDeps { + dstList.PrepareIndex() + + // Calculate architectures + if len(context.ArchitecturesList()) > 0 { + architecturesList = context.ArchitecturesList() + } else { + architecturesList = dstList.Architectures(false) + } + + sort.Strings(architecturesList) + + if len(architecturesList) == 0 { + return &task.ProcessReturnValue{Code: http.StatusUnprocessableEntity, Value: nil}, fmt.Errorf("unable to determine list of architectures, please specify explicitly") + } + } + + // srcList.Filter|FilterWithProgress only accept query list + queries := make([]deb.PackageQuery, 1) + queries[0], err = query.Parse(c.Params.ByName("file")) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusUnprocessableEntity, Value: nil}, fmt.Errorf("unable to parse query: %s", err) + } + + toProcess, err := srcList.FilterWithProgress(queries, jsonBody.WithDeps, dstList, context.DependencyOptions(), architecturesList, context.Progress()) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("filter error: %s", err) + } + + err = toProcess.ForEach(func(p *deb.Package) error { + err = dstList.Add(p) + if err != nil { + return err + } + + name := fmt.Sprintf("added %s-%s(%s)", p.Name, p.Version, p.Architecture) + reporter.AddedLines = append(reporter.AddedLines, name) + return nil + }) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("error processing dest add: %s", err) + } + + if jsonBody.DryRun { + reporter.Warning("Changes not saved, as dry run has been requested") + } else { + dstRepo.UpdateRefList(deb.NewPackageRefListFromPackageList(dstList)) + + err = collectionFactory.LocalRepoCollection().Update(dstRepo) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err) + } + } + + return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{ + "Report": reporter, + }}, nil + }) +} + // POST /repos/:name/include/:dir/:file func apiReposIncludePackageFromFile(c *gin.Context) { // redirect all work to dir method diff --git a/api/router.go b/api/router.go index ddd30e976..e82da3b64 100644 --- a/api/router.go +++ b/api/router.go @@ -116,6 +116,7 @@ func Router(c *ctx.AptlyContext) http.Handler { api.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile) api.POST("/repos/:name/file/:dir", apiReposPackageFromDir) + api.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage) api.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile) api.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir) diff --git a/system/t12_api/repos.py b/system/t12_api/repos.py index 8396ddb50..bc0a43ccf 100644 --- a/system/t12_api/repos.py +++ b/system/t12_api/repos.py @@ -360,3 +360,33 @@ def check(self): self.check_not_in(b"Warnings: ", resp.content) self.check_equal(self.get("/api/repos/" + repo_name + "/packages?maximumVersion=1").json(), ['Pi386 libboost-program-options-dev 1.62.0.1 7760e62f99c551cb']) + + +class ReposAPITestCopyPackage(APITest): + """ + POST /api/repos/:name/copy/:src/:file + """ + def check(self): + pkg_name = "libboost-program-options-dev_1.49.0.1_i386" + + # Creating origin repo + repo1_name = self.random_name() + self.check_equal(self.post("/api/repos", json={"Name": repo1_name, "Comment": "origin repo"}).status_code, 201) + + # Uploading test package + d = self.random_name() + self.check_equal(self.upload(f"/api/files/{d}", f"{pkg_name}.deb").status_code, 200) + resp = self.post_task(f"/api/repos/{repo1_name}/file/{d}") + self.check_equal(resp.json()['State'], TASK_SUCCEEDED) + + # Creating target repo + repo2_name = self.random_name() + self.check_equal(self.post("/api/repos", json={"Name": repo2_name, "Comment": "target repo"}).status_code, 201) + + # Copy the package + resp = self.post_task(f"/api/repos/{repo2_name}/copy/{repo1_name}/{pkg_name}") + self.check_equal(resp.status_code, 200) + self.check_equal(resp.json()['State'], TASK_SUCCEEDED) + + self.check_equal(self.get(f"/api/repos/{repo2_name}/packages").json(), + ['Pi386 libboost-program-options-dev 1.49.0.1 918d2f433384e378'])