Files

5545 lines
163 KiB
Go

package handlers
import "crypto/rand"
import "database/sql"
import "encoding/hex"
import "errors"
import "fmt"
import "io"
import "mime/multipart"
import "net/http"
import "os"
import "path/filepath"
import "strconv"
import "strings"
import "time"
import "unicode"
import "codit/internal/auth"
import "codit/internal/config"
import "codit/internal/db"
import "codit/internal/docker"
import "codit/internal/git"
import "codit/internal/middleware"
import "codit/internal/models"
import "codit/internal/rpm"
import "codit/internal/storage"
import "codit/internal/util"
type API struct {
Cfg config.Config
Store *db.Store
Repos git.RepoManager
RpmBase string
RpmMeta *rpm.MetaManager
RpmMirror *rpm.MirrorManager
DockerBase string
Uploads storage.FileStore
Logger *util.Logger
OnTLSListenersChanged func()
OnTLSListenerRuntimeStatus func() map[string]int
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type createUserRequest struct {
Username string `json:"username"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Password string `json:"password"`
IsAdmin bool `json:"is_admin"`
}
type updateMeRequest struct {
DisplayName string `json:"display_name"`
Email string `json:"email"`
Password string `json:"password"`
}
type updateAuthSettingsRequest struct {
AuthMode string `json:"auth_mode"`
OIDCEnabled bool `json:"oidc_enabled"`
LDAPURL string `json:"ldap_url"`
LDAPBindDN string `json:"ldap_bind_dn"`
LDAPBindPassword string `json:"ldap_bind_password"`
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
LDAPUserFilter string `json:"ldap_user_filter"`
LDAPTLSInsecureSkipVerify bool `json:"ldap_tls_insecure_skip_verify"`
OIDCClientID string `json:"oidc_client_id"`
OIDCClientSecret string `json:"oidc_client_secret"`
OIDCAuthorizeURL string `json:"oidc_authorize_url"`
OIDCTokenURL string `json:"oidc_token_url"`
OIDCUserInfoURL string `json:"oidc_userinfo_url"`
OIDCRedirectURL string `json:"oidc_redirect_url"`
OIDCScopes string `json:"oidc_scopes"`
OIDCTLSInsecureSkipVerify bool `json:"oidc_tls_insecure_skip_verify"`
}
type testLDAPSettingsRequest struct {
AuthMode string `json:"auth_mode"`
LDAPURL string `json:"ldap_url"`
LDAPBindDN string `json:"ldap_bind_dn"`
LDAPBindPassword string `json:"ldap_bind_password"`
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
LDAPUserFilter string `json:"ldap_user_filter"`
LDAPTLSInsecureSkipVerify *bool `json:"ldap_tls_insecure_skip_verify"`
Username string `json:"username"`
Password string `json:"password"`
}
type updateTLSSettingsRequest struct {
HTTPAddrs []string `json:"http_addrs"`
HTTPSAddrs []string `json:"https_addrs"`
TLSServerCertSource string `json:"tls_server_cert_source"`
TLSCertFile string `json:"tls_cert_file"`
TLSKeyFile string `json:"tls_key_file"`
TLSPKIServerCertID string `json:"tls_pki_server_cert_id"`
TLSClientAuth string `json:"tls_client_auth"`
TLSClientCAFile string `json:"tls_client_ca_file"`
TLSPKIClientCAID string `json:"tls_pki_client_ca_id"`
TLSMinVersion string `json:"tls_min_version"`
}
type tlsListenerRequest struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
HTTPAddrs []string `json:"http_addrs"`
HTTPSAddrs []string `json:"https_addrs"`
AuthPolicy string `json:"auth_policy"`
ApplyPolicyAPI bool `json:"apply_policy_api"`
ApplyPolicyGit bool `json:"apply_policy_git"`
ApplyPolicyRPM bool `json:"apply_policy_rpm"`
ApplyPolicyV2 bool `json:"apply_policy_v2"`
ClientCertAllowlist []string `json:"client_cert_allowlist"`
TLSServerCertSource string `json:"tls_server_cert_source"`
TLSCertFile string `json:"tls_cert_file"`
TLSKeyFile string `json:"tls_key_file"`
TLSPKIServerCertID string `json:"tls_pki_server_cert_id"`
TLSClientAuth string `json:"tls_client_auth"`
TLSClientCAFile string `json:"tls_client_ca_file"`
TLSPKIClientCAID string `json:"tls_pki_client_ca_id"`
TLSMinVersion string `json:"tls_min_version"`
}
type servicePrincipalRequest struct {
Name string `json:"name"`
Description string `json:"description"`
IsAdmin bool `json:"is_admin"`
Disabled bool `json:"disabled"`
}
type certPrincipalBindingRequest struct {
Fingerprint string `json:"fingerprint"`
PrincipalID string `json:"principal_id"`
Enabled bool `json:"enabled"`
}
type principalProjectRoleRequest struct {
ProjectID string `json:"project_id"`
Role string `json:"role"`
}
type createProjectRequest struct {
Slug string `json:"slug"`
Name string `json:"name"`
Description string `json:"description"`
HomePage string `json:"home_page"`
}
type createRepoRequest struct {
Name string `json:"name"`
Type string `json:"type"`
}
type updateRepoRequest struct {
Name string `json:"name"`
}
type attachRepoRequest struct {
RepoID string `json:"repo_id"`
}
type repoResponse struct {
models.Repo
CloneURL string `json:"clone_url"`
RPMURL string `json:"rpm_url"`
DockerURL string `json:"docker_url"`
}
type repoListItem struct {
models.Repo
CloneURL string `json:"clone_url"`
RPMURL string `json:"rpm_url"`
DockerURL string `json:"docker_url"`
OwnerProject string `json:"owner_project"`
OwnerSlug string `json:"owner_slug"`
}
type availableRepoItem struct {
models.Repo
OwnerProject string `json:"owner_project"`
OwnerSlug string `json:"owner_slug"`
}
type repoStatsResponse struct {
RepoID string `json:"repo_id"`
Branches int `json:"branches"`
DefaultRef string `json:"default_ref"`
LastCommit string `json:"last_commit"`
LastAuthor string `json:"last_author"`
LastWhen string `json:"last_when"`
LastMessage string `json:"last_message"`
CloneURL string `json:"clone_url"`
ProjectSlug string `json:"project_slug"`
Repository string `json:"repository"`
}
type repoBranchesInfo struct {
Default string `json:"default"`
Branches []git.BranchInfo `json:"branches"`
}
type repoTypeItem struct {
Value string `json:"value"`
Label string `json:"label"`
}
type repoBranchRequest struct {
Name string `json:"name"`
}
type repoBranchRenameRequest struct {
From string `json:"from"`
To string `json:"to"`
}
type repoBranchCreateRequest struct {
Name string `json:"name"`
From string `json:"from"`
}
type repoRPMCreateRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Parent string `json:"parent"`
Mode string `json:"mode"`
AllowDelete bool `json:"allow_delete"`
RemoteURL string `json:"remote_url"`
ConnectHost string `json:"connect_host"`
HostHeader string `json:"host_header"`
TLSServerName string `json:"tls_server_name"`
TLSInsecureSkipVerify bool `json:"tls_insecure_skip_verify"`
SyncIntervalSec int64 `json:"sync_interval_sec"`
}
type repoRPMUpdateRequest struct {
Path *string `json:"path"`
Name *string `json:"name"`
Mode *string `json:"mode"`
AllowDelete *bool `json:"allow_delete"`
RemoteURL *string `json:"remote_url"`
ConnectHost *string `json:"connect_host"`
HostHeader *string `json:"host_header"`
TLSServerName *string `json:"tls_server_name"`
TLSInsecureSkipVerify *bool `json:"tls_insecure_skip_verify"`
SyncIntervalSec *int64 `json:"sync_interval_sec"`
}
type createAPIKeyRequest struct {
Name string `json:"name"`
ExpiresAt int64 `json:"expires_at"`
}
type apiKeyResponse struct {
models.APIKey
Token string `json:"token,omitempty"`
}
type repoDockerRenameTagRequest struct {
Image string `json:"image"`
From string `json:"from"`
To string `json:"to"`
}
type repoDockerRenameImageRequest struct {
From string `json:"from"`
To string `json:"to"`
}
type repoRPMUploadResponse struct {
Filename string `json:"filename"`
Size int64 `json:"size"`
}
type createIssueRequest struct {
Title string `json:"title"`
Body string `json:"body"`
AssigneeID string `json:"assignee_id"`
}
type updateIssueRequest struct {
Title string `json:"title"`
Body string `json:"body"`
Status string `json:"status"`
AssigneeID string `json:"assignee_id"`
}
type createWikiRequest struct {
Title string `json:"title"`
Slug string `json:"slug"`
Body string `json:"body"`
}
type updateWikiRequest struct {
Title string `json:"title"`
Body string `json:"body"`
}
type issueCommentRequest struct {
Body string `json:"body"`
}
type projectMemberRequest struct {
UserID string `json:"user_id"`
Role string `json:"role"`
}
func (api *API) Login(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req loginRequest
var err error
var user models.User
var storedHash string
var ldapUser auth.LDAPUser
var newUser models.User
var created models.User
var authCfg config.Config
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Username == "" || req.Password == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"})
return
}
user, storedHash, err = api.Store.GetUserByUsername(req.Username)
if err == nil && user.Disabled {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "user disabled"})
return
}
if err == nil && storedHash != "" && auth.ComparePassword(storedHash, req.Password) == nil {
api.issueSession(w, user)
WriteJSON(w, http.StatusOK, user)
return
}
authCfg, _ = api.effectiveAuthConfig()
if authCfg.AuthMode == "ldap" || authCfg.AuthMode == "hybrid" {
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "ldap login attempt username=%s mode=%s", req.Username, authCfg.AuthMode)
}
ldapUser, err = auth.LDAPAuthenticateContext(r.Context(), authCfg, req.Username, req.Password)
if err == nil {
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "ldap login success username=%s", req.Username)
}
if user.ID != "" && user.Disabled {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "user disabled"})
return
}
if user.ID == "" {
newUser = models.User{
Username: ldapUser.Username,
DisplayName: ldapUser.DisplayName,
Email: ldapUser.Email,
AuthSource: "ldap",
}
if newUser.Email == "" {
newUser.Email = ldapUser.Username + "@local"
}
created, err = api.Store.CreateUser(newUser, "")
if err == nil {
user = created
}
}
api.issueSession(w, user)
WriteJSON(w, http.StatusOK, user)
return
}
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_WARN, "ldap login failed username=%s err=%v", req.Username, err)
}
}
WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
}
func (api *API) issueSession(w http.ResponseWriter, user models.User) {
var token string
var err error
var expires time.Time
var cookie *http.Cookie
token, err = auth.NewSessionToken()
if err != nil {
return
}
expires = auth.SessionExpiry(api.Cfg)
_ = api.Store.CreateSession(user.ID, token, expires)
cookie = &http.Cookie{
Name: "codit_session",
Value: token,
HttpOnly: true,
Path: "/",
Expires: expires,
}
http.SetCookie(w, cookie)
}
func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var cookie *http.Cookie
var err error
cookie, err = r.Cookie("codit_session")
if err == nil {
_ = api.Store.DeleteSession(cookie.Value)
}
cookie = &http.Cookie{
Name: "codit_session",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
}
http.SetCookie(w, cookie)
w.WriteHeader(http.StatusNoContent)
}
func (api *API) Me(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var user models.User
var ok bool
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
WriteJSON(w, http.StatusOK, user)
}
func (api *API) UpdateMe(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var ctxUser models.User
var ok bool
var user models.User
var req updateMeRequest
var err error
var hash string
var newPassword bool
ctxUser, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
user, err = api.Store.GetUserByID(ctxUser.ID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.DisplayName != "" {
user.DisplayName = req.DisplayName
}
if req.Email != "" {
user.Email = req.Email
}
newPassword = strings.TrimSpace(req.Password) != ""
if newPassword && user.AuthSource != "db" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "password update not supported for non-db users"})
return
}
if newPassword {
hash, err = auth.HashPassword(req.Password)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
return
}
err = api.Store.UpdateUserWithPassword(user, hash)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
} else {
err = api.Store.UpdateUser(user)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
WriteJSON(w, http.StatusOK, user)
}
func (api *API) ListUsers(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var users []models.User
var err error
if !api.requireAdmin(w, r) {
return
}
users, err = api.Store.ListUsers()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, users)
}
func (api *API) CreateUser(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req createUserRequest
var err error
var hash string
var user models.User
var created models.User
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Username == "" || req.Password == "" || req.DisplayName == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "username, display_name, and password required"})
return
}
hash, err = auth.HashPassword(req.Password)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
return
}
user = models.User{
Username: req.Username,
DisplayName: req.DisplayName,
Email: req.Email,
IsAdmin: req.IsAdmin,
AuthSource: "db",
}
created, err = api.Store.CreateUser(user, hash)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) UpdateUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var err error
var payload createUserRequest
var hash string
var newPassword bool
if !api.requireAdmin(w, r) {
return
}
user, err = api.Store.GetUserByID(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
return
}
err = DecodeJSON(r, &payload)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if payload.DisplayName != "" {
user.DisplayName = payload.DisplayName
}
if payload.Email != "" {
user.Email = payload.Email
}
user.IsAdmin = payload.IsAdmin
newPassword = payload.Password != ""
if newPassword {
hash, err = auth.HashPassword(payload.Password)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
return
}
err = api.Store.UpdateUserWithPassword(user, hash)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
} else {
err = api.Store.UpdateUser(user)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
WriteJSON(w, http.StatusOK, user)
}
func (api *API) DeleteUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.DeleteUser(params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) DisableUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.SetUserDisabled(params["id"], true)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) EnableUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.SetUserDisabled(params["id"], false)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) GetAuthSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var settings models.AuthSettings
var err error
var user models.User
var ok bool
if !api.requireAdmin(w, r) {
return
}
user, ok = middleware.UserFromContext(r.Context())
if ok && api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "auth settings fetch requested by user=%s", user.Username)
}
settings, err = api.getMergedAuthSettings()
if err != nil {
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_ERROR, "auth settings fetch failed err=%v", err)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "auth settings fetch success mode=%s ldap_url=%s oidc_authorize_url=%s", settings.AuthMode, settings.LDAPURL, settings.OIDCAuthorizeURL)
}
WriteJSON(w, http.StatusOK, settings)
}
func (api *API) UpdateAuthSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req updateAuthSettingsRequest
var err error
var settings models.AuthSettings
var user models.User
var ok bool
if !api.requireAdmin(w, r) {
return
}
user, ok = middleware.UserFromContext(r.Context())
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
settings = models.AuthSettings{
AuthMode: normalizeAuthMode(req.AuthMode),
OIDCEnabled: req.OIDCEnabled,
LDAPURL: strings.TrimSpace(req.LDAPURL),
LDAPBindDN: strings.TrimSpace(req.LDAPBindDN),
LDAPBindPassword: req.LDAPBindPassword,
LDAPUserBaseDN: strings.TrimSpace(req.LDAPUserBaseDN),
LDAPUserFilter: strings.TrimSpace(req.LDAPUserFilter),
LDAPTLSInsecureSkipVerify: req.LDAPTLSInsecureSkipVerify,
OIDCClientID: strings.TrimSpace(req.OIDCClientID),
OIDCClientSecret: req.OIDCClientSecret,
OIDCAuthorizeURL: strings.TrimSpace(req.OIDCAuthorizeURL),
OIDCTokenURL: strings.TrimSpace(req.OIDCTokenURL),
OIDCUserInfoURL: strings.TrimSpace(req.OIDCUserInfoURL),
OIDCRedirectURL: strings.TrimSpace(req.OIDCRedirectURL),
OIDCScopes: strings.TrimSpace(req.OIDCScopes),
OIDCTLSInsecureSkipVerify: req.OIDCTLSInsecureSkipVerify,
}
if settings.LDAPUserFilter == "" {
settings.LDAPUserFilter = "(uid={username})"
}
if settings.OIDCScopes == "" {
settings.OIDCScopes = "openid profile email"
}
if ok && api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "auth settings update requested by user=%s mode=%s oidc_enabled=%t ldap_url=%s oidc_authorize_url=%s",
user.Username, settings.AuthMode, settings.OIDCEnabled, settings.LDAPURL, settings.OIDCAuthorizeURL)
}
err = api.Store.SetAuthSettings(settings)
if err != nil {
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_ERROR, "auth settings update failed err=%v", err)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "auth settings update success mode=%s oidc_enabled=%t ldap_url=%s oidc_authorize_url=%s", settings.AuthMode, settings.OIDCEnabled, settings.LDAPURL, settings.OIDCAuthorizeURL)
}
WriteJSON(w, http.StatusOK, settings)
}
func (api *API) TestLDAPSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req testLDAPSettingsRequest
var err error
var merged models.AuthSettings
var cfg config.Config
var user auth.LDAPUser
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
merged, err = api.getMergedAuthSettings()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if strings.TrimSpace(req.AuthMode) != "" {
merged.AuthMode = normalizeAuthMode(req.AuthMode)
}
if strings.TrimSpace(req.LDAPURL) != "" {
merged.LDAPURL = strings.TrimSpace(req.LDAPURL)
}
if strings.TrimSpace(req.LDAPBindDN) != "" {
merged.LDAPBindDN = strings.TrimSpace(req.LDAPBindDN)
}
if req.LDAPBindPassword != "" {
merged.LDAPBindPassword = req.LDAPBindPassword
}
if strings.TrimSpace(req.LDAPUserBaseDN) != "" {
merged.LDAPUserBaseDN = strings.TrimSpace(req.LDAPUserBaseDN)
}
if strings.TrimSpace(req.LDAPUserFilter) != "" {
merged.LDAPUserFilter = strings.TrimSpace(req.LDAPUserFilter)
}
if req.LDAPTLSInsecureSkipVerify != nil {
merged.LDAPTLSInsecureSkipVerify = *req.LDAPTLSInsecureSkipVerify
}
if merged.LDAPUserFilter == "" {
merged.LDAPUserFilter = "(uid={username})"
}
cfg = api.Cfg
cfg.AuthMode = merged.AuthMode
cfg.LDAPURL = merged.LDAPURL
cfg.LDAPBindDN = merged.LDAPBindDN
cfg.LDAPBindPassword = merged.LDAPBindPassword
cfg.LDAPUserBaseDN = merged.LDAPUserBaseDN
cfg.LDAPUserFilter = merged.LDAPUserFilter
err = auth.LDAPTestConnectionContext(r.Context(), cfg)
if err != nil {
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_WARN, "ldap test connection failed url=%s err=%v", cfg.LDAPURL, err)
}
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "ldap test connection success url=%s", cfg.LDAPURL)
}
if strings.TrimSpace(req.Username) != "" && strings.TrimSpace(req.Password) != "" {
user, err = auth.LDAPAuthenticateContext(r.Context(), cfg, strings.TrimSpace(req.Username), req.Password)
if err != nil {
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_WARN, "ldap test bind failed username=%s err=%v", strings.TrimSpace(req.Username), err)
}
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if api.Logger != nil {
api.Logger.Write("auth", util.LOG_INFO, "ldap test bind success username=%s", user.Username)
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "user": user.Username})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) GetTLSSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var settings models.TLSSettings
var err error
if !api.requireAdmin(w, r) {
return
}
settings, err = api.getMergedTLSSettings()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, settings)
}
func (api *API) UpdateTLSSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req updateTLSSettingsRequest
var settings models.TLSSettings
var err error
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
settings = models.TLSSettings{
HTTPAddrs: normalizeAddrList(req.HTTPAddrs),
HTTPSAddrs: normalizeAddrList(req.HTTPSAddrs),
TLSServerCertSource: "pki",
TLSCertFile: "",
TLSKeyFile: "",
TLSPKIServerCertID: strings.TrimSpace(req.TLSPKIServerCertID),
TLSClientAuth: normalizeTLSClientAuth(req.TLSClientAuth),
TLSClientCAFile: "",
TLSPKIClientCAID: strings.TrimSpace(req.TLSPKIClientCAID),
TLSMinVersion: normalizeTLSMinVersion(req.TLSMinVersion),
}
if len(settings.HTTPAddrs) == 0 && len(settings.HTTPSAddrs) == 0 {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one of http_addrs or https_addrs is required"})
return
}
if len(settings.HTTPSAddrs) > 0 && strings.TrimSpace(settings.TLSPKIServerCertID) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_server_cert_id is required when https_addrs is configured"})
return
}
if tlsClientAuthNeedsCA(settings.TLSClientAuth) && !tlsClientCAConfigured(settings.TLSPKIClientCAID) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_client_ca_id is required for selected tls_client_auth"})
return
}
err = api.Store.SetTLSSettings(settings)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, settings)
}
func (api *API) ListTLSListeners(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var items []models.TLSListener
var err error
if !api.requireAdmin(w, r) {
return
}
items, err = api.Store.ListTLSListeners()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, items)
}
func (api *API) CreateTLSListener(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req tlsListenerRequest
var item models.TLSListener
var created models.TLSListener
var err error
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
item = normalizeTLSListenerRequest(req)
if item.Name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
if len(item.HTTPAddrs) == 0 && len(item.HTTPSAddrs) == 0 {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one of http_addrs or https_addrs is required"})
return
}
if len(item.HTTPSAddrs) > 0 && strings.TrimSpace(item.TLSPKIServerCertID) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_server_cert_id is required when https_addrs is configured"})
return
}
if tlsClientAuthNeedsCA(item.TLSClientAuth) && !tlsClientCAConfigured(item.TLSPKIClientCAID) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_client_ca_id is required for selected tls_client_auth"})
return
}
created, err = api.Store.CreateTLSListener(item)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if api.OnTLSListenersChanged != nil {
api.OnTLSListenersChanged()
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) UpdateTLSListener(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req tlsListenerRequest
var item models.TLSListener
var err error
if !api.requireAdmin(w, r) {
return
}
item, err = api.Store.GetTLSListener(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "listener not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
item = mergeTLSListener(item, normalizeTLSListenerRequest(req))
if item.Name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
if len(item.HTTPAddrs) == 0 && len(item.HTTPSAddrs) == 0 {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one of http_addrs or https_addrs is required"})
return
}
if len(item.HTTPSAddrs) > 0 && strings.TrimSpace(item.TLSPKIServerCertID) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_server_cert_id is required when https_addrs is configured"})
return
}
if tlsClientAuthNeedsCA(item.TLSClientAuth) && !tlsClientCAConfigured(item.TLSPKIClientCAID) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_client_ca_id is required for selected tls_client_auth"})
return
}
err = api.Store.UpdateTLSListener(item)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if api.OnTLSListenersChanged != nil {
api.OnTLSListenersChanged()
}
WriteJSON(w, http.StatusOK, item)
}
func (api *API) DeleteTLSListener(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.DeleteTLSListener(params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if api.OnTLSListenersChanged != nil {
api.OnTLSListenersChanged()
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) GetTLSListenerRuntimeStatus(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var status map[string]int
if !api.requireAdmin(w, r) {
return
}
status = make(map[string]int)
if api.OnTLSListenerRuntimeStatus != nil {
status = api.OnTLSListenerRuntimeStatus()
}
WriteJSON(w, http.StatusOK, status)
}
func (api *API) ListServicePrincipals(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var items []models.ServicePrincipal
var err error
if !api.requireAdmin(w, r) {
return
}
items, err = api.Store.ListServicePrincipals()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, items)
}
func (api *API) CreateServicePrincipal(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req servicePrincipalRequest
var item models.ServicePrincipal
var created models.ServicePrincipal
var err error
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
item = models.ServicePrincipal{
Name: strings.TrimSpace(req.Name),
Description: strings.TrimSpace(req.Description),
IsAdmin: req.IsAdmin,
Disabled: req.Disabled,
}
if item.Name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
created, err = api.Store.CreateServicePrincipal(item)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) UpdateServicePrincipal(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req servicePrincipalRequest
var item models.ServicePrincipal
var err error
if !api.requireAdmin(w, r) {
return
}
item, err = api.Store.GetServicePrincipal(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "principal not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
item.Name = strings.TrimSpace(req.Name)
item.Description = strings.TrimSpace(req.Description)
item.IsAdmin = req.IsAdmin
item.Disabled = req.Disabled
if item.Name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
err = api.Store.UpdateServicePrincipal(item)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, item)
}
func (api *API) DeleteServicePrincipal(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.DeleteServicePrincipal(params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) ListCertPrincipalBindings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var items []models.CertPrincipalBinding
var err error
if !api.requireAdmin(w, r) {
return
}
items, err = api.Store.ListCertPrincipalBindings()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, items)
}
func (api *API) UpsertCertPrincipalBinding(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req certPrincipalBindingRequest
var item models.CertPrincipalBinding
var updated models.CertPrincipalBinding
var err error
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
item = models.CertPrincipalBinding{
Fingerprint: strings.ToLower(strings.TrimSpace(req.Fingerprint)),
PrincipalID: strings.TrimSpace(req.PrincipalID),
Enabled: req.Enabled,
}
if item.Fingerprint == "" || item.PrincipalID == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "fingerprint and principal_id are required"})
return
}
updated, err = api.Store.UpsertCertPrincipalBinding(item)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, updated)
}
func (api *API) DeleteCertPrincipalBinding(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.DeleteCertPrincipalBinding(params["fingerprint"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) ListPrincipalProjectRoles(w http.ResponseWriter, r *http.Request, params map[string]string) {
var items []models.PrincipalProjectRole
var err error
if !api.requireAdmin(w, r) {
return
}
items, err = api.Store.ListPrincipalProjectRoles(params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, items)
}
func (api *API) UpsertPrincipalProjectRole(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req principalProjectRoleRequest
var item models.PrincipalProjectRole
var saved models.PrincipalProjectRole
var err error
if !api.requireAdmin(w, r) {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
item = models.PrincipalProjectRole{
PrincipalID: strings.TrimSpace(params["id"]),
ProjectID: strings.TrimSpace(req.ProjectID),
Role: normalizeRole(req.Role),
}
if item.PrincipalID == "" || item.ProjectID == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "principal id and project id are required"})
return
}
saved, err = api.Store.UpsertPrincipalProjectRole(item)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, saved)
}
func (api *API) DeletePrincipalProjectRole(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.DeletePrincipalProjectRole(params["id"], params["projectId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) ListProjects(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var projects []models.Project
var err error
var limit int
var offset int
var query string
var v string
var i int
var user models.User
var ok bool
limit = 0
offset = 0
query = strings.TrimSpace(r.URL.Query().Get("q"))
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
v = r.URL.Query().Get("limit")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil && i > 0 {
limit = i
}
}
v = r.URL.Query().Get("offset")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil && i >= 0 {
offset = i
}
}
if user.IsAdmin {
if query != "" || limit > 0 || offset > 0 {
projects, err = api.Store.ListProjectsFiltered(limit, offset, query)
} else {
projects, err = api.Store.ListProjects()
}
} else {
if query != "" || limit > 0 || offset > 0 {
projects, err = api.Store.ListProjectsFilteredForUser(user.ID, limit, offset, query)
} else {
projects, err = api.Store.ListProjectsForUser(user.ID)
}
}
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, projects)
}
func (api *API) GetProject(w http.ResponseWriter, r *http.Request, params map[string]string) {
var project models.Project
var err error
project, err = api.Store.GetProject(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
if !api.requireProjectRole(w, r, project.ID, "viewer") {
return
}
WriteJSON(w, http.StatusOK, project)
}
func (api *API) CreateProject(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req createProjectRequest
var err error
var user models.User
var project models.Project
var created models.Project
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Slug == "" || req.Name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "slug and name required"})
return
}
if slugHasWhitespace(req.Slug) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "slug cannot contain whitespace"})
return
}
user, _ = middleware.UserFromContext(r.Context())
project = models.Project{
Slug: req.Slug,
Name: req.Name,
Description: req.Description,
HomePage: normalizeProjectHomePage(req.HomePage),
CreatedBy: user.ID,
UpdatedBy: user.ID,
}
created, err = api.Store.CreateProject(project)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) UpdateProject(w http.ResponseWriter, r *http.Request, params map[string]string) {
var project models.Project
var err error
var req createProjectRequest
var user models.User
if !api.requireProjectRole(w, r, params["id"], "admin") {
return
}
project, err = api.Store.GetProject(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Name != "" {
project.Name = req.Name
}
if req.Slug != "" {
if slugHasWhitespace(req.Slug) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "slug cannot contain whitespace"})
return
}
project.Slug = req.Slug
}
project.Description = req.Description
if req.HomePage != "" {
project.HomePage = normalizeProjectHomePage(req.HomePage)
}
if project.HomePage == "" {
project.HomePage = "info"
}
user, _ = middleware.UserFromContext(r.Context())
project.UpdatedBy = user.ID
project.UpdatedAt = time.Now().UTC().Unix()
err = api.Store.UpdateProject(project)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, project)
}
func (api *API) DeleteProject(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
var repos []models.Repo
var running bool
var i int
var tempPaths []string
var sourcePaths []string
var temp string
if !api.requireProjectRole(w, r, params["id"], "admin") {
return
}
repos, err = api.Store.ListReposOwned(params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
for i = 0; i < len(repos); i++ {
if repos[i].Type != "rpm" {
continue
}
running, err = api.Store.HasRunningRPMMirrorTask(repos[i].ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if running {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete project while rpm mirror sync is running", "repo_id": repos[i].ID, "repo_name": repos[i].Name})
return
}
}
tempPaths = make([]string, 0, len(repos))
sourcePaths = make([]string, 0, len(repos))
for i = 0; i < len(repos); i++ {
if repos[i].Path == "" {
continue
}
temp, err = moveToTrash(repos[i].Path)
if err != nil {
restoreFromTrash(tempPaths, sourcePaths)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
tempPaths = append(tempPaths, temp)
sourcePaths = append(sourcePaths, repos[i].Path)
}
err = api.Store.DeleteProject(params["id"])
if err != nil {
restoreFromTrash(tempPaths, sourcePaths)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
removeTrash(tempPaths)
w.WriteHeader(http.StatusNoContent)
}
func (api *API) ListProjectMembers(w http.ResponseWriter, r *http.Request, params map[string]string) {
var members []models.ProjectMember
var err error
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
return
}
members, err = api.Store.ListProjectMembers(params["projectId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, members)
}
func (api *API) AddProjectMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req projectMemberRequest
var err error
var member models.ProjectMember
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.UserID == "" || req.Role == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id and role required"})
return
}
member, err = api.Store.AddProjectMember(params["projectId"], req.UserID, normalizeRole(req.Role))
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, member)
}
func (api *API) UpdateProjectMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req projectMemberRequest
var err error
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.UserID == "" || req.Role == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id and role required"})
return
}
err = api.Store.UpdateProjectMemberRole(params["projectId"], req.UserID, normalizeRole(req.Role))
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) RemoveProjectMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
err = api.Store.RemoveProjectMember(params["projectId"], params["userId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) ListProjectMemberCandidates(w http.ResponseWriter, r *http.Request, params map[string]string) {
var users []models.User
var err error
var query string
var filtered []models.User
var i int
var username string
var displayName string
var email string
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
users, err = api.Store.ListUsers()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
if query == "" {
WriteJSON(w, http.StatusOK, users)
return
}
filtered = make([]models.User, 0, len(users))
for i = 0; i < len(users); i++ {
username = strings.ToLower(users[i].Username)
displayName = strings.ToLower(users[i].DisplayName)
email = strings.ToLower(users[i].Email)
if strings.Contains(username, query) || strings.Contains(displayName, query) || strings.Contains(email, query) {
filtered = append(filtered, users[i])
}
}
WriteJSON(w, http.StatusOK, filtered)
}
func (api *API) ListRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
var repos []models.Repo
var resp []repoListItem
var i int
var repo models.Repo
var cache map[string]models.Project
var owner models.Project
var ok bool
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
return
}
repos, err = api.Store.ListRepos(params["projectId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
resp = make([]repoListItem, 0, len(repos))
cache = map[string]models.Project{}
for i = 0; i < len(repos); i++ {
repo = repos[i]
owner, ok = cache[repo.ProjectID]
if !ok {
owner, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
cache[repo.ProjectID] = owner
}
var cloneURL string
var rpmURL string
var dockerURL string
if repo.Type == "git" {
cloneURL = api.cloneURL(owner.Slug, repo.Name)
}
if repo.Type == "rpm" {
rpmURL = api.rpmURL(owner.Slug, repo.Name)
}
if repo.Type == "docker" {
dockerURL = api.dockerURL(owner.Slug, repo.Name)
}
resp = append(resp, repoListItem{
Repo: repo,
CloneURL: cloneURL,
RPMURL: rpmURL,
DockerURL: dockerURL,
OwnerProject: owner.ID,
OwnerSlug: owner.Slug,
})
}
WriteJSON(w, http.StatusOK, resp)
}
func (api *API) ListAllRepos(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var err error
var repos []models.Repo
var resp []repoListItem
var i int
var repo models.Repo
var cache map[string]models.Project
var owner models.Project
var ok bool
var user models.User
var projectIDs []string
var query string
var repoType string
var filtered []models.Repo
var okUser bool
user, okUser = middleware.UserFromContext(r.Context())
if !okUser {
w.WriteHeader(http.StatusUnauthorized)
return
}
if user.IsAdmin {
repos, err = api.Store.ListAllRepos()
} else {
projectIDs, err = api.Store.ListProjectIDsForUser(user.ID)
if err == nil {
repos, err = api.Store.ListReposByProjectIDs(projectIDs)
}
}
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
repoType = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type")))
if query != "" || (repoType != "" && repoType != "all") {
filtered = make([]models.Repo, 0, len(repos))
for i = 0; i < len(repos); i++ {
repo = repos[i]
if repoType != "" && repoType != "all" && repo.Type != repoType {
continue
}
if query != "" {
if !strings.Contains(strings.ToLower(repo.Name), query) && !strings.Contains(strings.ToLower(repo.ID), query) {
continue
}
}
filtered = append(filtered, repo)
}
repos = filtered
}
resp = make([]repoListItem, 0, len(repos))
cache = map[string]models.Project{}
for i = 0; i < len(repos); i++ {
repo = repos[i]
owner, ok = cache[repo.ProjectID]
if !ok {
owner, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
cache[repo.ProjectID] = owner
}
var cloneURL string
var rpmURL string
var dockerURL string
if repo.Type == "git" {
cloneURL = api.cloneURL(owner.Slug, repo.Name)
}
if repo.Type == "rpm" {
rpmURL = api.rpmURL(owner.Slug, repo.Name)
}
if repo.Type == "docker" {
dockerURL = api.dockerURL(owner.Slug, repo.Name)
}
resp = append(resp, repoListItem{
Repo: repo,
CloneURL: cloneURL,
RPMURL: rpmURL,
DockerURL: dockerURL,
OwnerProject: owner.ID,
OwnerSlug: owner.Slug,
})
}
WriteJSON(w, http.StatusOK, resp)
}
func (api *API) GetRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var project models.Project
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
project, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
var cloneURL string
var rpmURL string
var dockerURL string
if repo.Type == "git" {
cloneURL = api.cloneURL(project.Slug, repo.Name)
}
if repo.Type == "rpm" {
rpmURL = api.rpmURL(project.Slug, repo.Name)
}
if repo.Type == "docker" {
dockerURL = api.dockerURL(project.Slug, repo.Name)
}
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
}
func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req createRepoRequest
var err error
var user models.User
var repoID string
var project models.Project
var repoPath string
var repo models.Repo
var created models.Repo
var repoType string
var projectStorageID int64
var repoStorageID int64
var ok bool
var exists bool
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
return
}
if nameHasWhitespace(req.Name) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name cannot contain whitespace"})
return
}
repoType, ok = normalizeRepoType(req.Type)
if !ok {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported repo type"})
return
}
exists, err = api.Store.RepoNameExists(params["projectId"], req.Name, repoType)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if exists {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "repository name already exists"})
return
}
user, _ = middleware.UserFromContext(r.Context())
repoID, err = util.NewID()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create repo"})
return
}
project, err = api.Store.GetProject(params["projectId"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
repo = models.Repo{
ID: repoID,
ProjectID: params["projectId"],
Name: req.Name,
Type: repoType,
Path: "",
CreatedBy: user.ID,
}
created, err = api.Store.CreateRepo(repo)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
projectStorageID, repoStorageID, err = api.Store.GetRepoStorageIDs(created.ID)
if err != nil {
_ = api.Store.DeleteRepo(created.ID)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
repoPath = api.repoStoragePathByType(created.Type, projectStorageID, repoStorageID)
if created.Type == "git" {
err = os.MkdirAll(filepath.Dir(repoPath), 0o755)
if err == nil {
err = api.Repos.InitRepo(repoPath, true)
}
} else {
err = os.MkdirAll(repoPath, 0o755)
}
if err != nil {
_ = api.Store.DeleteRepo(created.ID)
_ = os.RemoveAll(repoPath)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
created.Path = repoPath
err = api.Store.UpdateRepo(created)
if err != nil {
_ = api.Store.DeleteRepo(created.ID)
_ = os.RemoveAll(repoPath)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
var cloneURL string
var rpmURL string
var dockerURL string
if created.Type == "git" {
cloneURL = api.cloneURL(project.Slug, created.Name)
}
if created.Type == "rpm" {
rpmURL = api.rpmURL(project.Slug, created.Name)
}
if created.Type == "docker" {
dockerURL = api.dockerURL(project.Slug, created.Name)
}
WriteJSON(w, http.StatusCreated, repoResponse{Repo: created, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
}
func (api *API) ListAvailableRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repos []models.Repo
var err error
var query string
var limit int
var offset int
var v string
var i int
var resp []availableRepoItem
var repo models.Repo
var cache map[string]models.Project
var owner models.Project
var ok bool
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
query = strings.TrimSpace(r.URL.Query().Get("q"))
limit = 0
offset = 0
v = r.URL.Query().Get("limit")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil && i > 0 {
limit = i
}
}
v = r.URL.Query().Get("offset")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil && i >= 0 {
offset = i
}
}
repos, err = api.Store.ListAvailableReposForProject(params["projectId"], query, limit, offset)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
resp = make([]availableRepoItem, 0, len(repos))
cache = map[string]models.Project{}
for i = 0; i < len(repos); i++ {
repo = repos[i]
owner, ok = cache[repo.ProjectID]
if !ok {
owner, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
cache[repo.ProjectID] = owner
}
resp = append(resp, availableRepoItem{
Repo: repo,
OwnerProject: owner.ID,
OwnerSlug: owner.Slug,
})
}
WriteJSON(w, http.StatusOK, resp)
}
func (api *API) AttachForeignRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req attachRepoRequest
var err error
var repo models.Repo
var project models.Project
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.RepoID == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo_id required"})
return
}
repo, err = api.getRepoResolved(req.RepoID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if repo.ProjectID == params["projectId"] {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo already belongs to this project"})
return
}
project, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
err = api.Store.AttachRepoToProject(params["projectId"], repo.ID)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
repo.IsForeign = true
var cloneURL string
var rpmURL string
var dockerURL string
if repo.Type == "git" {
cloneURL = api.cloneURL(project.Slug, repo.Name)
}
if repo.Type == "rpm" {
rpmURL = api.rpmURL(project.Slug, repo.Name)
}
if repo.Type == "docker" {
dockerURL = api.dockerURL(project.Slug, repo.Name)
}
WriteJSON(w, http.StatusCreated, repoListItem{
Repo: repo,
CloneURL: cloneURL,
RPMURL: rpmURL,
DockerURL: dockerURL,
OwnerProject: project.ID,
OwnerSlug: project.Slug,
})
}
func (api *API) DetachForeignRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
return
}
repo, err = api.getRepoResolved(params["repoId"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if repo.ProjectID == params["projectId"] {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot detach owned repo"})
return
}
err = api.Store.DetachRepoFromProject(params["projectId"], repo.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) UpdateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var project models.Project
var req updateRepoRequest
var err error
var newPath string
var projectStorageID int64
var repoStorageID int64
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireProjectRole(w, r, repo.ProjectID, "writer") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if strings.TrimSpace(req.Name) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
return
}
if nameHasWhitespace(req.Name) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name cannot contain whitespace"})
return
}
project, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
projectStorageID, repoStorageID, err = api.Store.GetRepoStorageIDs(repo.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
newPath = api.repoStoragePathByType(repo.Type, projectStorageID, repoStorageID)
repo.Name = req.Name
repo.Path = newPath
err = api.Store.UpdateRepo(repo)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
var cloneURL string
var rpmURL string
var dockerURL string
if repo.Type == "git" {
cloneURL = api.cloneURL(project.Slug, repo.Name)
}
if repo.Type == "rpm" {
rpmURL = api.rpmURL(project.Slug, repo.Name)
}
if repo.Type == "docker" {
dockerURL = api.dockerURL(project.Slug, repo.Name)
}
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
}
func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var project models.Project
var running bool
var err error
var temp string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireProjectRole(w, r, repo.ProjectID, "writer") {
return
}
if repo.Type == "rpm" {
running, err = api.Store.HasRunningRPMMirrorTask(repo.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if running {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete repository while rpm mirror sync is running"})
return
}
}
project, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
if repo.Path != "" {
temp, err = moveToTrash(repo.Path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
err = api.Store.DeleteRepo(repo.ID)
if err != nil {
if temp != "" && repo.Path != "" {
_ = os.Rename(temp, repo.Path)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if temp != "" {
_ = os.RemoveAll(temp)
}
var cloneURL string
var rpmURL string
var dockerURL string
if repo.Type == "git" {
cloneURL = api.cloneURL(project.Slug, repo.Name)
}
if repo.Type == "rpm" {
rpmURL = api.rpmURL(project.Slug, repo.Name)
}
if repo.Type == "docker" {
dockerURL = api.dockerURL(project.Slug, repo.Name)
}
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
}
func moveToTrash(path string) (string, error) {
var dir string
var base string
var temp string
var err error
dir = filepath.Dir(path)
base = filepath.Base(path)
temp = filepath.Join(dir, base+"__deleted__"+strconv.FormatInt(time.Now().UnixNano(), 10))
err = os.Rename(path, temp)
if err != nil {
return "", err
}
return temp, nil
}
func restoreFromTrash(tempPaths []string, sourcePaths []string) {
var i int
if len(tempPaths) != len(sourcePaths) {
return
}
for i = len(tempPaths) - 1; i >= 0; i-- {
if tempPaths[i] == "" || sourcePaths[i] == "" {
continue
}
_ = os.Rename(tempPaths[i], sourcePaths[i])
}
}
func removeTrash(tempPaths []string) {
var i int
for i = 0; i < len(tempPaths); i++ {
if tempPaths[i] == "" {
continue
}
_ = os.RemoveAll(tempPaths[i])
}
}
func (api *API) RepoCommits(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var limit int
var offset int
var v string
var i int
var query string
var ref string
var commits []git.Commit
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
limit = 20
v = r.URL.Query().Get("limit")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil {
limit = i
}
}
offset = 0
v = r.URL.Query().Get("offset")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil {
offset = i
}
}
query = r.URL.Query().Get("q")
ref = r.URL.Query().Get("ref")
commits, err = git.ListCommits(repo.Path, ref, limit, offset, query)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, commits)
}
func (api *API) RepoBranches(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var branches []string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
branches, err = git.ListBranches(repo.Path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, branches)
}
func (api *API) RepoBranchesInfo(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var branches []git.BranchInfo
var info repoBranchesInfo
var ref string
var query string
var i int
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
branches, err = git.ListBranchInfos(repo.Path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
ref, _ = git.GetDefaultBranch(repo.Path)
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
if query != "" {
var filtered []git.BranchInfo
for i = 0; i < len(branches); i++ {
if strings.Contains(strings.ToLower(branches[i].Name), query) {
filtered = append(filtered, branches[i])
}
}
branches = filtered
}
info = repoBranchesInfo{Default: ref, Branches: branches}
WriteJSON(w, http.StatusOK, info)
}
func (api *API) RepoSetDefaultBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var req repoBranchRequest
if !api.requireRepoRole(w, r, params["id"], "admin") {
return
}
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil || strings.TrimSpace(req.Name) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "branch name required"})
return
}
err = git.SetDefaultBranch(repo.Path, strings.TrimSpace(req.Name))
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoDeleteBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var req repoBranchRequest
var name string
if !api.requireRepoRole(w, r, params["id"], "admin") {
return
}
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
name = strings.TrimSpace(req.Name)
if name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "branch name required"})
return
}
err = git.DeleteBranch(repo.Path, name)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoRenameBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var req repoBranchRenameRequest
var from string
var to string
if !api.requireRepoRole(w, r, params["id"], "admin") {
return
}
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
from = strings.TrimSpace(req.From)
to = strings.TrimSpace(req.To)
if from == "" || to == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "from and to required"})
return
}
if from == to {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "new branch name must be different"})
return
}
err = git.RenameBranch(repo.Path, from, to)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoCreateBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var req repoBranchCreateRequest
if !api.requireRepoRole(w, r, params["id"], "admin") {
return
}
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if strings.TrimSpace(req.Name) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "branch name required"})
return
}
err = git.CreateBranch(repo.Path, strings.TrimSpace(req.Name), strings.TrimSpace(req.From))
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoTree(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var ref string
var path string
var entries []git.TreeEntry
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
ref = r.URL.Query().Get("ref")
path = r.URL.Query().Get("path")
entries, err = git.ListTree(repo.Path, ref, path)
if err != nil {
if errors.Is(err, git.ErrPathNotFound) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, entries)
}
func (api *API) RepoBlob(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var ref string
var path string
var content string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
ref = r.URL.Query().Get("ref")
path = r.URL.Query().Get("path")
if path == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
content, err = git.ReadBlob(repo.Path, ref, path, 200*1024)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"path": path, "content": content})
}
func (api *API) RepoBlobRaw(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var ref string
var path string
var content []byte
var contentType string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
ref = r.URL.Query().Get("ref")
path = r.URL.Query().Get("path")
if path == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
content, err = git.ReadBlobBytes(repo.Path, ref, path, 5*1024*1024)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
contentType = http.DetectContentType(content)
w.Header().Set("Content-Type", contentType)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(content)
}
func (api *API) RepoFileHistory(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var ref string
var path string
var limit int
var v string
var i int
var commits []git.Commit
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
ref = r.URL.Query().Get("ref")
path = r.URL.Query().Get("path")
if path == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
limit = 20
v = r.URL.Query().Get("limit")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil {
limit = i
}
}
commits, err = git.ListFileHistory(repo.Path, ref, path, limit)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, commits)
}
func (api *API) RepoFileDiff(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var ref string
var path string
var diff string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
ref = r.URL.Query().Get("ref")
path = r.URL.Query().Get("path")
if path == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
diff, err = git.DiffFile(repo.Path, ref, path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"path": path, "diff": diff})
}
func (api *API) RepoCommitDetail(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var hash string
var detail git.CommitDetail
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
hash = r.URL.Query().Get("hash")
if hash == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "hash required"})
return
}
detail, err = git.GetCommitDetail(repo.Path, hash)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, detail)
}
func (api *API) RepoCommitDiff(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var hash string
var diff string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
hash = r.URL.Query().Get("hash")
if hash == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "hash required"})
return
}
diff, err = git.CommitDiff(repo.Path, hash)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"hash": hash, "diff": diff})
}
func (api *API) RepoCompare(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var base string
var head string
var limit int
var v string
var i int
var commits []git.Commit
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
base = r.URL.Query().Get("base")
head = r.URL.Query().Get("head")
if base == "" || head == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "base and head required"})
return
}
limit = 50
v = r.URL.Query().Get("limit")
if v != "" {
i, err = strconv.Atoi(v)
if err == nil {
limit = i
}
}
commits, err = git.ListCommitsBetween(repo.Path, base, head, limit)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, commits)
}
func (api *API) RepoStats(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var project models.Project
var err error
var branches []string
var ref string
var commits []git.Commit
var stats repoStatsResponse
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
project, err = api.Store.GetProject(repo.ProjectID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
return
}
if repo.Type != "git" {
stats = repoStatsResponse{
RepoID: repo.ID,
Branches: 0,
DefaultRef: "",
ProjectSlug: project.Slug,
Repository: repo.Name,
CloneURL: "",
}
WriteJSON(w, http.StatusOK, stats)
return
}
branches, err = git.ListBranches(repo.Path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
ref = ""
if contains(branches, "main") {
ref = "main"
} else if contains(branches, "master") {
ref = "master"
} else if len(branches) > 0 {
ref = branches[0]
}
if ref != "" {
commits, err = git.ListCommits(repo.Path, ref, 1, 0, "")
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
stats = repoStatsResponse{
RepoID: repo.ID,
Branches: len(branches),
DefaultRef: ref,
ProjectSlug: project.Slug,
Repository: repo.Name,
CloneURL: api.cloneURL(project.Slug, repo.Name),
}
if len(commits) > 0 {
stats.LastCommit = commits[0].Hash
stats.LastAuthor = commits[0].Author
stats.LastWhen = commits[0].When
stats.LastMessage = commits[0].Message
}
WriteJSON(w, http.StatusOK, stats)
}
func (api *API) RepoRPMPackages(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var packages []rpm.PackageSummary
var query string
var filtered []rpm.PackageSummary
var i int
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
packages, err = rpm.ListPackages(repo.Path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
if query == "" {
WriteJSON(w, http.StatusOK, packages)
return
}
filtered = make([]rpm.PackageSummary, 0, len(packages))
for i = 0; i < len(packages); i++ {
if strings.Contains(strings.ToLower(packages[i].Name), query) || strings.Contains(strings.ToLower(packages[i].Filename), query) {
filtered = append(filtered, packages[i])
}
}
WriteJSON(w, http.StatusOK, filtered)
}
func (api *API) RepoRPMPackage(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var filename string
var detail rpm.PackageDetail
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
filename = strings.TrimSpace(r.URL.Query().Get("file"))
if filename == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file is required"})
return
}
detail, err = rpm.GetPackageDetail(repo.Path, filename)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, detail)
}
func (api *API) RepoTypes(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var types []repoTypeItem
types = []repoTypeItem{
{Value: "git", Label: "Git"},
{Value: "rpm", Label: "RPM"},
{Value: "docker", Label: "Docker"},
}
WriteJSON(w, http.StatusOK, types)
}
func (api *API) RepoRPMCreateSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var dirConfig models.RPMRepoDir
var writeBlocked bool
var writeBlockedPath string
var err error
var req repoRPMCreateRequest
var name string
var dirType string
var mode string
var parent string
var parentPath string
var fullPath string
var repodataPath string
var fullRel string
var fullRelLower string
var absParent string
var hasRepoAncestor bool
var allowDelete bool
var tlsInsecureSkipVerify bool
var syncIntervalSec int64
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
name = strings.TrimSpace(req.Name)
if name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
return
}
if strings.EqualFold(name, "repodata") {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata is reserved"})
return
}
if !isSafeSubdirName(name) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid subdirectory name"})
return
}
dirType = strings.ToLower(strings.TrimSpace(req.Type))
if dirType == "" {
dirType = "container"
}
if dirType != "container" && dirType != "repo" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid subdirectory type"})
return
}
parent = strings.TrimSpace(req.Parent)
if parent != "" {
if !isSafeSubdirPath(parent) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parent path"})
return
}
parentPath = filepath.FromSlash(parent)
}
parent = filepath.ToSlash(parentPath)
if parent == "." {
parent = ""
}
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, parent)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if writeBlocked {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
return
}
fullRel = filepath.ToSlash(filepath.Join(parentPath, name))
fullRelLower = strings.ToLower(fullRel)
if fullRelLower == "repodata" || strings.HasPrefix(fullRelLower, "repodata/") || strings.Contains(fullRelLower, "/repodata/") {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "subdirectories under repodata are not allowed"})
return
}
if dirType == "repo" {
mode = normalizeRPMRepoDirMode(req.Mode)
allowDelete = req.AllowDelete
tlsInsecureSkipVerify = req.TLSInsecureSkipVerify
syncIntervalSec = req.SyncIntervalSec
if syncIntervalSec == 0 {
syncIntervalSec = 300
}
syncIntervalSec = normalizeRPMMirrorIntervalSec(syncIntervalSec)
absParent = filepath.Join(repo.Path, parentPath)
hasRepoAncestor, err = hasRepodataAncestor(repo.Path, absParent)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if hasRepoAncestor {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo directories cannot be created under another repo directory"})
return
}
err = validateRPMMirrorConfig(mode, strings.TrimSpace(req.RemoteURL), strings.TrimSpace(req.ConnectHost), strings.TrimSpace(req.HostHeader), strings.TrimSpace(req.TLSServerName))
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
}
fullPath = filepath.Join(repo.Path, parentPath, name)
err = os.MkdirAll(fullPath, 0o755)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if dirType == "repo" {
repodataPath = filepath.Join(fullPath, "repodata")
err = os.MkdirAll(repodataPath, 0o755)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
dirConfig = models.RPMRepoDir{
RepoID: repo.ID,
Path: fullRel,
Mode: mode,
AllowDelete: allowDelete,
RemoteURL: strings.TrimSpace(req.RemoteURL),
ConnectHost: strings.TrimSpace(req.ConnectHost),
HostHeader: strings.TrimSpace(req.HostHeader),
TLSServerName: strings.TrimSpace(req.TLSServerName),
TLSInsecureSkipVerify: tlsInsecureSkipVerify,
SyncIntervalSec: syncIntervalSec,
SyncEnabled: true,
}
err = api.Store.UpsertRPMRepoDir(dirConfig)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoRPMGetSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var relPath string
var normalizedPath string
var config models.RPMRepoDir
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
if err == nil {
WriteJSON(w, http.StatusOK, config)
return
}
if !errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, models.RPMRepoDir{RepoID: repo.ID, Path: normalizedPath, Mode: "local"})
}
func (api *API) RepoRPMSyncSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var relPath string
var normalizedPath string
var config models.RPMRepoDir
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo directory config not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if normalizeRPMRepoDirMode(config.Mode) != "mirror" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sync is only supported for mirror mode"})
return
}
if !config.SyncEnabled {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "mirror sync is suspended"})
return
}
err = api.Store.MarkRPMMirrorTaskDirty(repo.ID, normalizedPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "scheduled"})
}
func (api *API) RepoRPMSuspendSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
api.repoRPMSetSyncEnabled(w, r, params, false)
}
func (api *API) RepoRPMResumeSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
api.repoRPMSetSyncEnabled(w, r, params, true)
}
func (api *API) RepoRPMRebuildSubdirMetadata(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var relPath string
var normalizedPath string
var fullPath string
var repodataPath string
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
fullPath = filepath.Join(repo.Path, filepath.FromSlash(normalizedPath))
repodataPath = filepath.Join(fullPath, "repodata")
_, err = os.Stat(repodataPath)
if err != nil {
if os.IsNotExist(err) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a repository directory"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if api.RpmMeta == nil {
WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "metadata manager unavailable"})
return
}
api.RpmMeta.Schedule(fullPath)
WriteJSON(w, http.StatusOK, map[string]string{"status": "scheduled"})
}
func (api *API) RepoRPMCancelSubdirSync(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var relPath string
var normalizedPath string
var config models.RPMRepoDir
var canceled bool
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo directory config not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if normalizeRPMRepoDirMode(config.Mode) != "mirror" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sync control is only supported for mirror mode"})
return
}
if !config.SyncRunning {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "sync is not running"})
return
}
if api.RpmMirror == nil {
WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "mirror manager unavailable"})
return
}
canceled = api.RpmMirror.CancelTask(repo.ID, normalizedPath)
if !canceled {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "sync is not running"})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "cancel_requested"})
}
func (api *API) repoRPMSetSyncEnabled(w http.ResponseWriter, r *http.Request, params map[string]string, enabled bool) {
var repo models.Repo
var relPath string
var normalizedPath string
var config models.RPMRepoDir
var cancelRequested bool
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo directory config not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if normalizeRPMRepoDirMode(config.Mode) != "mirror" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sync control is only supported for mirror mode"})
return
}
err = api.Store.SetRPMMirrorSyncEnabled(repo.ID, normalizedPath, enabled)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
cancelRequested = false
if !enabled && api.RpmMirror != nil {
cancelRequested = api.RpmMirror.CancelTask(repo.ID, normalizedPath)
}
WriteJSON(w, http.StatusOK, map[string]any{"status": "ok", "sync_enabled": enabled, "cancel_requested": cancelRequested})
}
func (api *API) RepoRPMMirrorRuns(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var relPath string
var normalizedPath string
var limit int
var runs []models.RPMMirrorRun
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
limit = 20
if strings.TrimSpace(r.URL.Query().Get("limit")) != "" {
limit, err = strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
if err != nil || limit <= 0 {
limit = 20
}
}
runs, err = api.Store.ListRPMMirrorRuns(repo.ID, normalizedPath, limit)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, runs)
}
func (api *API) RepoRPMClearMirrorRuns(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var relPath string
var normalizedPath string
var count int64
var err error
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
normalizedPath = normalizeRPMPath(relPath)
count, err = api.Store.DeleteRPMMirrorRuns(repo.ID, normalizedPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]any{"status": "ok", "deleted_count": count})
}
func (api *API) RepoRPMDeleteSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var writeBlocked bool
var writeBlockedPath string
var targetConfig models.RPMRepoDir
var targetHasConfig bool
var mirrorRoot string
var allowMirrorRootDelete bool
var allowMirrorDelete bool
var busy bool
var busyPath string
var busyReason string
var err error
var relPath string
var fullPath string
var info os.FileInfo
var relPathClean string
var parentPath string
var repodataPath string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
if isRepodataPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata cannot be deleted directly"})
return
}
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
relPathClean = strings.TrimPrefix(relPathClean, "./")
busy, busyPath, busyReason, err = api.hasBusyMirrorRootUnder(repo, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if busy {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete directory while mirror activity is running", "mirror_root": busyPath, "reason": busyReason})
return
}
targetConfig, err = api.Store.GetRPMRepoDir(repo.ID, relPathClean)
if err == nil {
targetHasConfig = true
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if writeBlocked {
mirrorRoot = normalizeRPMPath(writeBlockedPath)
allowMirrorRootDelete = targetHasConfig &&
normalizeRPMRepoDirMode(targetConfig.Mode) == "mirror" &&
normalizeRPMPath(targetConfig.Path) == normalizeRPMPath(relPathClean) &&
normalizeRPMPath(relPathClean) == mirrorRoot
if !allowMirrorRootDelete {
allowMirrorDelete, err = api.allowRPMMirrorDelete(repo, relPathClean, true)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !allowMirrorDelete {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
return
}
}
if targetConfig.SyncRunning {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete mirror repo directory while sync is running", "mirror_root": writeBlockedPath})
return
}
}
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
if writeBlocked && allowMirrorRootDelete && api.RpmMeta != nil && api.RpmMeta.IsRunning(fullPath) {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete mirror repo directory while metadata update is running", "mirror_root": writeBlockedPath})
return
}
info, err = os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if info == nil || !info.IsDir() {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a directory"})
return
}
if writeBlocked && !allowMirrorRootDelete && allowMirrorDelete {
repodataPath = filepath.Join(fullPath, "repodata")
_, err = os.Stat(repodataPath)
if err == nil {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "only container directories can be deleted in mirror subtree", "mirror_root": writeBlockedPath})
return
}
if err != nil && !os.IsNotExist(err) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
err = os.RemoveAll(fullPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
err = api.Store.DeleteRPMRepoDirSubtree(repo.ID, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
parentPath = filepath.Dir(fullPath)
repodataPath = filepath.Join(parentPath, "repodata")
_, err = os.Stat(repodataPath)
if err == nil && api.RpmMeta != nil {
api.RpmMeta.Schedule(parentPath)
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoRPMRenameSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var existingConfig models.RPMRepoDir
var dirConfig models.RPMRepoDir
var writeBlocked bool
var writeBlockedPath string
var newMode string
var renamed bool
var isRepoDir bool
var err error
var req repoRPMUpdateRequest
var relPath string
var relPathClean string
var newName string
var fullPath string
var info os.FileInfo
var parentRel string
var parentPath string
var newPath string
var newRelPath string
var repodataPath string
var hasAncestor bool
var absParent string
var existingConfigLoaded bool
var targetConfigExists bool
var allowDelete bool
var tlsInsecureSkipVerify bool
var syncIntervalSec int64
var modeValue string
var remoteURL string
var connectHost string
var hostHeader string
var tlsServerName string
var busy bool
var busyPath string
var busyReason string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Path == nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
relPath = strings.TrimSpace(*req.Path)
newName = ""
if req.Name != nil {
newName = strings.TrimSpace(*req.Name)
}
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
if isRepodataPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata cannot be renamed"})
return
}
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
relPathClean = strings.TrimPrefix(relPathClean, "./")
busy, busyPath, busyReason, err = api.hasBusyMirrorRootUnder(repo, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if busy {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot rename/update directory while mirror activity is running", "mirror_root": busyPath, "reason": busyReason})
return
}
if newName == "" {
newName = filepath.Base(filepath.FromSlash(relPathClean))
}
if strings.EqualFold(newName, "repodata") {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata is reserved"})
return
}
if !isSafeSubdirName(newName) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid name"})
return
}
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if writeBlocked && writeBlockedPath != relPathClean {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
return
}
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
info, err = os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if info == nil || !info.IsDir() {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a directory"})
return
}
parentRel = filepath.Dir(filepath.FromSlash(relPathClean))
if parentRel == "." {
parentRel = ""
}
parentPath = filepath.FromSlash(parentRel)
newPath = filepath.Join(repo.Path, parentPath, newName)
newRelPath = filepath.ToSlash(filepath.Join(parentRel, newName))
_, err = api.Store.GetRPMRepoDir(repo.ID, newRelPath)
if err == nil {
targetConfigExists = true
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
repodataPath = filepath.Join(fullPath, "repodata")
_, err = os.Stat(repodataPath)
if err == nil {
isRepoDir = true
existingConfig, err = api.Store.GetRPMRepoDir(repo.ID, relPathClean)
if err == nil {
existingConfigLoaded = true
if normalizeRPMRepoDirMode(existingConfig.Mode) == "mirror" && existingConfig.SyncRunning {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot rename mirror repo directory while sync is running"})
return
}
if normalizeRPMRepoDirMode(existingConfig.Mode) == "mirror" && api.RpmMeta != nil && api.RpmMeta.IsRunning(fullPath) {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot update mirror repo directory while metadata update is running"})
return
}
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
modeValue = ""
if req.Mode != nil {
modeValue = *req.Mode
}
newMode = normalizeRPMRepoDirMode(modeValue)
if newMode == "" {
if existingConfigLoaded {
newMode = normalizeRPMRepoDirMode(existingConfig.Mode)
} else {
newMode = "local"
}
}
if req.AllowDelete != nil {
allowDelete = *req.AllowDelete
} else if existingConfigLoaded {
allowDelete = existingConfig.AllowDelete
} else {
allowDelete = false
}
if req.TLSInsecureSkipVerify != nil {
tlsInsecureSkipVerify = *req.TLSInsecureSkipVerify
} else if existingConfigLoaded {
tlsInsecureSkipVerify = existingConfig.TLSInsecureSkipVerify
} else {
tlsInsecureSkipVerify = false
}
if req.SyncIntervalSec != nil {
syncIntervalSec = *req.SyncIntervalSec
} else if existingConfigLoaded {
syncIntervalSec = existingConfig.SyncIntervalSec
} else {
syncIntervalSec = 300
}
syncIntervalSec = normalizeRPMMirrorIntervalSec(syncIntervalSec)
if req.RemoteURL != nil {
remoteURL = strings.TrimSpace(*req.RemoteURL)
} else if existingConfigLoaded {
remoteURL = existingConfig.RemoteURL
} else {
remoteURL = ""
}
if req.ConnectHost != nil {
connectHost = strings.TrimSpace(*req.ConnectHost)
} else if existingConfigLoaded {
connectHost = existingConfig.ConnectHost
} else {
connectHost = ""
}
if req.HostHeader != nil {
hostHeader = strings.TrimSpace(*req.HostHeader)
} else if existingConfigLoaded {
hostHeader = existingConfig.HostHeader
} else {
hostHeader = ""
}
if req.TLSServerName != nil {
tlsServerName = strings.TrimSpace(*req.TLSServerName)
} else if existingConfigLoaded {
tlsServerName = existingConfig.TLSServerName
} else {
tlsServerName = ""
}
err = validateRPMMirrorConfig(newMode, remoteURL, connectHost, hostHeader, tlsServerName)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
absParent = filepath.Join(repo.Path, parentPath)
hasAncestor, err = hasRepodataAncestor(repo.Path, absParent)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if hasAncestor {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo directories cannot be renamed under another repo directory"})
return
}
}
renamed = newPath != fullPath
if renamed {
if targetConfigExists {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "target repo directory config already exists"})
return
}
_, err = os.Stat(newPath)
if err == nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "target already exists"})
return
}
if err != nil && !os.IsNotExist(err) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
err = os.Rename(fullPath, newPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
err = api.Store.MoveRPMRepoDir(repo.ID, relPathClean, newRelPath)
if err != nil {
_ = os.Rename(newPath, fullPath)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
if isRepoDir {
dirConfig = models.RPMRepoDir{
RepoID: repo.ID,
Path: newRelPath,
Mode: newMode,
AllowDelete: allowDelete,
RemoteURL: remoteURL,
ConnectHost: connectHost,
HostHeader: hostHeader,
TLSServerName: tlsServerName,
TLSInsecureSkipVerify: tlsInsecureSkipVerify,
SyncIntervalSec: syncIntervalSec,
}
if existingConfigLoaded {
dirConfig.SyncEnabled = existingConfig.SyncEnabled
} else if errors.Is(err, sql.ErrNoRows) {
dirConfig.SyncEnabled = true
} else {
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
dirConfig.SyncEnabled = true
}
err = api.Store.UpsertRPMRepoDir(dirConfig)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
repodataPath = filepath.Join(filepath.Join(repo.Path, parentPath), "repodata")
_, err = os.Stat(repodataPath)
if err == nil && api.RpmMeta != nil {
api.RpmMeta.Schedule(filepath.Join(repo.Path, parentPath))
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoRPMDeleteFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var writeBlocked bool
var writeBlockedPath string
var allowMirrorDelete bool
var err error
var relPath string
var relPathClean string
var fullPath string
var info os.FileInfo
var parentPath string
var repodataPath string
var lower string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
if isRepodataPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata files cannot be deleted"})
return
}
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
relPathClean = strings.TrimPrefix(relPathClean, "./")
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if writeBlocked {
allowMirrorDelete, err = api.allowRPMMirrorDelete(repo, relPathClean, false)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !allowMirrorDelete {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
return
}
}
lower = strings.ToLower(relPathClean)
if !strings.HasSuffix(lower, ".rpm") {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "only rpm files can be deleted"})
return
}
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
info, err = os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if info == nil || info.IsDir() {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a file"})
return
}
err = os.Remove(fullPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
parentPath = filepath.Dir(fullPath)
repodataPath = filepath.Join(parentPath, "repodata")
_, err = os.Stat(repodataPath)
if err == nil && api.RpmMeta != nil {
api.RpmMeta.Schedule(parentPath)
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoRPMFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var relPath string
var cleanPath string
var fullPath string
var lower string
var allowed bool
var file *os.File
var size int64
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if relPath == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
return
}
if !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
cleanPath = filepath.ToSlash(relPath)
cleanPath = strings.TrimPrefix(cleanPath, "./")
lower = strings.ToLower(cleanPath)
allowed = strings.HasSuffix(lower, "repodata/repomd.xml") ||
strings.HasSuffix(lower, "repodata/primary.xml.gz") ||
strings.HasSuffix(lower, "repodata/other.xml.gz") ||
strings.HasSuffix(lower, "repodata/filelists.xml.gz")
if !allowed {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file not allowed"})
return
}
fullPath = filepath.Join(repo.Path, filepath.FromSlash(cleanPath))
file, err = os.Open(fullPath)
if err != nil {
if os.IsNotExist(err) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
defer file.Close()
if strings.HasSuffix(lower, ".gz") {
w.Header().Set("Content-Type", "application/gzip")
} else {
w.Header().Set("Content-Type", "application/xml")
}
size, err = io.Copy(w, file)
_ = size
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
func (api *API) RepoRPMTree(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var relPath string
var entries []rpm.TreeEntry
var repoDirs []models.RPMRepoDir
var modeByPath map[string]string
var i int
var entryPath string
var repodataPath string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
if strings.Contains(relPath, "..") {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
entries, err = rpm.ListTree(repo.Path, relPath)
if err != nil {
if errors.Is(err, rpm.ErrPathNotFound) {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
repoDirs, err = api.Store.ListRPMRepoDirs(repo.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
modeByPath = make(map[string]string)
for i = 0; i < len(repoDirs); i++ {
modeByPath[normalizeRPMPath(repoDirs[i].Path)] = normalizeRPMRepoDirMode(repoDirs[i].Mode)
}
for i = 0; i < len(entries); i++ {
if entries[i].Type != "dir" {
continue
}
entryPath = normalizeRPMPath(entries[i].Path)
if modeByPath[entryPath] != "" {
entries[i].IsRepoDir = true
entries[i].RepoMode = modeByPath[entryPath]
continue
}
repodataPath = filepath.Join(repo.Path, filepath.FromSlash(entryPath), "repodata")
_, err = os.Stat(repodataPath)
if err == nil {
entries[i].IsRepoDir = true
entries[i].RepoMode = "local"
continue
}
if err != nil && !os.IsNotExist(err) {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
WriteJSON(w, http.StatusOK, entries)
}
func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var writeBlocked bool
var writeBlockedPath string
var err error
var relPath string
var relPathClean string
var dirPath string
var repodataDir string
var file multipart.File
var header *multipart.FileHeader
var filename string
var lower string
var fullPath string
var tempPath string
var out *os.File
var size int64
var detail rpm.PackageDetail
var overwriteParam string
var overwrite bool
var info os.FileInfo
var oldTemp string
var ts string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "rpm" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
return
}
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
overwriteParam = strings.TrimSpace(r.URL.Query().Get("overwrite"))
overwriteParam = strings.ToLower(overwriteParam)
overwrite = overwriteParam == "1" || overwriteParam == "true" || overwriteParam == "yes"
if relPath != "" && !isSafeSubdirPath(relPath) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
relPathClean = strings.TrimPrefix(relPathClean, "./")
if relPath == "" {
relPathClean = ""
}
if isRepodataPath(relPathClean) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "uploads are not allowed in repodata"})
return
}
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if writeBlocked {
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
return
}
dirPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
repodataDir, err = nearestRepodataDir(repo.Path, dirPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if repodataDir == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata not found in ancestor directories"})
return
}
file, header, err = r.FormFile("file")
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file required"})
return
}
defer file.Close()
filename = sanitizeFilename(header.Filename)
filename = filepath.Base(filename)
if filename == "" || filename == "." || filename == string(filepath.Separator) {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"})
return
}
lower = strings.ToLower(filename)
if !strings.HasSuffix(lower, ".rpm") {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "only rpm files are allowed"})
return
}
fullPath = filepath.Join(dirPath, filename)
info, err = os.Stat(fullPath)
if err == nil {
if !overwrite {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "file already exists"})
return
}
if info != nil && info.IsDir() {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is a directory"})
return
}
oldTemp, err = moveToTrash(fullPath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
if err != nil && !os.IsNotExist(err) {
if oldTemp != "" {
_ = os.Rename(oldTemp, fullPath)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
ts = strconv.FormatInt(time.Now().UnixNano(), 10)
tempPath = fullPath + ".uploading-" + ts
out, err = os.Create(tempPath)
if err != nil {
if oldTemp != "" {
_ = os.Rename(oldTemp, fullPath)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
defer out.Close()
size, err = io.Copy(out, file)
if err != nil {
_ = os.Remove(tempPath)
if oldTemp != "" {
_ = os.Rename(oldTemp, fullPath)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
err = os.Rename(tempPath, fullPath)
if err != nil {
_ = os.Remove(tempPath)
if oldTemp != "" {
_ = os.Rename(oldTemp, fullPath)
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
detail, err = rpm.GetPackageDetail(repo.Path, filepath.ToSlash(filepath.Join(relPathClean, filename)))
if err != nil {
_ = os.Remove(fullPath)
if oldTemp != "" {
_ = os.Rename(oldTemp, fullPath)
}
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid rpm file"})
return
}
_ = detail
if oldTemp != "" {
_ = os.RemoveAll(oldTemp)
}
if api.RpmMeta != nil {
api.RpmMeta.Schedule(repodataDir)
}
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
}
func (api *API) RepoDockerTags(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var tags []docker.TagInfo
var image string
var imagePath string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
image = strings.TrimSpace(r.URL.Query().Get("image"))
imagePath = docker.ImagePath(repo.Path, image)
tags, err = docker.ListTags(imagePath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, tags)
}
func (api *API) RepoDockerManifest(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var ref string
var detail docker.ManifestDetail
var image string
var imagePath string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
ref = strings.TrimSpace(r.URL.Query().Get("ref"))
if ref == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "ref required"})
return
}
image = strings.TrimSpace(r.URL.Query().Get("image"))
imagePath = docker.ImagePath(repo.Path, image)
detail, err = docker.GetManifestDetail(imagePath, ref)
if err != nil {
if err == docker.ErrNotFound {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "manifest not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, detail)
}
func (api *API) RepoDockerImages(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var images []string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
images, err = docker.ListImages(repo.Path)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, images)
}
func (api *API) ListAPIKeys(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var user models.User
var ok bool
var keys []models.APIKey
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
keys, err = api.Store.ListAPIKeys(user.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, keys)
}
func (api *API) CreateAPIKey(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var user models.User
var ok bool
var req createAPIKeyRequest
var err error
var name string
var buf []byte
var token string
var prefix string
var hash string
var key models.APIKey
var nowUnix int64
var expiresAt int64
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
name = strings.TrimSpace(req.Name)
if name == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
return
}
expiresAt = req.ExpiresAt
if expiresAt < 0 {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "expires_at must be 0 or a future unix timestamp"})
return
}
if expiresAt > 0 {
nowUnix = time.Now().UTC().Unix()
if expiresAt <= nowUnix {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "expires_at must be in the future"})
return
}
}
buf = make([]byte, 32)
_, err = rand.Read(buf)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
return
}
token = "ck_" + hex.EncodeToString(buf)
if len(token) > 8 {
prefix = token[:8]
} else {
prefix = token
}
hash = util.HashToken(token)
key, err = api.Store.CreateAPIKey(user.ID, name, hash, prefix, expiresAt)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, apiKeyResponse{APIKey: key, Token: token})
}
func (api *API) DeleteAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = api.Store.DeleteAPIKey(user.ID, params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) DisableAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = api.Store.SetAPIKeyDisabled(user.ID, params["id"], true)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) EnableAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = api.Store.SetAPIKeyDisabled(user.ID, params["id"], false)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) ListAdminAPIKeys(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var keys []models.AdminAPIKey
var err error
var userID string
var query string
if !api.requireAdmin(w, r) {
return
}
userID = strings.TrimSpace(r.URL.Query().Get("user_id"))
query = strings.TrimSpace(r.URL.Query().Get("q"))
keys, err = api.Store.ListAPIKeysAdmin(userID, query)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, keys)
}
func (api *API) DeleteAdminAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.DeleteAPIKeyByID(params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) DisableAdminAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.SetAPIKeyDisabledByID(params["id"], true)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) EnableAdminAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.Store.SetAPIKeyDisabledByID(params["id"], false)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *API) RepoDockerDeleteTag(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var image string
var tag string
var imagePath string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
image = strings.TrimSpace(r.URL.Query().Get("image"))
tag = strings.TrimSpace(r.URL.Query().Get("tag"))
if tag == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tag required"})
return
}
imagePath = docker.ImagePath(repo.Path, image)
err = docker.DeleteTag(imagePath, tag)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoDockerDeleteImage(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var image string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
image = strings.TrimSpace(r.URL.Query().Get("image"))
err = docker.DeleteImage(repo.Path, image)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoDockerRenameTag(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var req repoDockerRenameTagRequest
var image string
var from string
var to string
var imagePath string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
image = strings.TrimSpace(req.Image)
from = strings.TrimSpace(req.From)
to = strings.TrimSpace(req.To)
if from == "" || to == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "from and to required"})
return
}
imagePath = docker.ImagePath(repo.Path, image)
err = docker.RenameTag(imagePath, from, to)
if err != nil {
if err == docker.ErrNotFound {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "tag not found"})
return
}
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) RepoDockerRenameImage(w http.ResponseWriter, r *http.Request, params map[string]string) {
var repo models.Repo
var err error
var req repoDockerRenameImageRequest
var from string
var to string
repo, err = api.getRepoResolved(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
return
}
if !api.requireRepoRole(w, r, repo.ID, "writer") {
return
}
if repo.Type != "docker" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
from = strings.TrimSpace(req.From)
to = strings.TrimSpace(req.To)
if from == "" && to == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "from or to required"})
return
}
err = docker.RenameImage(repo.Path, from, to)
if err != nil {
if err == docker.ErrNotFound {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "image not found"})
return
}
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) ListIssues(w http.ResponseWriter, r *http.Request, params map[string]string) {
var issues []models.Issue
var err error
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
return
}
issues, err = api.Store.ListIssues(params["projectId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, issues)
}
func (api *API) CreateIssue(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req createIssueRequest
var err error
var user models.User
var issue models.Issue
var created models.Issue
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Title == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "title required"})
return
}
user, _ = middleware.UserFromContext(r.Context())
issue = models.Issue{
ProjectID: params["projectId"],
Title: req.Title,
Body: req.Body,
AssigneeID: req.AssigneeID,
CreatedBy: user.ID,
}
created, err = api.Store.CreateIssue(issue)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) UpdateIssue(w http.ResponseWriter, r *http.Request, params map[string]string) {
var issue models.Issue
var err error
var req updateIssueRequest
issue, err = api.Store.GetIssue(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "issue not found"})
return
}
if !api.requireProjectRole(w, r, issue.ProjectID, "writer") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Title != "" {
issue.Title = req.Title
}
if req.Body != "" {
issue.Body = req.Body
}
if req.Status != "" {
issue.Status = req.Status
}
issue.AssigneeID = req.AssigneeID
err = api.Store.UpdateIssue(issue)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, issue)
}
func (api *API) AddIssueComment(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req issueCommentRequest
var err error
var user models.User
var comment models.IssueComment
var created models.IssueComment
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Body == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "comment body required"})
return
}
user, _ = middleware.UserFromContext(r.Context())
comment = models.IssueComment{IssueID: params["id"], Body: req.Body, CreatedBy: user.ID}
created, err = api.Store.AddIssueComment(comment)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) ListWikiPages(w http.ResponseWriter, r *http.Request, params map[string]string) {
var pages []models.WikiPage
var err error
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
return
}
pages, err = api.Store.ListWikiPages(params["projectId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, pages)
}
func (api *API) CreateWikiPage(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req createWikiRequest
var err error
var user models.User
var page models.WikiPage
var created models.WikiPage
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Title == "" || req.Slug == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "title and slug required"})
return
}
user, _ = middleware.UserFromContext(r.Context())
page = models.WikiPage{
ProjectID: params["projectId"],
Title: req.Title,
Slug: req.Slug,
Body: req.Body,
CreatedBy: user.ID,
}
created, err = api.Store.CreateWikiPage(page)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) UpdateWikiPage(w http.ResponseWriter, r *http.Request, params map[string]string) {
var page models.WikiPage
var err error
var req updateWikiRequest
page, err = api.Store.GetWikiPage(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "page not found"})
return
}
if !api.requireProjectRole(w, r, page.ProjectID, "writer") {
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Title != "" {
page.Title = req.Title
}
page.Body = req.Body
err = api.Store.UpdateWikiPage(page)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, page)
}
func (api *API) UploadFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var file multipart.File
var header *multipart.FileHeader
var err error
var id string
var storedName string
var tempName string
var tempPath string
var finalPath string
var size int64
var contentType string
var upload models.Upload
var created models.Upload
var ts string
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
return
}
user, _ = middleware.UserFromContext(r.Context())
file, header, err = r.FormFile("file")
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file required"})
return
}
defer file.Close()
id, err = util.NewID()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to generate id"})
return
}
storedName = id + filepath.Ext(header.Filename)
ts = strconv.FormatInt(time.Now().UnixNano(), 10)
tempName = storedName + ".uploading-" + ts
tempPath, size, err = api.Uploads.Save(tempName, file)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
finalPath = filepath.Join(api.Uploads.BaseDir, storedName)
err = os.Rename(tempPath, finalPath)
if err != nil {
_ = os.Remove(tempPath)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
contentType = header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
upload = models.Upload{
ID: id,
ProjectID: params["projectId"],
Filename: header.Filename,
ContentType: contentType,
Size: size,
StoragePath: finalPath,
CreatedBy: user.ID,
}
created, err = api.Store.CreateUpload(upload)
if err != nil {
_ = os.Remove(finalPath)
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusCreated, created)
}
func (api *API) ListUploads(w http.ResponseWriter, r *http.Request, params map[string]string) {
var uploads []models.Upload
var err error
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
return
}
uploads, err = api.Store.ListUploads(params["projectId"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
WriteJSON(w, http.StatusOK, uploads)
}
func (api *API) DownloadFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
var upload models.Upload
var err error
var file *os.File
upload, err = api.Store.GetUpload(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "upload not found"})
return
}
if !api.requireProjectRole(w, r, upload.ProjectID, "viewer") {
return
}
file, err = api.Uploads.Open(upload.StoragePath)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
defer file.Close()
w.Header().Set("Content-Type", upload.ContentType)
w.Header().Set("Content-Disposition", "attachment; filename=\""+sanitizeFilename(upload.Filename)+"\"")
_, _ = io.Copy(w, file)
}
func (api *API) Health(w http.ResponseWriter, _ *http.Request, _ map[string]string) {
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (api *API) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
var user models.User
var ok bool
var principal models.ServicePrincipal
user, ok = middleware.UserFromContext(r.Context())
if ok && user.IsAdmin {
return true
}
principal, ok = middleware.PrincipalFromContext(r.Context())
if ok && principal.IsAdmin && !principal.Disabled {
return true
}
w.WriteHeader(http.StatusForbidden)
return false
}
func (api *API) requireProjectRole(w http.ResponseWriter, r *http.Request, projectID, required string) bool {
var user models.User
var principal models.ServicePrincipal
var ok bool
user, ok = middleware.UserFromContext(r.Context())
if ok && user.IsAdmin {
return true
}
var role string
var err error
if ok {
role, err = api.Store.GetProjectMemberRole(projectID, user.ID)
if err != nil {
w.WriteHeader(http.StatusForbidden)
return false
}
if !roleAllows(role, required) {
w.WriteHeader(http.StatusForbidden)
return false
}
return true
}
principal, ok = middleware.PrincipalFromContext(r.Context())
if !ok || principal.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return false
}
if principal.IsAdmin {
return true
}
role, err = api.Store.GetPrincipalProjectRole(principal.ID, projectID)
if err != nil || !roleAllows(role, required) {
w.WriteHeader(http.StatusForbidden)
return false
}
return true
}
func (api *API) requireRepoRole(w http.ResponseWriter, r *http.Request, repoID, required string) bool {
var user models.User
var principal models.ServicePrincipal
var ok bool
var projectIDs []string
var err error
var i int
var role string
user, ok = middleware.UserFromContext(r.Context())
if ok && user.IsAdmin {
return true
}
projectIDs, err = api.Store.GetRepoProjectIDs(repoID)
if err != nil {
w.WriteHeader(http.StatusForbidden)
return false
}
if ok {
for i = 0; i < len(projectIDs); i++ {
role, err = api.Store.GetProjectMemberRole(projectIDs[i], user.ID)
if err != nil {
continue
}
if roleAllows(role, required) {
return true
}
}
w.WriteHeader(http.StatusForbidden)
return false
}
principal, ok = middleware.PrincipalFromContext(r.Context())
if !ok || principal.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return false
}
if principal.IsAdmin {
return true
}
for i = 0; i < len(projectIDs); i++ {
role, err = api.Store.GetPrincipalProjectRole(principal.ID, projectIDs[i])
if err != nil {
continue
}
if roleAllows(role, required) {
return true
}
}
w.WriteHeader(http.StatusForbidden)
return false
}
func normalizeRole(role string) string {
switch strings.ToLower(strings.TrimSpace(role)) {
case "admin":
return "admin"
case "writer", "write":
return "writer"
default:
return "viewer"
}
}
func normalizeProjectHomePage(value string) string {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "repos" || v == "issues" || v == "wiki" || v == "files" || v == "info" {
return v
}
return "info"
}
func normalizeAuthMode(value string) string {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "ldap" || v == "hybrid" || v == "db" {
return v
}
return "db"
}
func (api *API) getMergedAuthSettings() (models.AuthSettings, error) {
var settings models.AuthSettings
var saved models.AuthSettings
var err error
settings = models.AuthSettings{
AuthMode: normalizeAuthMode(api.Cfg.AuthMode),
LDAPURL: api.Cfg.LDAPURL,
LDAPBindDN: api.Cfg.LDAPBindDN,
LDAPBindPassword: api.Cfg.LDAPBindPassword,
LDAPUserBaseDN: api.Cfg.LDAPUserBaseDN,
LDAPUserFilter: api.Cfg.LDAPUserFilter,
LDAPTLSInsecureSkipVerify: api.Cfg.LDAPTLSInsecureSkipVerify,
OIDCClientID: api.Cfg.OIDCClientID,
OIDCClientSecret: api.Cfg.OIDCClientSecret,
OIDCAuthorizeURL: api.Cfg.OIDCAuthorizeURL,
OIDCTokenURL: api.Cfg.OIDCTokenURL,
OIDCUserInfoURL: api.Cfg.OIDCUserInfoURL,
OIDCRedirectURL: api.Cfg.OIDCRedirectURL,
OIDCScopes: api.Cfg.OIDCScopes,
OIDCEnabled: api.Cfg.OIDCEnabled,
OIDCTLSInsecureSkipVerify: api.Cfg.OIDCTLSInsecureSkipVerify,
}
saved, err = api.Store.GetAuthSettings()
if err != nil {
return settings, err
}
if strings.TrimSpace(saved.AuthMode) != "" {
settings.AuthMode = normalizeAuthMode(saved.AuthMode)
}
settings.OIDCEnabled = saved.OIDCEnabled
if strings.ToLower(strings.TrimSpace(saved.AuthMode)) == "oidc" {
settings.OIDCEnabled = true
}
if strings.TrimSpace(saved.LDAPURL) != "" {
settings.LDAPURL = saved.LDAPURL
}
if strings.TrimSpace(saved.LDAPBindDN) != "" {
settings.LDAPBindDN = saved.LDAPBindDN
}
if saved.LDAPBindPassword != "" {
settings.LDAPBindPassword = saved.LDAPBindPassword
}
if strings.TrimSpace(saved.LDAPUserBaseDN) != "" {
settings.LDAPUserBaseDN = saved.LDAPUserBaseDN
}
if strings.TrimSpace(saved.LDAPUserFilter) != "" {
settings.LDAPUserFilter = saved.LDAPUserFilter
}
if settings.LDAPUserFilter == "" {
settings.LDAPUserFilter = "(uid={username})"
}
if strings.TrimSpace(saved.OIDCClientID) != "" {
settings.OIDCClientID = saved.OIDCClientID
}
if strings.TrimSpace(saved.OIDCClientSecret) != "" {
settings.OIDCClientSecret = saved.OIDCClientSecret
}
if strings.TrimSpace(saved.OIDCAuthorizeURL) != "" {
settings.OIDCAuthorizeURL = saved.OIDCAuthorizeURL
}
if strings.TrimSpace(saved.OIDCTokenURL) != "" {
settings.OIDCTokenURL = saved.OIDCTokenURL
}
if strings.TrimSpace(saved.OIDCUserInfoURL) != "" {
settings.OIDCUserInfoURL = saved.OIDCUserInfoURL
}
if strings.TrimSpace(saved.OIDCRedirectURL) != "" {
settings.OIDCRedirectURL = saved.OIDCRedirectURL
}
if strings.TrimSpace(saved.OIDCScopes) != "" {
settings.OIDCScopes = saved.OIDCScopes
}
settings.OIDCTLSInsecureSkipVerify = saved.OIDCTLSInsecureSkipVerify
if strings.TrimSpace(settings.OIDCScopes) == "" {
settings.OIDCScopes = "openid profile email"
}
return settings, nil
}
func (api *API) effectiveAuthConfig() (config.Config, error) {
var cfg config.Config
var settings models.AuthSettings
var err error
cfg = api.Cfg
settings, err = api.getMergedAuthSettings()
if err != nil {
return cfg, err
}
cfg.AuthMode = settings.AuthMode
cfg.LDAPURL = settings.LDAPURL
cfg.LDAPBindDN = settings.LDAPBindDN
cfg.LDAPBindPassword = settings.LDAPBindPassword
cfg.LDAPUserBaseDN = settings.LDAPUserBaseDN
cfg.LDAPUserFilter = settings.LDAPUserFilter
cfg.LDAPTLSInsecureSkipVerify = settings.LDAPTLSInsecureSkipVerify
cfg.OIDCClientID = settings.OIDCClientID
cfg.OIDCClientSecret = settings.OIDCClientSecret
cfg.OIDCAuthorizeURL = settings.OIDCAuthorizeURL
cfg.OIDCTokenURL = settings.OIDCTokenURL
cfg.OIDCUserInfoURL = settings.OIDCUserInfoURL
cfg.OIDCRedirectURL = settings.OIDCRedirectURL
cfg.OIDCScopes = settings.OIDCScopes
cfg.OIDCEnabled = settings.OIDCEnabled
cfg.OIDCTLSInsecureSkipVerify = settings.OIDCTLSInsecureSkipVerify
return cfg, nil
}
func (api *API) getMergedTLSSettings() (models.TLSSettings, error) {
var settings models.TLSSettings
var saved models.TLSSettings
var err error
settings = models.TLSSettings{
HTTPAddrs: normalizeAddrList(api.Cfg.HTTPAddrs),
HTTPSAddrs: normalizeAddrList(api.Cfg.HTTPSAddrs),
TLSServerCertSource: "pki",
TLSCertFile: "",
TLSKeyFile: "",
TLSPKIServerCertID: api.Cfg.TLSPKIServerCertID,
TLSClientAuth: normalizeTLSClientAuth(api.Cfg.TLSClientAuth),
TLSClientCAFile: "",
TLSPKIClientCAID: api.Cfg.TLSPKIClientCAID,
TLSMinVersion: normalizeTLSMinVersion(api.Cfg.TLSMinVersion),
}
saved, err = api.Store.GetTLSSettings()
if err != nil {
return settings, err
}
if len(saved.HTTPAddrs) > 0 {
settings.HTTPAddrs = normalizeAddrList(saved.HTTPAddrs)
}
if len(saved.HTTPSAddrs) > 0 {
settings.HTTPSAddrs = normalizeAddrList(saved.HTTPSAddrs)
}
settings.TLSServerCertSource = "pki"
settings.TLSCertFile = ""
settings.TLSKeyFile = ""
if strings.TrimSpace(saved.TLSPKIServerCertID) != "" {
settings.TLSPKIServerCertID = saved.TLSPKIServerCertID
}
if strings.TrimSpace(saved.TLSClientAuth) != "" {
settings.TLSClientAuth = normalizeTLSClientAuth(saved.TLSClientAuth)
}
settings.TLSClientCAFile = ""
if strings.TrimSpace(saved.TLSPKIClientCAID) != "" {
settings.TLSPKIClientCAID = saved.TLSPKIClientCAID
}
if strings.TrimSpace(saved.TLSMinVersion) != "" {
settings.TLSMinVersion = normalizeTLSMinVersion(saved.TLSMinVersion)
}
return settings, nil
}
func normalizeRepoType(value string) (string, bool) {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "" {
return "git", true
}
if v == "git" || v == "rpm" || v == "docker" {
return v, true
}
return "", false
}
func normalizeAddrList(values []string) []string {
var out []string
var i int
var v string
for i = 0; i < len(values); i++ {
v = strings.TrimSpace(values[i])
if v == "" {
continue
}
out = append(out, v)
}
return out
}
func normalizeTLSServerCertSource(value string) string {
_ = value
return "pki"
}
func normalizeTLSClientAuth(value string) string {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "request" || v == "require" || v == "verify_if_given" || v == "require_and_verify" {
return v
}
return "none"
}
func normalizeTLSMinVersion(value string) string {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "1.0" || v == "1.1" || v == "1.3" || v == "tls1.0" || v == "tls1.1" || v == "tls1.3" {
return v
}
return "1.2"
}
func tlsClientAuthNeedsCA(value string) bool {
var v string
v = normalizeTLSClientAuth(value)
return v == "require_and_verify" || v == "verify_if_given"
}
func tlsClientCAConfigured(pkiCAID string) bool {
return strings.TrimSpace(pkiCAID) != ""
}
func normalizeTLSListenerRequest(req tlsListenerRequest) models.TLSListener {
var item models.TLSListener
var applyAPI bool
var applyGit bool
var applyRPM bool
var applyV2 bool
applyAPI = req.ApplyPolicyAPI
applyGit = req.ApplyPolicyGit
applyRPM = req.ApplyPolicyRPM
applyV2 = req.ApplyPolicyV2
if !applyAPI && !applyGit && !applyRPM && !applyV2 {
applyAPI = true
applyGit = true
applyRPM = true
applyV2 = true
}
item = models.TLSListener{
Name: strings.TrimSpace(req.Name),
Enabled: req.Enabled,
HTTPAddrs: normalizeAddrList(req.HTTPAddrs),
HTTPSAddrs: normalizeAddrList(req.HTTPSAddrs),
AuthPolicy: normalizeTLSAuthPolicy(req.AuthPolicy),
ApplyPolicyAPI: applyAPI,
ApplyPolicyGit: applyGit,
ApplyPolicyRPM: applyRPM,
ApplyPolicyV2: applyV2,
ClientCertAllowlist: normalizeTLSCertAllowlist(req.ClientCertAllowlist),
TLSServerCertSource: "pki",
TLSCertFile: "",
TLSKeyFile: "",
TLSPKIServerCertID: strings.TrimSpace(req.TLSPKIServerCertID),
TLSClientAuth: normalizeTLSClientAuth(req.TLSClientAuth),
TLSClientCAFile: "",
TLSPKIClientCAID: strings.TrimSpace(req.TLSPKIClientCAID),
TLSMinVersion: normalizeTLSMinVersion(req.TLSMinVersion),
}
return item
}
func mergeTLSListener(current models.TLSListener, updated models.TLSListener) models.TLSListener {
var out models.TLSListener
out = current
out.Name = updated.Name
out.Enabled = updated.Enabled
out.HTTPAddrs = updated.HTTPAddrs
out.HTTPSAddrs = updated.HTTPSAddrs
out.AuthPolicy = updated.AuthPolicy
out.ApplyPolicyAPI = updated.ApplyPolicyAPI
out.ApplyPolicyGit = updated.ApplyPolicyGit
out.ApplyPolicyRPM = updated.ApplyPolicyRPM
out.ApplyPolicyV2 = updated.ApplyPolicyV2
out.ClientCertAllowlist = updated.ClientCertAllowlist
out.TLSServerCertSource = updated.TLSServerCertSource
out.TLSCertFile = updated.TLSCertFile
out.TLSKeyFile = updated.TLSKeyFile
out.TLSPKIServerCertID = updated.TLSPKIServerCertID
out.TLSClientAuth = updated.TLSClientAuth
out.TLSClientCAFile = updated.TLSClientCAFile
out.TLSPKIClientCAID = updated.TLSPKIClientCAID
out.TLSMinVersion = updated.TLSMinVersion
return out
}
func normalizeTLSAuthPolicy(value string) string {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "read_open_write_cert" || v == "read_open_write_cert_or_auth" || v == "cert_only" || v == "read_only_public" {
return v
}
return "default"
}
func normalizeTLSCertAllowlist(values []string) []string {
var out []string
var i int
var v string
for i = 0; i < len(values); i++ {
v = strings.ToLower(strings.TrimSpace(values[i]))
if v == "" {
continue
}
out = append(out, v)
}
return out
}
func roleAllows(actual, required string) bool {
actual = normalizeRole(actual)
required = normalizeRole(required)
if actual == "admin" {
return true
}
if required == "admin" {
return actual == "admin"
}
if required == "writer" {
return actual == "writer"
}
return true
}
func sanitizeFilename(name string) string {
name = strings.ReplaceAll(name, "\n", "")
name = strings.ReplaceAll(name, "\r", "")
return name
}
func isSafeSubdirName(name string) bool {
if name == "" {
return false
}
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
return false
}
if strings.Contains(name, "..") {
return false
}
if strings.ContainsAny(name, "\r\n") {
return false
}
return true
}
func isSafeSubdirPath(path string) bool {
var parts []string
var part string
path = strings.ReplaceAll(path, "\\", "/")
if strings.Contains(path, "..") {
return false
}
parts = strings.Split(path, "/")
for _, part = range parts {
if part == "" {
continue
}
if !isSafeSubdirName(part) {
return false
}
}
return true
}
func isRepodataPath(path string) bool {
var normalized string
var parts []string
var part string
normalized = strings.Trim(path, "/")
normalized = strings.ReplaceAll(normalized, "\\", "/")
if normalized == "" {
return false
}
parts = strings.Split(normalized, "/")
for _, part = range parts {
if strings.EqualFold(part, "repodata") {
return true
}
}
return false
}
func hasRepodataAncestor(root string, parent string) (bool, error) {
var current string
var relative string
var err error
var repodata string
current = filepath.Clean(parent)
root = filepath.Clean(root)
for {
relative, err = filepath.Rel(root, current)
if err != nil {
return false, err
}
if strings.HasPrefix(relative, "..") {
return false, nil
}
repodata = filepath.Join(current, "repodata")
_, err = os.Stat(repodata)
if err == nil {
return true, nil
}
if err != nil && !os.IsNotExist(err) {
return false, err
}
if current == root {
return false, nil
}
current = filepath.Dir(current)
}
}
func nearestRepodataDir(root string, target string) (string, error) {
var current string
var relative string
var err error
var repodata string
current = filepath.Clean(target)
root = filepath.Clean(root)
for {
relative, err = filepath.Rel(root, current)
if err != nil {
return "", err
}
if strings.HasPrefix(relative, "..") {
return "", nil
}
repodata = filepath.Join(current, "repodata")
_, err = os.Stat(repodata)
if err == nil {
return current, nil
}
if err != nil && !os.IsNotExist(err) {
return "", err
}
if current == root {
return "", nil
}
current = filepath.Dir(current)
}
}
func normalizeRPMPath(path string) string {
var cleaned string
cleaned = filepath.ToSlash(filepath.Clean(filepath.FromSlash(strings.TrimSpace(path))))
cleaned = strings.TrimPrefix(cleaned, "./")
if cleaned == "." {
return ""
}
return cleaned
}
func normalizeRPMRepoDirMode(mode string) string {
var v string
v = strings.ToLower(strings.TrimSpace(mode))
if v == "mirror" {
return "mirror"
}
return "local"
}
func normalizeRPMMirrorIntervalSec(value int64) int64 {
if value <= 0 {
return 300
}
if value < 10 {
return 10
}
return value
}
func validateRPMMirrorConfig(mode string, remoteURL string, connectHost string, hostHeader string, tlsServerName string) error {
if normalizeRPMRepoDirMode(mode) != "mirror" {
return nil
}
if strings.TrimSpace(remoteURL) == "" {
return errors.New("remote_url is required for mirror mode")
}
if strings.TrimSpace(connectHost) != "" {
if !strings.Contains(strings.TrimSpace(connectHost), ":") {
return errors.New("connect_host must include port")
}
}
_ = hostHeader
_ = tlsServerName
return nil
}
func pathUnderRoot(path string, root string) bool {
if root == "" {
return true
}
if path == root {
return true
}
return strings.HasPrefix(path, root+"/")
}
func (api *API) findRPMMirrorRoot(repo models.Repo, relPath string) (string, error) {
var dirs []models.RPMRepoDir
var normalizedPath string
var i int
var root string
var longest string
var found bool
var err error
dirs, err = api.Store.ListRPMRepoDirs(repo.ID)
if err != nil {
return "", err
}
normalizedPath = normalizeRPMPath(relPath)
for i = 0; i < len(dirs); i++ {
if normalizeRPMRepoDirMode(dirs[i].Mode) != "mirror" {
continue
}
root = normalizeRPMPath(dirs[i].Path)
if !pathUnderRoot(normalizedPath, root) {
continue
}
if !found {
longest = root
found = true
continue
}
if len(root) > len(longest) {
longest = root
}
}
if !found {
return "", nil
}
return longest, nil
}
func (api *API) isRPMWriteBlocked(repo models.Repo, relPath string) (bool, string, error) {
var root string
var err error
root, err = api.findRPMMirrorRoot(repo, relPath)
if err != nil {
return false, "", err
}
if root == "" {
return false, "", nil
}
return true, root, nil
}
func (api *API) allowRPMMirrorDelete(repo models.Repo, relPath string, isDir bool) (bool, error) {
var root string
var cfg models.RPMRepoDir
var normalizedPath string
var err error
root, err = api.findRPMMirrorRoot(repo, relPath)
if err != nil {
return false, err
}
if root == "" {
return true, nil
}
normalizedPath = normalizeRPMPath(relPath)
if normalizedPath == normalizeRPMPath(root) {
return false, nil
}
cfg, err = api.Store.GetRPMRepoDir(repo.ID, normalizeRPMPath(root))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
if !cfg.AllowDelete {
return false, nil
}
if !isDir {
return true, nil
}
_, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
if err == nil {
return false, nil
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, err
}
return true, nil
}
func (api *API) hasBusyMirrorRootUnder(repo models.Repo, relPath string) (bool, string, string, error) {
var dirs []models.RPMRepoDir
var target string
var root string
var full string
var i int
var err error
target = normalizeRPMPath(relPath)
dirs, err = api.Store.ListRPMRepoDirs(repo.ID)
if err != nil {
return false, "", "", err
}
for i = 0; i < len(dirs); i++ {
if normalizeRPMRepoDirMode(dirs[i].Mode) != "mirror" {
continue
}
root = normalizeRPMPath(dirs[i].Path)
if !pathUnderRoot(root, target) {
continue
}
if dirs[i].SyncRunning {
return true, root, "sync_running", nil
}
if api.RpmMeta != nil {
full = filepath.Join(repo.Path, filepath.FromSlash(root))
if api.RpmMeta.IsRunning(full) {
return true, root, "metadata_running", nil
}
}
}
return false, "", "", nil
}
func nameHasWhitespace(name string) bool {
return strings.IndexFunc(name, unicode.IsSpace) >= 0
}
func slugHasWhitespace(slug string) bool {
return strings.IndexFunc(slug, unicode.IsSpace) >= 0
}
func contains(list []string, value string) bool {
var item string
for _, item = range list {
if item == value {
return true
}
}
return false
}
func (api *API) cloneURL(projectSlug, repoName string) string {
var base string
var prefix string
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
prefix = api.Cfg.GitHTTPPrefix
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
return base + prefix + "/" + projectSlug + "/" + repoName + ".git"
}
func (api *API) rpmURL(projectSlug, repoName string) string {
var base string
var prefix string
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
prefix = api.Cfg.RPMHTTPPrefix
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
return base + prefix + "/" + projectSlug + "/" + repoName
}
func (api *API) dockerURL(projectSlug, repoName string) string {
var base string
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
return base + "/v2/" + projectSlug + "/" + repoName
}
func (api *API) getRepoResolved(repoID string) (models.Repo, error) {
var repo models.Repo
var err error
repo, err = api.Store.GetRepo(repoID)
if err != nil {
return repo, err
}
err = api.ensureRepoPath(&repo)
if err != nil {
return repo, err
}
return repo, nil
}
func (api *API) ensureRepoPath(repo *models.Repo) error {
var err error
var projectStorageID int64
var repoStorageID int64
projectStorageID, repoStorageID, err = api.Store.GetRepoStorageIDs(repo.ID)
if err != nil {
return err
}
repo.Path = api.repoStoragePathByType(repo.Type, projectStorageID, repoStorageID)
return nil
}
func (api *API) repoStoragePathByType(repoType string, projectID int64, repoID int64) string {
var projectPart string
var repoPart string
projectPart = fmt.Sprintf("%016x", projectID)
repoPart = fmt.Sprintf("%016x", repoID)
if repoType == "git" {
return filepath.Join(api.Repos.BaseDir, projectPart, repoPart+".git")
}
if repoType == "rpm" {
return filepath.Join(api.RpmBase, projectPart, repoPart)
}
if repoType == "docker" {
return filepath.Join(api.DockerBase, projectPart, repoPart)
}
return ""
}