diff --git a/apis/project.go b/apis/project.go index 50b14f1..91b83e0 100644 --- a/apis/project.go +++ b/apis/project.go @@ -1,237 +1,240 @@ /* Copyright © 2017-2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package apis import ( "net/http" "strings" "invent.kde.org/sysadmin/projects-api.git/models" "github.com/gin-gonic/gin" ) type projectService interface { Get(path string) (*models.Project, error) Find(filter *models.ProjectFilter) ([]string, error) List(path string) ([]string, error) } type projectResource struct { service projectService } // ServeProjectResource creates a new API resource. func ServeProjectResource(rg *gin.RouterGroup, service projectService) { r := &projectResource{service} rg.GET("/repo/*path", r.repo) rg.GET("/project/*path", r.get) rg.GET("/find", r.find) rg.GET("/projects", r.projects) rg.GET("/projects/*path", r.projects) } /** * @apiDefine ProjectSuccessExample * @apiSuccessExample {json} Success-Response: * { * "i18n": { * "stable": "none", * "stableKF5": "krita/3.1", * "trunk": "none", * "trunkKF5": "master", * "component": "calligra" * }, * "repo": "krita" * "path": "calligra/krita" * } */ /** * @api {get} /project/:path Get (by Project) * * @apiVersion 1.0.0 * @apiGroup Project * @apiName project * * @apiDescription Gets the metadata of the project identified by path. * * @apiUse ProjectSuccessExample * * @apiError Forbidden Path may not be accessed. */ func (r *projectResource) get(c *gin.Context) { path := strings.TrimLeft(c.Param("path"), "/") if strings.Contains(path, "/..") || strings.Contains(path, "../") { c.AbortWithStatus(http.StatusForbidden) return } response, err := r.service.Get(path) if err != nil { panic(err) } if response == nil { c.AbortWithStatus(http.StatusNotFound) return } c.JSON(http.StatusOK, response) } /** * @apiDeprecated use the /project endpoint. Since the gitlab migration the * repo path and the project path are the same. * @api {get} /repo/:path Get (by Repository) * @apiParam {String} path Repository path as seen in git clone kde:path/is/this. * * @apiVersion 1.0.0 * @apiGroup Project * @apiName repo * * @apiDescription Gets the metadata of the project identified by a query. * * @apiUse ProjectSuccessExample * * @apiError NotFound Couldn't find a project associated with this repo. * @apiError MultipleChoices Couldn't find a unique project for this repo. * You'll need to pick a project and get it via the /project/ * endpoint * @apiErrorExample {json} MultipleChoices-Response: * [ * "books/kf5book", * "books/kf5book-duplcate" * ] */ func (r *projectResource) repo(c *gin.Context) { path := strings.TrimLeft(c.Param("path"), "/") matches, err := r.service.Find(&models.ProjectFilter{RepoPath: path}) if err != nil { panic(err) } if len(matches) < 1 { c.AbortWithStatus(http.StatusNotFound) return } if len(matches) > 1 { c.JSON(http.StatusMultipleChoices, matches) return } response, err := r.service.Get(matches[0]) if err != nil { panic(err) } c.JSON(http.StatusOK, response) } /** * @api {get} /find Find + * @apiParam {String} i18n_component I18n component of the project to find. + * Since the gitlab transition this acts as unique project identifier. + * This is the i18n{component:foo} value returned by the /project endpoint. * @apiParam {String} repo repo attribute of the project * to find. * @apiParam {Bool} active Whether to find only projects marked active (inactive * projects can be skipped by code that wants to iterate worthwhile code * projects only e.g. sysadmin repos are usually inactive). * This defaults to false, giving you all repos. * @apiParam {Bool} inactive Whether to find only inactive projects (see active) * This defaults to false, giving you all repos. * @apiParam {String} id Basename of the project to find. Since the gitlab * migration this filter doesn't have much purpose since the basenames are * not unique. Do not assume that you'll get a unique result when using this * filter! * * @apiVersion 1.0.0 * @apiGroup Project * @apiName find * * @apiDescription Finds matching projects by a combination of filter params or * none to list all projects. * * @apiError NotFound Nothing matched the filter criteria * * @apiSuccessExample {json} Success-Response: * [ * "books", * "books/kf5book", * "calligra", * ... * ] */ func (r *projectResource) find(c *gin.Context) { var filter models.ProjectFilter c.BindQuery(&filter) matches, err := r.service.Find(&filter) if len(matches) == 0 || err != nil { c.AbortWithStatus(http.StatusNotFound) return } c.JSON(http.StatusOK, matches) } /** * @api {get} /projects/:prefix List * @apiParam {String} prefix Prefix path of the project. This will usually be * the module/components the project is sorted under. * * @apiVersion 1.0.0 * @apiGroup Project * @apiName projects * * @apiDescription Lists all *projects* underneath the prefix. If the prefix is * a project itself it will be included in the list. If the prefix is not a * project only its "children" will be returned. * * @apiSuccessExample {json} Project (/projects/calligra/krita): * [ * "calligra/krita" * ] * * @apiSuccessExample {json} Project with Children (/projects/papa): * [ * "aba", * "aba/bubbale1", * "aba/bubbale2" * ] * * @apiSuccessExample {json} Component (/projects/frameworks) * [ * "frameworks/solid", * "frameworks/ki18n", * "..." * ] */ func (r *projectResource) projects(c *gin.Context) { path := strings.TrimLeft(c.Param("path"), "/") matches, err := r.service.List(path) if len(matches) == 0 || err != nil { c.AbortWithStatus(http.StatusNotFound) return } c.JSON(http.StatusOK, matches) } diff --git a/models/project_filter.go b/models/project_filter.go index 208d2e6..ce5ee7c 100644 --- a/models/project_filter.go +++ b/models/project_filter.go @@ -1,31 +1,32 @@ /* - Copyright © 2019 Harald Sitter + Copyright © 2019-2020 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package models // ProjectFilter is used to filter projects on find() calls. // The type(names) in this struct must be chosen so that an uninitialized // filter results in match-all behavior! type ProjectFilter struct { - ID string `form:"id"` // basename of project path - RepoPath string `form:"repo"` // complete repo path - ActiveOnly bool `form:"active"` // whether to select active projects - InactiveOnly bool `form:"inactive"` // whether to select inactive projects + ID string `form:"id"` // basename of project path + RepoPath string `form:"repo"` // complete repo path + ActiveOnly bool `form:"active"` // whether to select active projects + InactiveOnly bool `form:"inactive"` // whether to select inactive projects + I18nComponent string `form:"i18n_component"` // the i18n component (aka identifier) } diff --git a/services/project.go b/services/project.go index 879fa5f..9769b9c 100644 --- a/services/project.go +++ b/services/project.go @@ -1,94 +1,97 @@ /* - Copyright © 2017-2019 Harald Sitter + Copyright © 2017-2020 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package services import ( "path/filepath" "strings" "invent.kde.org/sysadmin/projects-api.git/models" ) type ProjectService struct { dao gitDAO } func NewProjectService(dao gitDAO) *ProjectService { return &ProjectService{dao} } func (s *ProjectService) Get(path string) (*models.Project, error) { return s.dao.Get(path) } func (s *ProjectService) isProject(path string) bool { model, err := s.Get(path) if err != nil { panic(err) } return model != nil && model.Repo != "" } func (s *ProjectService) Find(filter *models.ProjectFilter) ([]string, error) { matches := []string{} for _, path := range s.dao.List() { if !s.isProject(path) { continue } if len(filter.ID) != 0 && filter.ID != filepath.Base(path) { continue } model, _ := s.Get(path) if len(filter.RepoPath) != 0 { if model.Repo != filter.RepoPath { continue // doesn't match repopath constraint } } if filter.ActiveOnly && !model.Active { continue } if filter.InactiveOnly && model.Active { continue } + if len(filter.I18nComponent) != 0 && filter.I18nComponent != model.Identifier { + continue + } matches = append(matches, path) } return matches, nil } func (s *ProjectService) List(prefix string) ([]string, error) { matches := []string{} for _, path := range s.dao.List() { if !s.isProject(path) { continue } if len(prefix) == 0 || path == prefix || strings.HasPrefix(path, prefix+"/") { matches = append(matches, path) } } return matches, nil }