Compare commits
2 Commits
298d792577
...
cc176b4d29
| Author | SHA1 | Date | |
|---|---|---|---|
| cc176b4d29 | |||
| c95953d078 |
@@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "flag"
|
import "flag"
|
||||||
import "io"
|
//import "io"
|
||||||
import "log"
|
import "log"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "os"
|
import "os"
|
||||||
@@ -211,11 +211,11 @@ func main() {
|
|||||||
log.Fatalf("config error: %v", err)
|
log.Fatalf("config error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetOutput(io.Discard)
|
// log.SetOutput(io.Discard)
|
||||||
//if cfg.APP.LogFile == "" {
|
//if cfg.APP.LogFile == "" {
|
||||||
logger = util.NewLogger("codit", os.Stderr, util.LogStrToMask(nil))
|
logger = util.NewLogger("codit", os.Stderr, util.LogStrsToMask(nil))
|
||||||
//} else {
|
//} else {
|
||||||
// logger, err = util.AppLoggerToFile("codit", cfg.APP.LogFile, cfg.APP.LogMaxSize, cfg.APP.LogRotate, util.LogStrToMask(cfg.APP.LogMask))
|
// logger, err = util.NewLoggerToFile("codit", cfg.APP.LogFile, cfg.APP.LogMaxSize, cfg.APP.LogRotate, util.LogStrsToMask(cfg.APP.LogMask))
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// log.Fatalf("failed to initialize logger - %s", err.Error())
|
// log.Fatalf("failed to initialize logger - %s", err.Error())
|
||||||
// }
|
// }
|
||||||
@@ -347,6 +347,7 @@ func main() {
|
|||||||
router.Handle("GET", "/api/repos/:id/rpm/packages", api.RepoRPMPackages)
|
router.Handle("GET", "/api/repos/:id/rpm/packages", api.RepoRPMPackages)
|
||||||
router.Handle("GET", "/api/repos/:id/rpm/package", api.RepoRPMPackage)
|
router.Handle("GET", "/api/repos/:id/rpm/package", api.RepoRPMPackage)
|
||||||
router.Handle("POST", "/api/repos/:id/rpm/subdirs", api.RepoRPMCreateSubdir)
|
router.Handle("POST", "/api/repos/:id/rpm/subdirs", api.RepoRPMCreateSubdir)
|
||||||
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/rename", api.RepoRPMRenameSubdir)
|
||||||
router.Handle("DELETE", "/api/repos/:id/rpm/subdir", api.RepoRPMDeleteSubdir)
|
router.Handle("DELETE", "/api/repos/:id/rpm/subdir", api.RepoRPMDeleteSubdir)
|
||||||
router.Handle("DELETE", "/api/repos/:id/rpm/file", api.RepoRPMDeleteFile)
|
router.Handle("DELETE", "/api/repos/:id/rpm/file", api.RepoRPMDeleteFile)
|
||||||
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
|
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
|
||||||
@@ -377,10 +378,10 @@ func main() {
|
|||||||
rpmIDRewrite = &rpmIDPathRewriteHandler{next: rpmServer, store: store}
|
rpmIDRewrite = &rpmIDPathRewriteHandler{next: rpmServer, store: store}
|
||||||
mux.Handle(cfg.RPMHTTPPrefix+"-id/", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite))
|
mux.Handle(cfg.RPMHTTPPrefix+"-id/", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite))
|
||||||
mux.Handle("/api/graphql", middleware.WithUser(store, middleware.RequireAuth(graphqlHandler)))
|
mux.Handle("/api/graphql", middleware.WithUser(store, middleware.RequireAuth(graphqlHandler)))
|
||||||
mux.Handle("/api/", middleware.WithUser(store, middleware.RequireAuth(router)))
|
mux.Handle("/api/", middleware.WithUser(store, middleware.AccessLog(logger, middleware.RequireAuth(router))))
|
||||||
mux.Handle("/api/login", middleware.WithUser(store, router))
|
mux.Handle("/api/login", middleware.WithUser(store, middleware.AccessLog(logger, router)))
|
||||||
mux.Handle("/api/logout", middleware.WithUser(store, router))
|
mux.Handle("/api/logout", middleware.WithUser(store, middleware.AccessLog(logger, router)))
|
||||||
mux.Handle("/api/health", router)
|
mux.Handle("/api/health", middleware.AccessLog(logger, router))
|
||||||
mux.Handle("/", middleware.WithUser(store, spaHandler(filepath.Join("..", "frontend", "dist"))))
|
mux.Handle("/", middleware.WithUser(store, spaHandler(filepath.Join("..", "frontend", "dist"))))
|
||||||
|
|
||||||
//log.Printf("codit server listening on %s", cfg.HTTPAddr)
|
//log.Printf("codit server listening on %s", cfg.HTTPAddr)
|
||||||
|
|||||||
@@ -94,3 +94,12 @@ func (r *statusRecorder) WriteHeader(code int) {
|
|||||||
r.status = code
|
r.status = code
|
||||||
r.ResponseWriter.WriteHeader(code)
|
r.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *statusRecorder) Flush() {
|
||||||
|
var flusher http.Flusher
|
||||||
|
var ok bool
|
||||||
|
flusher, ok = r.ResponseWriter.(http.Flusher)
|
||||||
|
if ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ type repoRPMSubdirRequest struct {
|
|||||||
Parent string `json:"parent"`
|
Parent string `json:"parent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type repoRPMRenameRequest struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
type repoRPMUploadResponse struct {
|
type repoRPMUploadResponse struct {
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
@@ -1783,6 +1788,10 @@ func (api *API) RepoRPMCreateSubdir(w http.ResponseWriter, r *http.Request, para
|
|||||||
var parentPath string
|
var parentPath string
|
||||||
var fullPath string
|
var fullPath string
|
||||||
var repodataPath string
|
var repodataPath string
|
||||||
|
var fullRel string
|
||||||
|
var fullRelLower string
|
||||||
|
var absParent string
|
||||||
|
var hasRepoAncestor bool
|
||||||
repo, err = api.Store.GetRepo(params["id"])
|
repo, err = api.Store.GetRepo(params["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||||
@@ -1805,6 +1814,10 @@ func (api *API) RepoRPMCreateSubdir(w http.ResponseWriter, r *http.Request, para
|
|||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.EqualFold(name, "repodata") {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata is reserved"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if !isSafeSubdirName(name) {
|
if !isSafeSubdirName(name) {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid subdirectory name"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid subdirectory name"})
|
||||||
return
|
return
|
||||||
@@ -1825,6 +1838,24 @@ func (api *API) RepoRPMCreateSubdir(w http.ResponseWriter, r *http.Request, para
|
|||||||
}
|
}
|
||||||
parentPath = filepath.FromSlash(parent)
|
parentPath = filepath.FromSlash(parent)
|
||||||
}
|
}
|
||||||
|
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" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
fullPath = filepath.Join(repo.Path, parentPath, name)
|
fullPath = filepath.Join(repo.Path, parentPath, name)
|
||||||
err = os.MkdirAll(fullPath, 0o755)
|
err = os.MkdirAll(fullPath, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1871,6 +1902,10 @@ func (api *API) RepoRPMDeleteSubdir(w http.ResponseWriter, r *http.Request, para
|
|||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isRepodataPath(relPath) {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata cannot be deleted directly"})
|
||||||
|
return
|
||||||
|
}
|
||||||
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPath))
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPath))
|
||||||
info, err = os.Stat(fullPath)
|
info, err = os.Stat(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1899,6 +1934,119 @@ func (api *API) RepoRPMDeleteSubdir(w http.ResponseWriter, r *http.Request, para
|
|||||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
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 err error
|
||||||
|
var req repoRPMRenameRequest
|
||||||
|
var relPath string
|
||||||
|
var newName string
|
||||||
|
var fullPath string
|
||||||
|
var info os.FileInfo
|
||||||
|
var parentRel string
|
||||||
|
var parentPath string
|
||||||
|
var newPath string
|
||||||
|
var repodataPath string
|
||||||
|
var hasAncestor bool
|
||||||
|
var absParent string
|
||||||
|
repo, err = api.Store.GetRepo(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
|
||||||
|
}
|
||||||
|
relPath = strings.TrimSpace(req.Path)
|
||||||
|
newName = strings.TrimSpace(req.Name)
|
||||||
|
if relPath == "" || newName == "" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path and name required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.EqualFold(newName, "repodata") {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata is reserved"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !isSafeSubdirPath(relPath) {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !isSafeSubdirName(newName) {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isRepodataPath(relPath) {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata cannot be renamed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPath))
|
||||||
|
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(relPath))
|
||||||
|
if parentRel == "." {
|
||||||
|
parentRel = ""
|
||||||
|
}
|
||||||
|
parentPath = filepath.FromSlash(parentRel)
|
||||||
|
newPath = filepath.Join(repo.Path, parentPath, newName)
|
||||||
|
if newPath == fullPath {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is unchanged"})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
repodataPath = filepath.Join(fullPath, "repodata")
|
||||||
|
_, err = os.Stat(repodataPath)
|
||||||
|
if err == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = os.Rename(fullPath, newPath)
|
||||||
|
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) {
|
func (api *API) RepoRPMDeleteFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
var repo models.Repo
|
var repo models.Repo
|
||||||
var err error
|
var err error
|
||||||
@@ -1929,6 +2077,10 @@ func (api *API) RepoRPMDeleteFile(w http.ResponseWriter, r *http.Request, params
|
|||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isRepodataPath(relPath) {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata files cannot be deleted"})
|
||||||
|
return
|
||||||
|
}
|
||||||
lower = strings.ToLower(relPath)
|
lower = strings.ToLower(relPath)
|
||||||
if !strings.HasSuffix(lower, ".rpm") {
|
if !strings.HasSuffix(lower, ".rpm") {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "only rpm files can be deleted"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "only rpm files can be deleted"})
|
||||||
@@ -2067,6 +2219,7 @@ func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map
|
|||||||
var err error
|
var err error
|
||||||
var relPath string
|
var relPath string
|
||||||
var dirPath string
|
var dirPath string
|
||||||
|
var repodataDir string
|
||||||
var file multipart.File
|
var file multipart.File
|
||||||
var header *multipart.FileHeader
|
var header *multipart.FileHeader
|
||||||
var filename string
|
var filename string
|
||||||
@@ -2075,7 +2228,6 @@ func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map
|
|||||||
var tempPath string
|
var tempPath string
|
||||||
var out *os.File
|
var out *os.File
|
||||||
var size int64
|
var size int64
|
||||||
var repodataPath string
|
|
||||||
var detail rpm.PackageDetail
|
var detail rpm.PackageDetail
|
||||||
var overwriteParam string
|
var overwriteParam string
|
||||||
var overwrite bool
|
var overwrite bool
|
||||||
@@ -2102,15 +2254,18 @@ func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map
|
|||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if relPath == "repodata" || strings.HasPrefix(relPath, "repodata/") {
|
if isRepodataPath(relPath) {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "uploads are not allowed in repodata"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "uploads are not allowed in repodata"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dirPath = filepath.Join(repo.Path, filepath.FromSlash(relPath))
|
dirPath = filepath.Join(repo.Path, filepath.FromSlash(relPath))
|
||||||
repodataPath = filepath.Join(dirPath, "repodata")
|
repodataDir, err = nearestRepodataDir(repo.Path, dirPath)
|
||||||
_, err = os.Stat(repodataPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata not found in target directory"})
|
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
|
return
|
||||||
}
|
}
|
||||||
file, header, err = r.FormFile("file")
|
file, header, err = r.FormFile("file")
|
||||||
@@ -2197,7 +2352,7 @@ func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map
|
|||||||
_ = os.RemoveAll(oldTemp)
|
_ = os.RemoveAll(oldTemp)
|
||||||
}
|
}
|
||||||
if api.RpmMeta != nil {
|
if api.RpmMeta != nil {
|
||||||
api.RpmMeta.Schedule(dirPath)
|
api.RpmMeta.Schedule(repodataDir)
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
|
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
|
||||||
}
|
}
|
||||||
@@ -2644,6 +2799,84 @@ func isSafeSubdirPath(path string) bool {
|
|||||||
return true
|
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 nameHasWhitespace(name string) bool {
|
func nameHasWhitespace(name string) bool {
|
||||||
return strings.IndexFunc(name, unicode.IsSpace) >= 0
|
return strings.IndexFunc(name, unicode.IsSpace) >= 0
|
||||||
}
|
}
|
||||||
|
|||||||
43
backend/internal/middleware/access-log.go
Normal file
43
backend/internal/middleware/access-log.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
import "codit/internal/models"
|
||||||
|
import "codit/internal/util"
|
||||||
|
|
||||||
|
type statusRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *statusRecorder) WriteHeader(code int) {
|
||||||
|
r.status = code
|
||||||
|
r.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AccessLog(logger *util.Logger, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var recorder *statusRecorder
|
||||||
|
var start time.Time
|
||||||
|
var duration time.Duration
|
||||||
|
var userLabel string
|
||||||
|
var user models.User
|
||||||
|
var ok bool
|
||||||
|
if logger == nil {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recorder = &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||||
|
start = time.Now()
|
||||||
|
next.ServeHTTP(recorder, r)
|
||||||
|
duration = time.Since(start)
|
||||||
|
userLabel = "-"
|
||||||
|
user, ok = UserFromContext(r.Context())
|
||||||
|
if ok && user.Username != "" {
|
||||||
|
userLabel = user.Username
|
||||||
|
}
|
||||||
|
logger.Write("api", util.LOG_INFO, "method=%s path=%s remote=%s user=%s status=%d dur_ms=%d",
|
||||||
|
r.Method, r.URL.Path, r.RemoteAddr, userLabel, recorder.status, duration.Milliseconds())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -280,7 +280,7 @@ func (l* Logger) log_level_to_ansi_code(level LogLevel) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogStrToMask(str []string) LogMask {
|
func LogStrsToMask(str []string) LogMask {
|
||||||
|
|
||||||
var mask LogMask
|
var mask LogMask
|
||||||
|
|
||||||
|
|||||||
@@ -346,6 +346,11 @@ export const api = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name, type, parent })
|
body: JSON.stringify({ name, type, parent })
|
||||||
}),
|
}),
|
||||||
|
renameRpmSubdir: (repoId: string, path: string, name: string) =>
|
||||||
|
request<{ status: string }>(`/api/repos/${repoId}/rpm/subdir/rename`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path, name })
|
||||||
|
}),
|
||||||
deleteRpmSubdir: (repoId: string, path: string) => {
|
deleteRpmSubdir: (repoId: string, path: string) => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.set('path', path)
|
params.set('path', path)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { api, Project, Repo, RpmPackageDetail, RpmPackageSummary, RpmTreeEntry }
|
|||||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
|
||||||
|
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline'
|
||||||
import FolderIcon from '@mui/icons-material/Folder'
|
import FolderIcon from '@mui/icons-material/Folder'
|
||||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
|
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
|
||||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'
|
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'
|
||||||
@@ -70,6 +71,12 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
const [deleteConfirm, setDeleteConfirm] = useState('')
|
const [deleteConfirm, setDeleteConfirm] = useState('')
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null)
|
const [deleteError, setDeleteError] = useState<string | null>(null)
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false)
|
||||||
|
const [renamePath, setRenamePath] = useState('')
|
||||||
|
const [renameName, setRenameName] = useState('')
|
||||||
|
const [renameNewName, setRenameNewName] = useState('')
|
||||||
|
const [renameError, setRenameError] = useState<string | null>(null)
|
||||||
|
const [renaming, setRenaming] = useState(false)
|
||||||
const [rpmPath, setRpmPath] = useState('')
|
const [rpmPath, setRpmPath] = useState('')
|
||||||
const [rpmPathSegments, setRpmPathSegments] = useState<string[]>([])
|
const [rpmPathSegments, setRpmPathSegments] = useState<string[]>([])
|
||||||
const [rpmTree, setRpmTree] = useState<RpmTreeEntry[]>([])
|
const [rpmTree, setRpmTree] = useState<RpmTreeEntry[]>([])
|
||||||
@@ -173,8 +180,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasRepodata = rpmTree.some((entry) => entry.type === 'dir' && entry.name.toLowerCase() === 'repodata')
|
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!repoId) return
|
if (!repoId) return
|
||||||
if (!uploadFiles.length) {
|
if (!uploadFiles.length) {
|
||||||
@@ -283,6 +288,37 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRenameSubdir = async () => {
|
||||||
|
if (!repoId || !renamePath) return
|
||||||
|
if (!renameNewName.trim()) {
|
||||||
|
setRenameError('New name is required.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setRenameError(null)
|
||||||
|
setRenaming(true)
|
||||||
|
try {
|
||||||
|
await api.renameRpmSubdir(repoId, renamePath, renameNewName.trim())
|
||||||
|
setRenameOpen(false)
|
||||||
|
setRenamePath('')
|
||||||
|
setRenameName('')
|
||||||
|
setRenameNewName('')
|
||||||
|
api.listRpmTree(repoId, rpmPath)
|
||||||
|
.then((list) => {
|
||||||
|
setRpmTree(Array.isArray(list) ? list : [])
|
||||||
|
setRpmTreeError(null)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load files'
|
||||||
|
setRpmTreeError(message)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to rename folder'
|
||||||
|
setRenameError(message)
|
||||||
|
} finally {
|
||||||
|
setRenaming(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleRpmBack = () => {
|
const handleRpmBack = () => {
|
||||||
if (!rpmPath) return
|
if (!rpmPath) return
|
||||||
const nextSegments = rpmPathSegments.slice(0, -1)
|
const nextSegments = rpmPathSegments.slice(0, -1)
|
||||||
@@ -346,6 +382,23 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
return new Response(stream).text()
|
return new Response(stream).text()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rpmFileLink = () => {
|
||||||
|
if (!repo || repo.type !== 'rpm' || !repo.rpm_url || !rpmSelectedEntry) return ''
|
||||||
|
const base = repo.rpm_url.replace(/\/+$/, '')
|
||||||
|
const path = rpmSelectedEntry.path.replace(/^\/+/, '')
|
||||||
|
return `${base}/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpmFileLabel = () => {
|
||||||
|
if (!rpmSelected) return ''
|
||||||
|
return rpmSelected.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttpUrl = (value: string) => {
|
||||||
|
const trimmed = value.trim().toLowerCase()
|
||||||
|
return trimmed.startsWith('http://') || trimmed.startsWith('https://')
|
||||||
|
}
|
||||||
|
|
||||||
const handleRpmEntry = (entry: RpmTreeEntry) => {
|
const handleRpmEntry = (entry: RpmTreeEntry) => {
|
||||||
if (entry.type === 'dir') {
|
if (entry.type === 'dir') {
|
||||||
setRpmPath(entry.path)
|
setRpmPath(entry.path)
|
||||||
@@ -450,7 +503,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
setUploadFiles([])
|
setUploadFiles([])
|
||||||
setUploadOpen(true)
|
setUploadOpen(true)
|
||||||
}}
|
}}
|
||||||
disabled={!hasRepodata}
|
|
||||||
>
|
>
|
||||||
Upload RPM...
|
Upload RPM...
|
||||||
</Button>
|
</Button>
|
||||||
@@ -520,39 +572,59 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
{entry.type === 'dir' && canWrite ? (
|
{canWrite ? (
|
||||||
<IconButton
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, pr: 0.5 }}>
|
||||||
size="small"
|
{entry.type === 'dir' && entry.name.toLowerCase() !== 'repodata' ? (
|
||||||
onClick={(event) => {
|
<IconButton
|
||||||
event.stopPropagation()
|
size="small"
|
||||||
setDeleteError(null)
|
onClick={(event) => {
|
||||||
setDeletePath(entry.path)
|
event.stopPropagation()
|
||||||
setDeleteName(entry.name)
|
setRenameError(null)
|
||||||
setDeleteIsFile(false)
|
setRenamePath(entry.path)
|
||||||
setDeleteConfirm('')
|
setRenameName(entry.name)
|
||||||
setDeleteOpen(true)
|
setRenameNewName(entry.name)
|
||||||
}}
|
setRenameOpen(true)
|
||||||
aria-label={`Delete folder ${entry.name}`}
|
}}
|
||||||
>
|
aria-label={`Rename folder ${entry.name}`}
|
||||||
<DeleteOutlineIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<DriveFileRenameOutlineIcon fontSize="small" />
|
||||||
) : null}
|
</IconButton>
|
||||||
{entry.type !== 'dir' && entry.name.toLowerCase().endsWith('.rpm') && canWrite ? (
|
) : null}
|
||||||
<IconButton
|
{entry.type === 'dir' ? (
|
||||||
size="small"
|
<IconButton
|
||||||
onClick={(event) => {
|
size="small"
|
||||||
event.stopPropagation()
|
onClick={(event) => {
|
||||||
setDeleteError(null)
|
event.stopPropagation()
|
||||||
setDeletePath(entry.path)
|
setDeleteError(null)
|
||||||
setDeleteName(entry.name)
|
setDeletePath(entry.path)
|
||||||
setDeleteIsFile(true)
|
setDeleteName(entry.name)
|
||||||
setDeleteConfirm('')
|
setDeleteIsFile(false)
|
||||||
setDeleteOpen(true)
|
setDeleteConfirm('')
|
||||||
}}
|
setDeleteOpen(true)
|
||||||
aria-label={`Delete file ${entry.name}`}
|
}}
|
||||||
>
|
aria-label={`Delete folder ${entry.name}`}
|
||||||
<DeleteOutlineIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
{entry.type !== 'dir' && entry.name.toLowerCase().endsWith('.rpm') ? (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setDeleteError(null)
|
||||||
|
setDeletePath(entry.path)
|
||||||
|
setDeleteName(entry.name)
|
||||||
|
setDeleteIsFile(true)
|
||||||
|
setDeleteConfirm('')
|
||||||
|
setDeleteOpen(true)
|
||||||
|
}}
|
||||||
|
aria-label={`Delete file ${entry.name}`}
|
||||||
|
>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
@@ -594,14 +666,36 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
{rpmDetail && rpmTab === 'meta' ? (
|
{rpmDetail && rpmTab === 'meta' ? (
|
||||||
<Box sx={{ mt: 1, display: 'grid', gap: 0.5 }}>
|
<Box sx={{ mt: 1, display: 'grid', gap: 0.5 }}>
|
||||||
<Typography variant="body2">Name: {rpmDetail.name}</Typography>
|
<Typography variant="body2">
|
||||||
|
Name:{' '}
|
||||||
|
{rpmFileLink() ? (
|
||||||
|
<a href={rpmFileLink()} target="_blank" rel="noreferrer">
|
||||||
|
{rpmDetail.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
rpmDetail.name
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Version: {rpmDetail.version}-{rpmDetail.release}
|
Version: {rpmDetail.version}-{rpmDetail.release}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">Arch: {rpmDetail.arch}</Typography>
|
<Typography variant="body2">Arch: {rpmDetail.arch}</Typography>
|
||||||
<Typography variant="body2">Summary: {rpmDetail.summary}</Typography>
|
<Typography variant="body2">Summary: {rpmDetail.summary}</Typography>
|
||||||
<Typography variant="body2">License: {rpmDetail.license || 'n/a'}</Typography>
|
<Typography variant="body2">License: {rpmDetail.license || 'n/a'}</Typography>
|
||||||
<Typography variant="body2">URL: {rpmDetail.url || 'n/a'}</Typography>
|
<Typography variant="body2">
|
||||||
|
URL:{' '}
|
||||||
|
{rpmDetail.url ? (
|
||||||
|
isHttpUrl(rpmDetail.url) ? (
|
||||||
|
<a href={rpmDetail.url} target="_blank" rel="noreferrer">
|
||||||
|
{rpmDetail.url}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
rpmDetail.url
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Build Time: {rpmDetail.build_time ? new Date(rpmDetail.build_time * 1000).toLocaleString() : 'n/a'}
|
Build Time: {rpmDetail.build_time ? new Date(rpmDetail.build_time * 1000).toLocaleString() : 'n/a'}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -712,11 +806,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
<DialogTitle>Upload RPM</DialogTitle>
|
<DialogTitle>Upload RPM</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{uploadError ? <Alert severity="error">{uploadError}</Alert> : null}
|
{uploadError ? <Alert severity="error">{uploadError}</Alert> : null}
|
||||||
{!hasRepodata ? (
|
|
||||||
<Alert severity="warning" sx={{ mt: 1 }}>
|
|
||||||
Upload is only allowed in a directory with a repodata folder.
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
<Box sx={{ display: 'grid', gap: 1, mt: 1 }}>
|
<Box sx={{ display: 'grid', gap: 1, mt: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
type="file"
|
type="file"
|
||||||
@@ -735,7 +824,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setUploadOpen(false)}>Cancel</Button>
|
<Button onClick={() => setUploadOpen(false)}>Cancel</Button>
|
||||||
<Button onClick={handleUpload} variant="contained" disabled={uploading || !hasRepodata}>
|
<Button onClick={handleUpload} variant="contained" disabled={uploading}>
|
||||||
{uploading ? 'Uploading...' : 'Upload'}
|
{uploading ? 'Uploading...' : 'Upload'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -769,6 +858,28 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<Dialog open={renameOpen} onClose={() => setRenameOpen(false)} maxWidth="xs" fullWidth>
|
||||||
|
<DialogTitle>Rename folder</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{renameError ? <Alert severity="error">{renameError}</Alert> : null}
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Current name: {renameName}
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label="New name"
|
||||||
|
value={renameNewName}
|
||||||
|
onChange={(event) => setRenameNewName(event.target.value)}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setRenameOpen(false)}>Cancel</Button>
|
||||||
|
<Button onClick={handleRenameSubdir} variant="contained" disabled={renaming || !renameNewName.trim()}>
|
||||||
|
{renaming ? 'Renaming...' : 'Rename'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user