Compare commits

...

8 Commits

42 changed files with 2626 additions and 842 deletions

21
backend/Makefile Normal file
View File

@@ -0,0 +1,21 @@
CODIT_SERVER_NAME=codit-server
CODIT_SERVER_VERSION=0.5.0
CODIT_SERVER_SRCS = cmd/codit-server/main.go
CODIT_DATA_BROWSER_SRCS = cmd/codit-data-browser/main.go
all: codit-server codit-data-browser
codit-server:
CGO_ENABLED=0 go build -x -ldflags "-X 'main.CODIT_SERVER_NAME=$(NAME)' -X 'main.CODIT_SERVER_VERSION=$(VERSION)'" -o $@ $(CODIT_SERVER_SRCS)
codit-server.debug:
CGO_ENABLED=1 go build -race -x -ldflags "-X 'main.CODIT_SERVER_NAME=$(NAME)' -X 'main.CODIT_SERVER_VERSION=$(VERSION)'" -o $@ $(CODIT_SERVER_SRCS)
codit-data-browser:
CGO_ENABLED=0 go build -x -ldflags "-X 'main.CODIT_SERVER_NAME=$(NAME)' -X 'main.CODIT_SERVER_VERSION=$(VERSION)'" -o $@ $(CODIT_DATA_BROWSER_SRCS)
clean:
go clean -x
rm -rf codit-server codit-server.debug codit-data-browser

View File

@@ -0,0 +1,572 @@
package main
import "database/sql"
import "flag"
import "fmt"
import "os"
import "path/filepath"
import "sort"
import "strconv"
import "strings"
import _ "modernc.org/sqlite"
import "github.com/gdamore/tcell/v2"
import "github.com/rivo/tview"
type projectInfo struct {
ID int64
PublicID string
Slug string
Name string
}
type repoInfo struct {
ID int64
PublicID string
ProjectID int64
Name string
RepoType string
LegacyPath string
}
type entryInfo struct {
Name string
IsDir bool
Hint string
}
type browser struct {
DB *sql.DB
DataDir string
Cwd string
ProjectsByID map[int64]projectInfo
ReposByID map[int64]repoInfo
Entries []entryInfo
App *tview.Application
Root tview.Primitive
Header *tview.TextView
Table *tview.Table
Status *tview.TextView
}
func main() {
var dbPath string
var dataDir string
var err error
var br *browser
flag.StringVar(&dbPath, "db", "./codit-data/codit.db", "sqlite database path")
flag.StringVar(&dataDir, "data", "./codit-data", "codit data directory")
flag.Parse()
br, err = newBrowser(dbPath, dataDir)
if err != nil {
fmt.Fprintf(os.Stderr, "init error: %v\n", err)
os.Exit(1)
}
err = br.run()
if err != nil {
fmt.Fprintf(os.Stderr, "run error: %v\n", err)
os.Exit(1)
}
}
func newBrowser(dbPath string, dataDir string) (*browser, error) {
var err error
var db *sql.DB
var br *browser
db, err = sql.Open("sqlite", "file:"+dbPath+"?_pragma=foreign_keys(1)")
if err != nil {
return nil, err
}
br = &browser{
DB: db,
DataDir: filepath.Clean(dataDir),
Cwd: filepath.Clean(dataDir),
ProjectsByID: map[int64]projectInfo{},
ReposByID: map[int64]repoInfo{},
App: tview.NewApplication(),
Header: tview.NewTextView(),
Table: tview.NewTable(),
Status: tview.NewTextView(),
}
err = br.loadMaps()
if err != nil {
_ = db.Close()
return nil, err
}
err = br.refreshEntries()
if err != nil {
_ = db.Close()
return nil, err
}
br.setupUI()
br.renderAll("Ready. q quit, Enter/Right open, Left/Backspace up, PgUp/PgDn/Home/End, r reload, i info, c check")
return br, nil
}
func (b *browser) run() error {
var err error
b.Root = b.layout()
err = b.App.SetRoot(b.Root, true).EnableMouse(false).Run()
_ = b.DB.Close()
return err
}
func (b *browser) layout() tview.Primitive {
var root *tview.Flex
root = tview.NewFlex().SetDirection(tview.FlexRow)
root.AddItem(b.Header, 1, 0, false)
root.AddItem(b.Table, 0, 1, true)
root.AddItem(b.Status, 1, 0, false)
return root
}
func (b *browser) setupUI() {
var header string
header = "Type Name Hint"
b.Header.SetDynamicColors(true)
b.Header.SetText(header)
b.Table.SetSelectable(true, false)
b.Table.SetFixed(0, 0)
b.Table.SetBorders(false)
b.Table.SetSeparator(' ')
b.Table.SetInputCapture(b.captureKey)
b.Table.SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorLightCyan))
}
func (b *browser) renderAll(status string) {
var i int
var e entryInfo
var kind string
var name string
var rel string
var err error
var row int
b.Table.Clear()
for i = 0; i < len(b.Entries); i++ {
e = b.Entries[i]
if e.IsDir {
kind = "DIR "
} else {
kind = "FILE"
}
name = e.Name
row = i
b.Table.SetCell(row, 0, tview.NewTableCell(kind).SetSelectable(false))
b.Table.SetCell(row, 1, tview.NewTableCell(name))
b.Table.SetCell(row, 2, tview.NewTableCell(e.Hint).SetSelectable(false))
}
if len(b.Entries) > 0 {
b.Table.Select(0, 1)
}
rel, err = filepath.Rel(b.DataDir, b.Cwd)
if err != nil {
rel = b.Cwd
}
if rel == "." {
rel = "/"
} else {
rel = "/" + filepath.ToSlash(rel)
}
b.Header.SetText("Path: " + rel + " (entries: " + strconv.Itoa(len(b.Entries)) + ")")
b.Status.SetText(status)
}
func (b *browser) captureKey(event *tcell.EventKey) *tcell.EventKey {
var key tcell.Key
var r rune
var row int
var _, _, _, h = b.Table.GetInnerRect()
var page int
var err error
var info string
var mismatches int
key = event.Key()
r = event.Rune()
if key == tcell.KeyEnter || key == tcell.KeyRight {
err = b.openSelected()
if err != nil {
b.Status.SetText("open failed: " + err.Error())
}
return nil
}
if key == tcell.KeyLeft || key == tcell.KeyBackspace || key == tcell.KeyBackspace2 {
err = b.up()
if err != nil {
b.Status.SetText("up failed: " + err.Error())
}
return nil
}
if key == tcell.KeyHome {
if len(b.Entries) > 0 {
b.Table.Select(0, 1)
}
return nil
}
if key == tcell.KeyEnd {
if len(b.Entries) > 0 {
b.Table.Select(len(b.Entries)-1, 1)
}
return nil
}
if key == tcell.KeyPgUp || key == tcell.KeyPgDn {
row, _ = b.Table.GetSelection()
page = h
if page < 1 {
page = 10
}
if key == tcell.KeyPgUp {
row = row - page
if row < 0 {
row = 0
}
} else {
row = row + page
if row >= len(b.Entries) {
row = len(b.Entries) - 1
}
if row < 0 {
row = 0
}
}
if len(b.Entries) > 0 {
b.Table.Select(row, 1)
}
return nil
}
if r == 'q' || r == 'Q' {
b.App.Stop()
return nil
}
if r == 'r' || r == 'R' {
b.ProjectsByID = map[int64]projectInfo{}
b.ReposByID = map[int64]repoInfo{}
err = b.loadMaps()
if err != nil {
b.Status.SetText("reload failed: " + err.Error())
return nil
}
err = b.refreshEntries()
if err != nil {
b.Status.SetText("reload list failed: " + err.Error())
return nil
}
b.renderAll("Reloaded")
return nil
}
if r == 'i' || r == 'I' {
info = b.selectedInfo()
b.Status.SetText(info)
return nil
}
if r == 'c' || r == 'C' {
mismatches = b.showMismatches()
if mismatches == 0 {
b.Status.SetText("check complete: no missing expected repo directories")
}
return nil
}
return event
}
func (b *browser) openSelected() error {
var row int
var name string
var err error
row, _ = b.Table.GetSelection()
if row < 0 || row >= len(b.Entries) {
return nil
}
if !b.Entries[row].IsDir {
return nil
}
name = b.Entries[row].Name
b.Cwd = filepath.Clean(filepath.Join(b.Cwd, name))
if !pathInside(b.Cwd, b.DataDir) {
return fmt.Errorf("outside data dir")
}
err = b.refreshEntries()
if err != nil {
return err
}
b.renderAll("Opened " + b.Cwd)
return nil
}
func (b *browser) up() error {
var parent string
var err error
parent = filepath.Dir(b.Cwd)
if !pathInside(parent, b.DataDir) {
return nil
}
b.Cwd = parent
err = b.refreshEntries()
if err != nil {
return err
}
b.renderAll("Opened " + b.Cwd)
return nil
}
func (b *browser) selectedInfo() string {
var row int
var e entryInfo
row, _ = b.Table.GetSelection()
if row < 0 || row >= len(b.Entries) {
return "no selection"
}
e = b.Entries[row]
if e.Hint == "" {
return e.Name
}
return e.Name + " | " + e.Hint
}
func (b *browser) refreshEntries() error {
var raw []os.DirEntry
var i int
var item os.DirEntry
var entries []entryInfo
var info entryInfo
var dirs []entryInfo
var files []entryInfo
var err error
raw, err = os.ReadDir(b.Cwd)
if err != nil {
return err
}
entries = make([]entryInfo, 0, len(raw))
for i = 0; i < len(raw); i++ {
item = raw[i]
info = entryInfo{
Name: item.Name(),
IsDir: item.IsDir(),
Hint: b.resolvePathHint(item.Name()),
}
entries = append(entries, info)
}
dirs = make([]entryInfo, 0, len(entries))
files = make([]entryInfo, 0, len(entries))
for i = 0; i < len(entries); i++ {
if entries[i].IsDir {
dirs = append(dirs, entries[i])
} else {
files = append(files, entries[i])
}
}
sort.Slice(dirs, func(i int, j int) bool {
return strings.ToLower(dirs[i].Name) < strings.ToLower(dirs[j].Name)
})
sort.Slice(files, func(i int, j int) bool {
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
})
b.Entries = make([]entryInfo, 0, len(entries))
b.Entries = append(b.Entries, dirs...)
b.Entries = append(b.Entries, files...)
return nil
}
func (b *browser) loadMaps() error {
var err error
var rows *sql.Rows
var p projectInfo
var r repoInfo
rows, err = b.DB.Query(`SELECT id, public_id, slug, name FROM projects`)
if err != nil {
return err
}
for rows.Next() {
err = rows.Scan(&p.ID, &p.PublicID, &p.Slug, &p.Name)
if err != nil {
rows.Close()
return err
}
b.ProjectsByID[p.ID] = p
}
err = rows.Close()
if err != nil {
return err
}
rows, err = b.DB.Query(`SELECT id, public_id, project_id, name, type, path FROM repos`)
if err != nil {
return err
}
for rows.Next() {
err = rows.Scan(&r.ID, &r.PublicID, &r.ProjectID, &r.Name, &r.RepoType, &r.LegacyPath)
if err != nil {
rows.Close()
return err
}
b.ReposByID[r.ID] = r
}
err = rows.Close()
if err != nil {
return err
}
return nil
}
func (b *browser) resolvePathHint(name string) string {
var rel string
var parts []string
var service string
var projectID int64
var repoID int64
var project projectInfo
var repo repoInfo
var err error
rel, err = filepath.Rel(b.DataDir, filepath.Join(b.Cwd, name))
if err != nil {
return ""
}
parts = strings.Split(filepath.ToSlash(rel), "/")
if len(parts) < 2 {
return ""
}
service = parts[0]
projectID, err = parseStorageIDSegment(parts[1])
if err == nil {
project = b.ProjectsByID[projectID]
if project.ID > 0 && len(parts) == 2 {
return fmt.Sprintf("project: %s (%s)", project.Slug, project.PublicID)
}
}
if len(parts) < 3 {
return ""
}
if service == "git" {
parts[2] = strings.TrimSuffix(parts[2], ".git")
}
repoID, err = parseStorageIDSegment(parts[2])
if err != nil {
return ""
}
repo = b.ReposByID[repoID]
if repo.ID == 0 {
return ""
}
return fmt.Sprintf("repo: %s (%s) type=%s", repo.Name, repo.PublicID, repo.RepoType)
}
func (b *browser) countMismatches() int {
var ids []int64
var repoID int64
var repo repoInfo
var expected string
var err error
var count int
ids = make([]int64, 0, len(b.ReposByID))
for repoID = range b.ReposByID {
ids = append(ids, repoID)
}
sort.Slice(ids, func(i int, j int) bool { return ids[i] < ids[j] })
count = 0
for _, repoID = range ids {
repo = b.ReposByID[repoID]
expected = expectedRepoPath(b.DataDir, repo.RepoType, repo.ProjectID, repo.ID)
_, err = os.Stat(expected)
if err != nil {
count = count + 1
}
}
return count
}
func (b *browser) showMismatches() int {
var lines []string
var text string
var modal *tview.Modal
lines = b.collectMismatchLines()
if len(lines) == 0 {
return 0
}
text = strings.Join(lines, "\n")
modal = tview.NewModal().
SetText(text).
AddButtons([]string{"Close"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
b.App.SetRoot(b.Root, true).SetFocus(b.Table)
})
b.App.SetRoot(modal, true).SetFocus(modal)
return len(lines)
}
func (b *browser) collectMismatchLines() []string {
var ids []int64
var repoID int64
var repo repoInfo
var project projectInfo
var expected string
var lines []string
var err error
ids = make([]int64, 0, len(b.ReposByID))
for repoID = range b.ReposByID {
ids = append(ids, repoID)
}
sort.Slice(ids, func(i int, j int) bool { return ids[i] < ids[j] })
lines = []string{}
for _, repoID = range ids {
repo = b.ReposByID[repoID]
expected = expectedRepoPath(b.DataDir, repo.RepoType, repo.ProjectID, repo.ID)
_, err = os.Stat(expected)
if err == nil {
continue
}
project = b.ProjectsByID[repo.ProjectID]
lines = append(lines, fmt.Sprintf("%s/%s [%s]", project.Slug, repo.Name, repo.RepoType))
lines = append(lines, "expected: "+expected)
lines = append(lines, "current : "+repo.LegacyPath)
lines = append(lines, "")
}
if len(lines) > 0 {
lines = lines[:len(lines)-1]
}
return lines
}
func expectedRepoPath(dataDir string, repoType string, projectID int64, repoID int64) string {
var p string
var r string
p = formatStorageIDSegment(projectID)
r = formatStorageIDSegment(repoID)
if repoType == "git" {
return filepath.Join(dataDir, "git", p, r+".git")
}
if repoType == "rpm" {
return filepath.Join(dataDir, "rpm", p, r)
}
if repoType == "docker" {
return filepath.Join(dataDir, "docker", p, r)
}
return ""
}
func formatStorageIDSegment(id int64) string {
return fmt.Sprintf("%016x", id)
}
func parseStorageIDSegment(s string) (int64, error) {
var parsed int64
var err error
var trimmed string
trimmed = strings.TrimSpace(s)
parsed, err = strconv.ParseInt(trimmed, 16, 64)
if err == nil {
return parsed, nil
}
return strconv.ParseInt(trimmed, 10, 64)
}
func pathInside(path string, root string) bool {
var p string
var r string
var prefix string
p = filepath.Clean(path)
r = filepath.Clean(root)
if p == r {
return true
}
prefix = r + string(os.PathSeparator)
return strings.HasPrefix(p, prefix)
}

View File

@@ -38,6 +38,21 @@ type gitPathRewriteHandler struct {
store *db.Store store *db.Store
} }
type server_http_log_writer struct {
l *util.Logger
id string
depth int
}
func (hlw *server_http_log_writer) Write(p []byte) (n int, err error) {
// the standard http.Server always requires *log.Logger
// use this iowriter to create a logger to pass it to the http server.
// since this is another log write wrapper, give adjustment value
hlw.l.WriteWithCallDepth(hlw.id, util.LOG_INFO, hlw.depth, string(p))
return len(p), nil
}
func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var path string var path string
var parts []string var parts []string
@@ -47,6 +62,8 @@ func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
var repoName string var repoName string
var project models.Project var project models.Project
var repo models.Repo var repo models.Repo
var projectStorageID int64
var repoStorageID int64
var err error var err error
var newPath string var newPath string
path = strings.TrimPrefix(r.URL.Path, "/") path = strings.TrimPrefix(r.URL.Path, "/")
@@ -77,7 +94,12 @@ func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
newPath = "/" + project.ID + "/" + repo.Name + ".git" + rest projectStorageID, repoStorageID, err = h.store.GetRepoStorageIDs(repo.ID)
if err != nil {
http.NotFound(w, r)
return
}
newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest
r.URL.Path = newPath r.URL.Path = newPath
h.next.ServeHTTP(w, r) h.next.ServeHTTP(w, r)
} }
@@ -92,6 +114,8 @@ func (h *gitIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
var parts []string var parts []string
var rest string var rest string
var repo models.Repo var repo models.Repo
var projectStorageID int64
var repoStorageID int64
var err error var err error
var newPath string var newPath string
var repoID string var repoID string
@@ -107,6 +131,11 @@ func (h *gitIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
projectStorageID, repoStorageID, err = h.store.GetRepoStorageIDs(repo.ID)
if err != nil {
http.NotFound(w, r)
return
}
if len(parts) == 3 { if len(parts) == 3 {
rest = "/" + parts[1] + "/" + parts[2] rest = "/" + parts[1] + "/" + parts[2]
} else if len(parts) == 2 { } else if len(parts) == 2 {
@@ -114,7 +143,7 @@ func (h *gitIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
} else { } else {
rest = "" rest = ""
} }
newPath = "/" + repo.ProjectID + "/" + repo.Name + ".git" + rest newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest
r.URL.Path = newPath r.URL.Path = newPath
h.next.ServeHTTP(w, r) h.next.ServeHTTP(w, r)
} }
@@ -132,6 +161,8 @@ func (h *rpmPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
var rest string var rest string
var project models.Project var project models.Project
var repo models.Repo var repo models.Repo
var projectStorageID int64
var repoStorageID int64
var err error var err error
var newPath string var newPath string
path = strings.TrimPrefix(r.URL.Path, "/") path = strings.TrimPrefix(r.URL.Path, "/")
@@ -161,7 +192,12 @@ func (h *rpmPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
newPath = "/" + project.ID + "/" + repo.Name + rest projectStorageID, repoStorageID, err = h.store.GetRepoStorageIDs(repo.ID)
if err != nil {
http.NotFound(w, r)
return
}
newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest
r.URL.Path = newPath r.URL.Path = newPath
h.next.ServeHTTP(w, r) h.next.ServeHTTP(w, r)
} }
@@ -176,6 +212,8 @@ func (h *rpmIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
var parts []string var parts []string
var rest string var rest string
var repo models.Repo var repo models.Repo
var projectStorageID int64
var repoStorageID int64
var err error var err error
var newPath string var newPath string
var repoID string var repoID string
@@ -191,6 +229,11 @@ func (h *rpmIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
projectStorageID, repoStorageID, err = h.store.GetRepoStorageIDs(repo.ID)
if err != nil {
http.NotFound(w, r)
return
}
if len(parts) == 3 { if len(parts) == 3 {
rest = "/" + parts[1] + "/" + parts[2] rest = "/" + parts[1] + "/" + parts[2]
} else if len(parts) == 2 { } else if len(parts) == 2 {
@@ -198,11 +241,15 @@ func (h *rpmIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
} else { } else {
rest = "" rest = ""
} }
newPath = "/" + repo.ProjectID + "/" + repo.Name + rest newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest
r.URL.Path = newPath r.URL.Path = newPath
h.next.ServeHTTP(w, r) h.next.ServeHTTP(w, r)
} }
func storageIDSegment(id int64) string {
return fmt.Sprintf("%016x", id)
}
func main() { func main() {
var configPath string var configPath string
var cfg config.Config var cfg config.Config
@@ -272,6 +319,10 @@ func main() {
rpmMirror = rpm.NewMirrorManager(store, logger, rpmMeta) rpmMirror = rpm.NewMirrorManager(store, logger, rpmMeta)
var uploadStore storage.FileStore var uploadStore storage.FileStore
uploadStore = storage.FileStore{BaseDir: filepath.Join(cfg.DataDir, "uploads")} uploadStore = storage.FileStore{BaseDir: filepath.Join(cfg.DataDir, "uploads")}
err = os.MkdirAll(repoManager.BaseDir, 0o755)
if err != nil {
log.Fatalf("git dir error: %v", err)
}
err = os.MkdirAll(rpmBase, 0o755) err = os.MkdirAll(rpmBase, 0o755)
if err != nil { if err != nil {
log.Fatalf("rpm dir error: %v", err) log.Fatalf("rpm dir error: %v", err)
@@ -288,6 +339,7 @@ func main() {
Repos: repoManager, Repos: repoManager,
RpmBase: rpmBase, RpmBase: rpmBase,
RpmMeta: rpmMeta, RpmMeta: rpmMeta,
RpmMirror: rpmMirror,
DockerBase: dockerBase, DockerBase: dockerBase,
Uploads: uploadStore, Uploads: uploadStore,
Logger: logger, Logger: logger,
@@ -448,10 +500,13 @@ func main() {
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("GET", "/api/repos/:id/rpm/subdir", api.RepoRPMGetSubdir) router.Handle("GET", "/api/repos/:id/rpm/subdir", api.RepoRPMGetSubdir)
router.Handle("POST", "/api/repos/:id/rpm/subdir/update", api.RepoRPMRenameSubdir)
router.Handle("POST", "/api/repos/:id/rpm/subdir/rename", api.RepoRPMRenameSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/rename", api.RepoRPMRenameSubdir)
router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", api.RepoRPMSyncSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", api.RepoRPMSyncSubdir)
router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", api.RepoRPMSuspendSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", api.RepoRPMSuspendSubdir)
router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", api.RepoRPMResumeSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", api.RepoRPMResumeSubdir)
router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", api.RepoRPMRebuildSubdirMetadata)
router.Handle("POST", "/api/repos/:id/rpm/subdir/cancel", api.RepoRPMCancelSubdirSync)
router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMMirrorRuns) router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMMirrorRuns)
router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMClearMirrorRuns) router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMClearMirrorRuns)
router.Handle("DELETE", "/api/repos/:id/rpm/subdir", api.RepoRPMDeleteSubdir) router.Handle("DELETE", "/api/repos/:id/rpm/subdir", api.RepoRPMDeleteSubdir)
@@ -510,7 +565,7 @@ func main() {
mux.Handle("/api/auth/oidc/login", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/auth/oidc/login", middleware.WithUser(store, middleware.AccessLog(logger, router)))
mux.Handle("/api/auth/oidc/callback", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/auth/oidc/callback", middleware.WithUser(store, middleware.AccessLog(logger, router)))
mux.Handle("/api/health", middleware.AccessLog(logger, 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(cfg.FrontendDir)))
extraListenerManager = newAdditionalListenerManager(store, mux, logger) extraListenerManager = newAdditionalListenerManager(store, mux, logger)
api.OnTLSListenersChanged = extraListenerManager.NotifyReload api.OnTLSListenersChanged = extraListenerManager.NotifyReload
api.OnTLSListenerRuntimeStatus = extraListenerManager.ListenerEndpointCounts api.OnTLSListenerRuntimeStatus = extraListenerManager.ListenerEndpointCounts
@@ -758,6 +813,7 @@ func (m *additionalListenerManager) startEndpoint(endpoint listenerEndpoint) run
Addr: endpoint.Addr, Addr: endpoint.Addr,
Handler: m.Handler, Handler: m.Handler,
ConnContext: connContextWithListenerPolicy(endpoint.Policy), ConnContext: connContextWithListenerPolicy(endpoint.Policy),
ErrorLog: log.New(&server_http_log_writer{l: m.Logger, id: endpoint.Name, depth: +2}, "", 0),
} }
if endpoint.IsHTTPS { if endpoint.IsHTTPS {
server.TLSConfig = endpoint.TLSConfig server.TLSConfig = endpoint.TLSConfig
@@ -768,9 +824,9 @@ func (m *additionalListenerManager) startEndpoint(endpoint listenerEndpoint) run
} }
if m.Logger != nil { if m.Logger != nil {
if endpoint.IsHTTPS { if endpoint.IsHTTPS {
m.Logger.Write("", util.LOG_INFO, "additional listener started name=%s https://%s", endpoint.Name, endpoint.Addr) m.Logger.Write(endpoint.Name, util.LOG_INFO, "additional listener started https://%s", endpoint.Addr)
} else { } else {
m.Logger.Write("", util.LOG_INFO, "additional listener started name=%s %s", endpoint.Name, endpoint.Addr) m.Logger.Write(endpoint.Name, util.LOG_INFO, "additional listener started %s", endpoint.Addr)
} }
} }
go func(ep listenerEndpoint, srv *http.Server) { go func(ep listenerEndpoint, srv *http.Server) {
@@ -781,7 +837,7 @@ func (m *additionalListenerManager) startEndpoint(endpoint listenerEndpoint) run
err = srv.ListenAndServe() err = srv.ListenAndServe()
} }
if err != nil && !errors.Is(err, http.ErrServerClosed) && m.Logger != nil { if err != nil && !errors.Is(err, http.ErrServerClosed) && m.Logger != nil {
m.Logger.Write("", util.LOG_ERROR, "additional listener failed name=%s addr=%s err=%v", ep.Name, ep.Addr, err) m.Logger.Write(ep.Name, util.LOG_ERROR, "additional listener failed addr=%s err=%v", ep.Addr, err)
} }
}(endpoint, server) }(endpoint, server)
return running return running
@@ -1288,12 +1344,26 @@ func serveListeners(endpoints []listenerEndpoint, handler http.Handler, logger *
go func(endpoint listenerEndpoint) { go func(endpoint listenerEndpoint) {
var err error var err error
var server *http.Server var server *http.Server
var l *log.Logger
defer wg.Done() defer wg.Done()
l = log.New(&server_http_log_writer{l: logger, id: "", depth: +2}, "", 0)
if endpoint.IsHTTPS { if endpoint.IsHTTPS {
server = &http.Server{Addr: endpoint.Addr, Handler: handler, TLSConfig: endpoint.TLSConfig, ConnContext: connContextWithListenerPolicy(endpoint.Policy)} server = &http.Server{
Addr: endpoint.Addr,
Handler: handler,
TLSConfig: endpoint.TLSConfig,
ErrorLog: l,
ConnContext: connContextWithListenerPolicy(endpoint.Policy),
}
err = server.ListenAndServeTLS("", "") err = server.ListenAndServeTLS("", "")
} else { } else {
server = &http.Server{Addr: endpoint.Addr, Handler: handler, ConnContext: connContextWithListenerPolicy(endpoint.Policy)} server = &http.Server{
Addr: endpoint.Addr,
Handler: handler,
ErrorLog: l,
ConnContext: connContextWithListenerPolicy(endpoint.Policy),
}
err = server.ListenAndServe() err = server.ListenAndServe()
} }
errs <- err errs <- err

View File

@@ -3,9 +3,11 @@ module codit
go 1.25.3 go 1.25.3
require ( require (
github.com/gdamore/tcell/v2 v2.13.8
github.com/go-git/go-git/v5 v5.12.0 github.com/go-git/go-git/v5 v5.12.0
github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-ldap/ldap/v3 v3.4.6
github.com/graphql-go/graphql v0.8.0 github.com/graphql-go/graphql v0.8.0
github.com/rivo/tview v0.42.0
github.com/sosedoff/gitkit v0.4.0 github.com/sosedoff/gitkit v0.4.0
golang.org/x/crypto v0.47.0 golang.org/x/crypto v0.47.0
modernc.org/sqlite v1.30.0 modernc.org/sqlite v1.30.0
@@ -22,6 +24,7 @@ require (
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect
@@ -32,19 +35,24 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sassoftware/go-rpmutils v0.4.1-0.20250318174028-2660c86d578c // indirect github.com/sassoftware/go-rpmutils v0.4.1-0.20250318174028-2660c86d578c // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.2.2 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/mod v0.16.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.19.0 // indirect golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.40.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.50.9 // indirect modernc.org/libc v1.50.9 // indirect

View File

@@ -28,6 +28,10 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=
github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
@@ -70,6 +74,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -84,6 +90,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sassoftware/go-rpmutils v0.4.1-0.20250318174028-2660c86d578c h1:Y+MtXJBE7rpqj0nk6GhSzD/48pXSKNEJPIYhtoSCbjk= github.com/sassoftware/go-rpmutils v0.4.1-0.20250318174028-2660c86d578c h1:Y+MtXJBE7rpqj0nk6GhSzD/48pXSKNEJPIYhtoSCbjk=
@@ -120,8 +130,8 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -133,8 +143,8 @@ golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -164,14 +174,15 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -3,6 +3,7 @@ package config
import "encoding/json" import "encoding/json"
import "errors" import "errors"
import "os" import "os"
import "path/filepath"
import "strings" import "strings"
import "time" import "time"
import "strconv" import "strconv"
@@ -12,6 +13,7 @@ type Config struct {
HTTPSAddrs []string `json:"https_addrs"` HTTPSAddrs []string `json:"https_addrs"`
PublicBaseURL string `json:"public_base_url"` PublicBaseURL string `json:"public_base_url"`
DataDir string `json:"data_dir"` DataDir string `json:"data_dir"`
FrontendDir string `json:"frontend_dir"`
DBDriver string `json:"db_driver"` DBDriver string `json:"db_driver"`
DBDSN string `json:"db_dsn"` DBDSN string `json:"db_dsn"`
SessionTTL Duration `json:"session_ttl"` SessionTTL Duration `json:"session_ttl"`
@@ -51,6 +53,7 @@ func Load(path string) (Config, error) {
HTTPAddrs: []string{":1080"}, HTTPAddrs: []string{":1080"},
HTTPSAddrs: []string{}, HTTPSAddrs: []string{},
DataDir: "./codit-data", DataDir: "./codit-data",
FrontendDir: filepath.Join("..", "frontend", "dist"),
DBDriver: "sqlite", DBDriver: "sqlite",
DBDSN: "file:./codit-data/codit.db?_pragma=foreign_keys(1)", DBDSN: "file:./codit-data/codit.db?_pragma=foreign_keys(1)",
SessionTTL: Duration(24 * time.Hour), SessionTTL: Duration(24 * time.Hour),
@@ -106,6 +109,10 @@ func override(cfg *Config) {
if v != "" { if v != "" {
cfg.DataDir = v cfg.DataDir = v
} }
v = os.Getenv("CODIT_FRONTEND_DIR")
if v != "" {
cfg.FrontendDir = v
}
v = os.Getenv("CODIT_DB_DRIVER") v = os.Getenv("CODIT_DB_DRIVER")
if v != "" { if v != "" {
cfg.DBDriver = v cfg.DBDriver = v

View File

@@ -18,6 +18,9 @@ func TestLoadDefaults(t *testing.T) {
if cfg.GitHTTPPrefix != "/git" { if cfg.GitHTTPPrefix != "/git" {
t.Fatalf("unexpected git prefix default: %s", cfg.GitHTTPPrefix) t.Fatalf("unexpected git prefix default: %s", cfg.GitHTTPPrefix)
} }
if cfg.FrontendDir == "" {
t.Fatalf("frontend_dir default missing")
}
} }
func TestLoadFromJSONAndEnvOverride(t *testing.T) { func TestLoadFromJSONAndEnvOverride(t *testing.T) {
@@ -34,6 +37,7 @@ func TestLoadFromJSONAndEnvOverride(t *testing.T) {
t.Fatalf("write config file: %v", err) t.Fatalf("write config file: %v", err)
} }
t.Setenv("CODIT_DB_DSN", "file:override.db") t.Setenv("CODIT_DB_DSN", "file:override.db")
t.Setenv("CODIT_FRONTEND_DIR", "/srv/codit/frontend")
cfg, err = Load(path) cfg, err = Load(path)
if err != nil { if err != nil {
t.Fatalf("Load() error: %v", err) t.Fatalf("Load() error: %v", err)
@@ -44,6 +48,9 @@ func TestLoadFromJSONAndEnvOverride(t *testing.T) {
if cfg.AuthMode != "hybrid" { if cfg.AuthMode != "hybrid" {
t.Fatalf("auth_mode normalization failed: %s", cfg.AuthMode) t.Fatalf("auth_mode normalization failed: %s", cfg.AuthMode)
} }
if cfg.FrontendDir != "/srv/codit/frontend" {
t.Fatalf("frontend_dir env override failed: %s", cfg.FrontendDir)
}
} }
func TestDurationUnmarshalJSON(t *testing.T) { func TestDurationUnmarshalJSON(t *testing.T) {

View File

@@ -12,7 +12,10 @@ func (s *Store) ListPKICAs() ([]models.PKICA, error) {
var err error var err error
var items []models.PKICA var items []models.PKICA
var item models.PKICA var item models.PKICA
rows, err = s.DB.Query(`SELECT id, name, COALESCE(parent_ca_id, ''), is_root, cert_pem, key_pem, serial_counter, status, created_at, updated_at FROM pki_cas ORDER BY name`) rows, err = s.DB.Query(`SELECT c.public_id, c.name, COALESCE(p.public_id, ''), c.is_root, c.cert_pem, c.key_pem, c.serial_counter, c.status, c.created_at, c.updated_at
FROM pki_cas c
LEFT JOIN pki_cas p ON p.id = c.parent_ca_id
ORDER BY c.name`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,7 +38,10 @@ func (s *Store) GetPKICA(id string) (models.PKICA, error) {
var row *sql.Row var row *sql.Row
var item models.PKICA var item models.PKICA
var err error var err error
row = s.DB.QueryRow(`SELECT id, name, COALESCE(parent_ca_id, ''), is_root, cert_pem, key_pem, serial_counter, status, created_at, updated_at FROM pki_cas WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT c.public_id, c.name, COALESCE(p.public_id, ''), c.is_root, c.cert_pem, c.key_pem, c.serial_counter, c.status, c.created_at, c.updated_at
FROM pki_cas c
LEFT JOIN pki_cas p ON p.id = c.parent_ca_id
WHERE c.public_id = ?`, id)
err = row.Scan(&item.ID, &item.Name, &item.ParentCAID, &item.IsRoot, &item.CertPEM, &item.KeyPEM, &item.SerialCounter, &item.Status, &item.CreatedAt, &item.UpdatedAt) err = row.Scan(&item.ID, &item.Name, &item.ParentCAID, &item.IsRoot, &item.CertPEM, &item.KeyPEM, &item.SerialCounter, &item.Status, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
@@ -45,14 +51,13 @@ func (s *Store) GetPKICA(id string) (models.PKICA, error) {
func (s *Store) UpdatePKICAName(id string, name string) error { func (s *Store) UpdatePKICAName(id string, name string) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE pki_cas SET name = ?, updated_at = ? WHERE id = ?`, name, time.Now().UTC().Unix(), id) _, err = s.DB.Exec(`UPDATE pki_cas SET name = ?, updated_at = ? WHERE public_id = ?`, name, time.Now().UTC().Unix(), id)
return err return err
} }
func (s *Store) CreatePKICA(item models.PKICA) (models.PKICA, error) { func (s *Store) CreatePKICA(item models.PKICA) (models.PKICA, error) {
var id string var id string
var now int64 var now int64
var parentValue any
var err error var err error
if item.ID == "" { if item.ID == "" {
id, err = util.NewID() id, err = util.NewID()
@@ -70,12 +75,9 @@ func (s *Store) CreatePKICA(item models.PKICA) (models.PKICA, error) {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.CreatedAt = now item.CreatedAt = now
item.UpdatedAt = now item.UpdatedAt = now
parentValue = nil _, err = s.DB.Exec(`INSERT INTO pki_cas (public_id, name, parent_ca_id, is_root, cert_pem, key_pem, serial_counter, status, created_at, updated_at)
if item.ParentCAID != "" { VALUES (?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_cas WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?)`,
parentValue = item.ParentCAID item.ID, item.Name, item.ParentCAID, item.ParentCAID, item.IsRoot, item.CertPEM, item.KeyPEM, item.SerialCounter, item.Status, item.CreatedAt, item.UpdatedAt)
}
_, err = s.DB.Exec(`INSERT INTO pki_cas (id, name, parent_ca_id, is_root, cert_pem, key_pem, serial_counter, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.ID, item.Name, parentValue, item.IsRoot, item.CertPEM, item.KeyPEM, item.SerialCounter, item.Status, item.CreatedAt, item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
} }
@@ -86,7 +88,10 @@ func (s *Store) CountPKICAChildren(id string) (int, error) {
var row *sql.Row var row *sql.Row
var count int var count int
var err error var err error
row = s.DB.QueryRow(`SELECT COUNT(*) FROM pki_cas WHERE parent_ca_id = ?`, id) row = s.DB.QueryRow(`SELECT COUNT(*)
FROM pki_cas c
JOIN pki_cas p ON p.id = c.parent_ca_id
WHERE p.public_id = ?`, id)
err = row.Scan(&count) err = row.Scan(&count)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -98,7 +103,10 @@ func (s *Store) CountPKICertsByCA(id string) (int, error) {
var row *sql.Row var row *sql.Row
var count int var count int
var err error var err error
row = s.DB.QueryRow(`SELECT COUNT(*) FROM pki_certs WHERE ca_id = ?`, id) row = s.DB.QueryRow(`SELECT COUNT(*)
FROM pki_certs c
JOIN pki_cas ca ON ca.id = c.ca_id
WHERE ca.public_id = ?`, id)
err = row.Scan(&count) err = row.Scan(&count)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -108,7 +116,7 @@ func (s *Store) CountPKICertsByCA(id string) (int, error) {
func (s *Store) DeletePKICA(id string) error { func (s *Store) DeletePKICA(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM pki_cas WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM pki_cas WHERE public_id = ?`, id)
return err return err
} }
@@ -117,7 +125,7 @@ func (s *Store) DeletePKICASubtree(id string) error {
var rows *sql.Rows var rows *sql.Rows
var err error var err error
var itemID string var itemID string
var parentID sql.NullString var parentID string
var parentByID map[string]string var parentByID map[string]string
var pending []string var pending []string
var current string var current string
@@ -131,7 +139,9 @@ func (s *Store) DeletePKICASubtree(id string) error {
if err != nil { if err != nil {
return err return err
} }
rows, err = tx.Query(`SELECT id, parent_ca_id FROM pki_cas`) rows, err = tx.Query(`SELECT c.public_id, COALESCE(p.public_id, '')
FROM pki_cas c
LEFT JOIN pki_cas p ON p.id = c.parent_ca_id`)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return err return err
@@ -143,11 +153,7 @@ func (s *Store) DeletePKICASubtree(id string) error {
_ = tx.Rollback() _ = tx.Rollback()
return err return err
} }
if parentID.Valid { parentByID[itemID] = parentID
parentByID[itemID] = parentID.String
} else {
parentByID[itemID] = ""
}
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {
@@ -177,7 +183,7 @@ func (s *Store) DeletePKICASubtree(id string) error {
for i = len(toDelete) - 1; i >= 0; i-- { for i = len(toDelete) - 1; i >= 0; i-- {
j = i j = i
_ = j _ = j
_, err = tx.Exec(`DELETE FROM pki_cas WHERE id = ?`, toDelete[i]) _, err = tx.Exec(`DELETE FROM pki_cas WHERE public_id = ?`, toDelete[i])
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return err return err
@@ -199,13 +205,13 @@ func (s *Store) NextPKICASerial(caID string) (int64, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
row = tx.QueryRow(`SELECT serial_counter FROM pki_cas WHERE id = ?`, caID) row = tx.QueryRow(`SELECT serial_counter FROM pki_cas WHERE public_id = ?`, caID)
err = row.Scan(&serial) err = row.Scan(&serial)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return 0, err return 0, err
} }
_, err = tx.Exec(`UPDATE pki_cas SET serial_counter = ?, updated_at = ? WHERE id = ?`, serial+1, time.Now().UTC().Unix(), caID) _, err = tx.Exec(`UPDATE pki_cas SET serial_counter = ?, updated_at = ? WHERE public_id = ?`, serial+1, time.Now().UTC().Unix(), caID)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return 0, err return 0, err
@@ -223,11 +229,22 @@ func (s *Store) ListPKICerts(caID string) ([]models.PKICert, error) {
var items []models.PKICert var items []models.PKICert
var item models.PKICert var item models.PKICert
if caID == "" { if caID == "" {
rows, err = s.DB.Query(`SELECT id, COALESCE(ca_id, ''), serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at FROM pki_certs ORDER BY created_at DESC`) rows, err = s.DB.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
FROM pki_certs c
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
ORDER BY c.created_at DESC`)
} else if caID == "standalone" { } else if caID == "standalone" {
rows, err = s.DB.Query(`SELECT id, COALESCE(ca_id, ''), serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at FROM pki_certs WHERE ca_id IS NULL OR ca_id = '' ORDER BY created_at DESC`) rows, err = s.DB.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
FROM pki_certs c
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
WHERE c.ca_id IS NULL
ORDER BY c.created_at DESC`)
} else { } else {
rows, err = s.DB.Query(`SELECT id, COALESCE(ca_id, ''), serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at FROM pki_certs WHERE ca_id = ? ORDER BY created_at DESC`, caID) rows, err = s.DB.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
FROM pki_certs c
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
WHERE c.ca_id = (SELECT id FROM pki_cas WHERE public_id = ?)
ORDER BY c.created_at DESC`, caID)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -251,7 +268,10 @@ func (s *Store) GetPKICert(id string) (models.PKICert, error) {
var row *sql.Row var row *sql.Row
var item models.PKICert var item models.PKICert
var err error var err error
row = s.DB.QueryRow(`SELECT id, COALESCE(ca_id, ''), serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at FROM pki_certs WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT c.public_id, COALESCE(ca.public_id, ''), c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
FROM pki_certs c
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
WHERE c.public_id = ?`, id)
err = row.Scan(&item.ID, &item.CAID, &item.SerialHex, &item.CommonName, &item.SANDNS, &item.SANIPs, &item.IsCA, &item.CertPEM, &item.KeyPEM, &item.NotBefore, &item.NotAfter, &item.Status, &item.RevokedAt, &item.RevocationReason, &item.CreatedAt) err = row.Scan(&item.ID, &item.CAID, &item.SerialHex, &item.CommonName, &item.SANDNS, &item.SANIPs, &item.IsCA, &item.CertPEM, &item.KeyPEM, &item.NotBefore, &item.NotAfter, &item.Status, &item.RevokedAt, &item.RevocationReason, &item.CreatedAt)
if err != nil { if err != nil {
return item, err return item, err
@@ -262,7 +282,6 @@ func (s *Store) GetPKICert(id string) (models.PKICert, error) {
func (s *Store) CreatePKICert(item models.PKICert) (models.PKICert, error) { func (s *Store) CreatePKICert(item models.PKICert) (models.PKICert, error) {
var id string var id string
var now int64 var now int64
var caIDValue any
var err error var err error
if item.ID == "" { if item.ID == "" {
id, err = util.NewID() id, err = util.NewID()
@@ -276,12 +295,10 @@ func (s *Store) CreatePKICert(item models.PKICert) (models.PKICert, error) {
} }
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.CreatedAt = now item.CreatedAt = now
caIDValue = nil item.CAID = strings.TrimSpace(item.CAID)
if strings.TrimSpace(item.CAID) != "" { _, err = s.DB.Exec(`INSERT INTO pki_certs (public_id, ca_id, serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at)
caIDValue = strings.TrimSpace(item.CAID) VALUES (?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_cas WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
} item.ID, item.CAID, item.CAID, item.SerialHex, item.CommonName, item.SANDNS, item.SANIPs, item.IsCA, item.CertPEM, item.KeyPEM, item.NotBefore, item.NotAfter, item.Status, item.RevokedAt, item.RevocationReason, item.CreatedAt)
_, err = s.DB.Exec(`INSERT INTO pki_certs (id, ca_id, serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.ID, caIDValue, item.SerialHex, item.CommonName, item.SANDNS, item.SANIPs, item.IsCA, item.CertPEM, item.KeyPEM, item.NotBefore, item.NotAfter, item.Status, item.RevokedAt, item.RevocationReason, item.CreatedAt)
if err != nil { if err != nil {
return item, err return item, err
} }
@@ -290,13 +307,13 @@ func (s *Store) CreatePKICert(item models.PKICert) (models.PKICert, error) {
func (s *Store) RevokePKICert(id string, reason string) error { func (s *Store) RevokePKICert(id string, reason string) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE pki_certs SET status = 'revoked', revoked_at = ?, revocation_reason = ? WHERE id = ?`, time.Now().UTC().Unix(), reason, id) _, err = s.DB.Exec(`UPDATE pki_certs SET status = 'revoked', revoked_at = ?, revocation_reason = ? WHERE public_id = ?`, time.Now().UTC().Unix(), reason, id)
return err return err
} }
func (s *Store) DeletePKICert(id string) error { func (s *Store) DeletePKICert(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM pki_certs WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM pki_certs WHERE public_id = ?`, id)
return err return err
} }

View File

@@ -12,7 +12,7 @@ func (s *Store) ListServicePrincipals() ([]models.ServicePrincipal, error) {
var items []models.ServicePrincipal var items []models.ServicePrincipal
var item models.ServicePrincipal var item models.ServicePrincipal
var err error var err error
rows, err = s.DB.Query(`SELECT id, name, description, is_admin, disabled, created_at, updated_at FROM service_principals ORDER BY name`) rows, err = s.DB.Query(`SELECT public_id, name, description, is_admin, disabled, created_at, updated_at FROM service_principals ORDER BY name`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,7 +35,7 @@ func (s *Store) GetServicePrincipal(id string) (models.ServicePrincipal, error)
var row *sql.Row var row *sql.Row
var item models.ServicePrincipal var item models.ServicePrincipal
var err error var err error
row = s.DB.QueryRow(`SELECT id, name, description, is_admin, disabled, created_at, updated_at FROM service_principals WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT public_id, name, description, is_admin, disabled, created_at, updated_at FROM service_principals WHERE public_id = ?`, id)
err = row.Scan(&item.ID, &item.Name, &item.Description, &item.IsAdmin, &item.Disabled, &item.CreatedAt, &item.UpdatedAt) err = row.Scan(&item.ID, &item.Name, &item.Description, &item.IsAdmin, &item.Disabled, &item.CreatedAt, &item.UpdatedAt)
return item, err return item, err
} }
@@ -54,7 +54,7 @@ func (s *Store) CreateServicePrincipal(item models.ServicePrincipal) (models.Ser
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.CreatedAt = now item.CreatedAt = now
item.UpdatedAt = now item.UpdatedAt = now
_, err = s.DB.Exec(`INSERT INTO service_principals (id, name, description, is_admin, disabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO service_principals (public_id, name, description, is_admin, disabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
item.ID, item.Name, item.Description, item.IsAdmin, item.Disabled, item.CreatedAt, item.UpdatedAt) item.ID, item.Name, item.Description, item.IsAdmin, item.Disabled, item.CreatedAt, item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
@@ -67,14 +67,14 @@ func (s *Store) UpdateServicePrincipal(item models.ServicePrincipal) error {
var err error var err error
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.UpdatedAt = now item.UpdatedAt = now
_, err = s.DB.Exec(`UPDATE service_principals SET name = ?, description = ?, is_admin = ?, disabled = ?, updated_at = ? WHERE id = ?`, _, err = s.DB.Exec(`UPDATE service_principals SET name = ?, description = ?, is_admin = ?, disabled = ?, updated_at = ? WHERE public_id = ?`,
item.Name, item.Description, item.IsAdmin, item.Disabled, item.UpdatedAt, item.ID) item.Name, item.Description, item.IsAdmin, item.Disabled, item.UpdatedAt, item.ID)
return err return err
} }
func (s *Store) DeleteServicePrincipal(id string) error { func (s *Store) DeleteServicePrincipal(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM service_principals WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM service_principals WHERE public_id = ?`, id)
return err return err
} }
@@ -83,7 +83,10 @@ func (s *Store) ListCertPrincipalBindings() ([]models.CertPrincipalBinding, erro
var items []models.CertPrincipalBinding var items []models.CertPrincipalBinding
var item models.CertPrincipalBinding var item models.CertPrincipalBinding
var err error var err error
rows, err = s.DB.Query(`SELECT fingerprint, principal_id, enabled, created_at, updated_at FROM cert_principal_bindings ORDER BY fingerprint`) rows, err = s.DB.Query(`SELECT b.fingerprint, p.public_id, b.enabled, b.created_at, b.updated_at
FROM cert_principal_bindings b
JOIN service_principals p ON p.id = b.principal_id
ORDER BY b.fingerprint`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -109,7 +112,7 @@ func (s *Store) UpsertCertPrincipalBinding(item models.CertPrincipalBinding) (mo
item.Fingerprint = strings.ToLower(strings.TrimSpace(item.Fingerprint)) item.Fingerprint = strings.ToLower(strings.TrimSpace(item.Fingerprint))
item.UpdatedAt = now item.UpdatedAt = now
_, err = s.DB.Exec(`INSERT INTO cert_principal_bindings (fingerprint, principal_id, enabled, created_at, updated_at) _, err = s.DB.Exec(`INSERT INTO cert_principal_bindings (fingerprint, principal_id, enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?) VALUES (?, (SELECT id FROM service_principals WHERE public_id = ?), ?, ?, ?)
ON CONFLICT(fingerprint) DO UPDATE SET principal_id = excluded.principal_id, enabled = excluded.enabled, updated_at = excluded.updated_at`, ON CONFLICT(fingerprint) DO UPDATE SET principal_id = excluded.principal_id, enabled = excluded.enabled, updated_at = excluded.updated_at`,
item.Fingerprint, item.PrincipalID, item.Enabled, now, now) item.Fingerprint, item.PrincipalID, item.Enabled, now, now)
if err != nil { if err != nil {
@@ -132,7 +135,7 @@ func (s *Store) GetPrincipalByCertFingerprint(fingerprint string) (models.Servic
var enabled bool var enabled bool
var err error var err error
fingerprint = strings.ToLower(strings.TrimSpace(fingerprint)) fingerprint = strings.ToLower(strings.TrimSpace(fingerprint))
row = s.DB.QueryRow(`SELECT p.id, p.name, p.description, p.is_admin, p.disabled, p.created_at, p.updated_at, b.enabled row = s.DB.QueryRow(`SELECT p.public_id, p.name, p.description, p.is_admin, p.disabled, p.created_at, p.updated_at, b.enabled
FROM cert_principal_bindings b FROM cert_principal_bindings b
INNER JOIN service_principals p ON p.id = b.principal_id INNER JOIN service_principals p ON p.id = b.principal_id
WHERE b.fingerprint = ?`, fingerprint) WHERE b.fingerprint = ?`, fingerprint)
@@ -154,7 +157,12 @@ func (s *Store) ListPrincipalProjectRoles(principalID string) ([]models.Principa
var items []models.PrincipalProjectRole var items []models.PrincipalProjectRole
var item models.PrincipalProjectRole var item models.PrincipalProjectRole
var err error var err error
rows, err = s.DB.Query(`SELECT principal_id, project_id, role, created_at FROM principal_project_roles WHERE principal_id = ? ORDER BY project_id`, principalID) rows, err = s.DB.Query(`SELECT sp.public_id, p.public_id, r.role, r.created_at
FROM principal_project_roles r
JOIN service_principals sp ON sp.id = r.principal_id
JOIN projects p ON p.id = r.project_id
WHERE r.principal_id = (SELECT id FROM service_principals WHERE public_id = ?)
ORDER BY p.public_id`, principalID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -178,7 +186,8 @@ func (s *Store) UpsertPrincipalProjectRole(item models.PrincipalProjectRole) (mo
var err error var err error
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.CreatedAt = now item.CreatedAt = now
_, err = s.DB.Exec(`INSERT INTO principal_project_roles (principal_id, project_id, role, created_at) VALUES (?, ?, ?, ?) _, err = s.DB.Exec(`INSERT INTO principal_project_roles (principal_id, project_id, role, created_at)
VALUES ((SELECT id FROM service_principals WHERE public_id = ?), (SELECT id FROM projects WHERE public_id = ?), ?, ?)
ON CONFLICT(principal_id, project_id) DO UPDATE SET role = excluded.role`, ON CONFLICT(principal_id, project_id) DO UPDATE SET role = excluded.role`,
item.PrincipalID, item.ProjectID, item.Role, item.CreatedAt) item.PrincipalID, item.ProjectID, item.Role, item.CreatedAt)
if err != nil { if err != nil {
@@ -189,7 +198,9 @@ func (s *Store) UpsertPrincipalProjectRole(item models.PrincipalProjectRole) (mo
func (s *Store) DeletePrincipalProjectRole(principalID string, projectID string) error { func (s *Store) DeletePrincipalProjectRole(principalID string, projectID string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM principal_project_roles WHERE principal_id = ? AND project_id = ?`, principalID, projectID) _, err = s.DB.Exec(`DELETE FROM principal_project_roles
WHERE principal_id = (SELECT id FROM service_principals WHERE public_id = ?)
AND project_id = (SELECT id FROM projects WHERE public_id = ?)`, principalID, projectID)
return err return err
} }
@@ -197,7 +208,9 @@ func (s *Store) GetPrincipalProjectRole(principalID string, projectID string) (s
var row *sql.Row var row *sql.Row
var role string var role string
var err error var err error
row = s.DB.QueryRow(`SELECT role FROM principal_project_roles WHERE principal_id = ? AND project_id = ?`, principalID, projectID) row = s.DB.QueryRow(`SELECT role FROM principal_project_roles
WHERE principal_id = (SELECT id FROM service_principals WHERE public_id = ?)
AND project_id = (SELECT id FROM projects WHERE public_id = ?)`, principalID, projectID)
err = row.Scan(&role) err = row.Scan(&role)
return role, err return role, err
} }

View File

@@ -12,13 +12,17 @@ func (s *Store) ListRPMRepoDirs(repoID string) ([]models.RPMRepoDir, error) {
var items []models.RPMRepoDir var items []models.RPMRepoDir
var item models.RPMRepoDir var item models.RPMRepoDir
var err error var err error
rows, err = s.DB.Query(`SELECT repo_id, path, mode, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec, sync_enabled, dirty, next_sync_at, sync_running, sync_status, sync_error, sync_step, sync_total, sync_done, sync_failed, sync_deleted, last_sync_started_at, last_sync_finished_at, last_sync_success_at, last_synced_revision, created_at, updated_at FROM rpm_repo_dirs WHERE repo_id = ? ORDER BY LENGTH(path), path`, repoID) rows, err = s.DB.Query(`SELECT r.public_id, d.path, d.mode, d.allow_delete, d.remote_url, d.connect_host, d.host_header, d.tls_server_name, d.tls_insecure_skip_verify, d.sync_interval_sec, d.sync_enabled, d.dirty, d.next_sync_at, d.sync_running, d.sync_status, d.sync_error, d.sync_step, d.sync_total, d.sync_done, d.sync_failed, d.sync_deleted, d.last_sync_started_at, d.last_sync_finished_at, d.last_sync_success_at, d.last_synced_revision, d.created_at, d.updated_at
FROM rpm_repo_dirs d
JOIN repos r ON r.id = d.repo_id
WHERE r.public_id = ?
ORDER BY LENGTH(d.path), d.path`, repoID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err = rows.Scan(&item.RepoID, &item.Path, &item.Mode, &item.RemoteURL, &item.ConnectHost, &item.HostHeader, &item.TLSServerName, &item.TLSInsecureSkipVerify, &item.SyncIntervalSec, &item.SyncEnabled, &item.Dirty, &item.NextSyncAt, &item.SyncRunning, &item.SyncStatus, &item.SyncError, &item.SyncStep, &item.SyncTotal, &item.SyncDone, &item.SyncFailed, &item.SyncDeleted, &item.LastSyncStartedAt, &item.LastSyncFinishedAt, &item.LastSyncSuccessAt, &item.LastSyncedRevision, &item.CreatedAt, &item.UpdatedAt) err = rows.Scan(&item.RepoID, &item.Path, &item.Mode, &item.AllowDelete, &item.RemoteURL, &item.ConnectHost, &item.HostHeader, &item.TLSServerName, &item.TLSInsecureSkipVerify, &item.SyncIntervalSec, &item.SyncEnabled, &item.Dirty, &item.NextSyncAt, &item.SyncRunning, &item.SyncStatus, &item.SyncError, &item.SyncStep, &item.SyncTotal, &item.SyncDone, &item.SyncFailed, &item.SyncDeleted, &item.LastSyncStartedAt, &item.LastSyncFinishedAt, &item.LastSyncSuccessAt, &item.LastSyncedRevision, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -39,10 +43,11 @@ func (s *Store) UpsertRPMRepoDir(item models.RPMRepoDir) error {
item.SyncIntervalSec = 300 item.SyncIntervalSec = 300
} }
_, err = s.DB.Exec(` _, err = s.DB.Exec(`
INSERT INTO rpm_repo_dirs (repo_id, path, mode, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec, sync_enabled, dirty, next_sync_at, created_at, updated_at) INSERT INTO rpm_repo_dirs (repo_id, path, mode, allow_delete, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec, sync_enabled, dirty, next_sync_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES ((SELECT id FROM repos WHERE public_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(repo_id, path) DO UPDATE SET ON CONFLICT(repo_id, path) DO UPDATE SET
mode = excluded.mode, mode = excluded.mode,
allow_delete = excluded.allow_delete,
remote_url = excluded.remote_url, remote_url = excluded.remote_url,
connect_host = excluded.connect_host, connect_host = excluded.connect_host,
host_header = excluded.host_header, host_header = excluded.host_header,
@@ -57,6 +62,7 @@ func (s *Store) UpsertRPMRepoDir(item models.RPMRepoDir) error {
item.RepoID, item.RepoID,
item.Path, item.Path,
item.Mode, item.Mode,
item.AllowDelete,
item.RemoteURL, item.RemoteURL,
item.ConnectHost, item.ConnectHost,
item.HostHeader, item.HostHeader,
@@ -75,8 +81,11 @@ func (s *Store) GetRPMRepoDir(repoID string, path string) (models.RPMRepoDir, er
var row *sql.Row var row *sql.Row
var item models.RPMRepoDir var item models.RPMRepoDir
var err error var err error
row = s.DB.QueryRow(`SELECT repo_id, path, mode, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec, sync_enabled, dirty, next_sync_at, sync_running, sync_status, sync_error, sync_step, sync_total, sync_done, sync_failed, sync_deleted, last_sync_started_at, last_sync_finished_at, last_sync_success_at, last_synced_revision, created_at, updated_at FROM rpm_repo_dirs WHERE repo_id = ? AND path = ?`, repoID, path) row = s.DB.QueryRow(`SELECT r.public_id, d.path, d.mode, d.allow_delete, d.remote_url, d.connect_host, d.host_header, d.tls_server_name, d.tls_insecure_skip_verify, d.sync_interval_sec, d.sync_enabled, d.dirty, d.next_sync_at, d.sync_running, d.sync_status, d.sync_error, d.sync_step, d.sync_total, d.sync_done, d.sync_failed, d.sync_deleted, d.last_sync_started_at, d.last_sync_finished_at, d.last_sync_success_at, d.last_synced_revision, d.created_at, d.updated_at
err = row.Scan(&item.RepoID, &item.Path, &item.Mode, &item.RemoteURL, &item.ConnectHost, &item.HostHeader, &item.TLSServerName, &item.TLSInsecureSkipVerify, &item.SyncIntervalSec, &item.SyncEnabled, &item.Dirty, &item.NextSyncAt, &item.SyncRunning, &item.SyncStatus, &item.SyncError, &item.SyncStep, &item.SyncTotal, &item.SyncDone, &item.SyncFailed, &item.SyncDeleted, &item.LastSyncStartedAt, &item.LastSyncFinishedAt, &item.LastSyncSuccessAt, &item.LastSyncedRevision, &item.CreatedAt, &item.UpdatedAt) FROM rpm_repo_dirs d
JOIN repos r ON r.id = d.repo_id
WHERE r.public_id = ? AND d.path = ?`, repoID, path)
err = row.Scan(&item.RepoID, &item.Path, &item.Mode, &item.AllowDelete, &item.RemoteURL, &item.ConnectHost, &item.HostHeader, &item.TLSServerName, &item.TLSInsecureSkipVerify, &item.SyncIntervalSec, &item.SyncEnabled, &item.Dirty, &item.NextSyncAt, &item.SyncRunning, &item.SyncStatus, &item.SyncError, &item.SyncStep, &item.SyncTotal, &item.SyncDone, &item.SyncFailed, &item.SyncDeleted, &item.LastSyncStartedAt, &item.LastSyncFinishedAt, &item.LastSyncSuccessAt, &item.LastSyncedRevision, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
} }
@@ -92,7 +101,7 @@ func (s *Store) ListDueRPMMirrorTasks(now int64, limit int) ([]models.RPMMirrorT
limit = 10 limit = 10
} }
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT d.repo_id, r.path, d.path, d.remote_url, d.connect_host, d.host_header, d.tls_server_name, d.tls_insecure_skip_verify, d.sync_interval_sec, d.dirty, d.last_synced_revision SELECT r.public_id, r.path, d.path, d.remote_url, d.connect_host, d.host_header, d.tls_server_name, d.tls_insecure_skip_verify, d.sync_interval_sec, d.dirty, d.last_synced_revision
FROM rpm_repo_dirs d FROM rpm_repo_dirs d
JOIN repos r ON r.id = d.repo_id JOIN repos r ON r.id = d.repo_id
WHERE d.mode = 'mirror' AND d.sync_enabled = 1 AND d.sync_running = 0 AND (d.dirty = 1 OR d.next_sync_at <= ? OR d.next_sync_at = 0) WHERE d.mode = 'mirror' AND d.sync_enabled = 1 AND d.sync_running = 0 AND (d.dirty = 1 OR d.next_sync_at <= ? OR d.next_sync_at = 0)
@@ -120,7 +129,7 @@ func (s *Store) TryStartRPMMirrorTask(repoID string, path string, now int64) (bo
var res sql.Result var res sql.Result
var rows int64 var rows int64
var err error var err error
res, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_running = 1, sync_status = 'running', sync_error = '', sync_step = 'start', sync_total = 0, sync_done = 0, sync_failed = 0, sync_deleted = 0, last_sync_started_at = ?, updated_at = ? WHERE repo_id = ? AND path = ? AND mode = 'mirror' AND sync_running = 0`, now, now, repoID, path) res, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_running = 1, sync_status = 'running', sync_error = '', sync_step = 'start', sync_total = 0, sync_done = 0, sync_failed = 0, sync_deleted = 0, last_sync_started_at = ?, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ? AND mode = 'mirror' AND sync_enabled = 1 AND sync_running = 0`, now, now, repoID, path)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -135,7 +144,7 @@ func (s *Store) UpdateRPMMirrorTaskProgress(repoID string, path string, step str
var now int64 var now int64
var err error var err error
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_step = ?, sync_total = ?, sync_done = ?, sync_failed = ?, sync_deleted = ?, updated_at = ? WHERE repo_id = ? AND path = ?`, step, total, done, failed, deleted, now, repoID, path) _, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_step = ?, sync_total = ?, sync_done = ?, sync_failed = ?, sync_deleted = ?, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, step, total, done, failed, deleted, now, repoID, path)
return err return err
} }
@@ -146,7 +155,7 @@ func (s *Store) FinishRPMMirrorTask(repoID string, path string, success bool, re
var interval int64 var interval int64
var row *sql.Row var row *sql.Row
var err error var err error
row = s.DB.QueryRow(`SELECT sync_interval_sec FROM rpm_repo_dirs WHERE repo_id = ? AND path = ?`, repoID, path) row = s.DB.QueryRow(`SELECT sync_interval_sec FROM rpm_repo_dirs WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, repoID, path)
err = row.Scan(&interval) err = row.Scan(&interval)
if err != nil { if err != nil {
return err return err
@@ -158,14 +167,14 @@ func (s *Store) FinishRPMMirrorTask(repoID string, path string, success bool, re
nextSync = now + interval nextSync = now + interval
if success { if success {
status = "success" status = "success"
_, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_running = 0, dirty = 0, next_sync_at = ?, sync_status = ?, sync_error = '', sync_step = 'idle', last_sync_finished_at = ?, last_sync_success_at = ?, last_synced_revision = ?, updated_at = ? WHERE repo_id = ? AND path = ?`, nextSync, status, now, now, revision, now, repoID, path) _, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_running = 0, dirty = 0, next_sync_at = ?, sync_status = ?, sync_error = '', sync_step = 'idle', last_sync_finished_at = ?, last_sync_success_at = ?, last_synced_revision = ?, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, nextSync, status, now, now, revision, now, repoID, path)
return err return err
} }
status = "failed" status = "failed"
if errMsg == "" { if errMsg == "" {
errMsg = "mirror sync failed" errMsg = "mirror sync failed"
} }
_, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_running = 0, dirty = 1, next_sync_at = ?, sync_status = ?, sync_error = ?, sync_step = 'idle', last_sync_finished_at = ?, updated_at = ? WHERE repo_id = ? AND path = ?`, now+30, status, errMsg, now, now, repoID, path) _, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_running = 0, dirty = 1, next_sync_at = ?, sync_status = ?, sync_error = ?, sync_step = 'idle', last_sync_finished_at = ?, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, now+30, status, errMsg, now, now, repoID, path)
return err return err
} }
@@ -173,7 +182,7 @@ func (s *Store) MarkRPMMirrorTaskDirty(repoID string, path string) error {
var now int64 var now int64
var err error var err error
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET dirty = 1, next_sync_at = ?, updated_at = ? WHERE repo_id = ? AND path = ?`, now, now, repoID, path) _, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET dirty = 1, next_sync_at = ?, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, now, now, repoID, path)
return err return err
} }
@@ -181,7 +190,7 @@ func (s *Store) SetRPMMirrorSyncEnabled(repoID string, path string, enabled bool
var now int64 var now int64
var err error var err error
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_enabled = ?, dirty = CASE WHEN ? THEN 1 ELSE dirty END, next_sync_at = CASE WHEN ? THEN ? ELSE next_sync_at END, updated_at = ? WHERE repo_id = ? AND path = ?`, _, err = s.DB.Exec(`UPDATE rpm_repo_dirs SET sync_enabled = ?, dirty = CASE WHEN ? THEN 1 ELSE dirty END, next_sync_at = CASE WHEN ? THEN ? ELSE next_sync_at END, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`,
enabled, enabled, enabled, now, now, repoID, path) enabled, enabled, enabled, now, now, repoID, path)
return err return err
} }
@@ -200,7 +209,7 @@ func (s *Store) ListRPMMirrorPaths() ([]models.RPMMirrorTask, error) {
var item models.RPMMirrorTask var item models.RPMMirrorTask
var err error var err error
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT d.repo_id, r.path, d.path SELECT r.public_id, r.path, d.path
FROM rpm_repo_dirs d FROM rpm_repo_dirs d
JOIN repos r ON r.id = d.repo_id JOIN repos r ON r.id = d.repo_id
WHERE d.mode = 'mirror'`) WHERE d.mode = 'mirror'`)
@@ -222,6 +231,18 @@ func (s *Store) ListRPMMirrorPaths() ([]models.RPMMirrorTask, error) {
return out, nil return out, nil
} }
func (s *Store) HasRunningRPMMirrorTask(repoID string) (bool, error) {
var row *sql.Row
var count int64
var err error
row = s.DB.QueryRow(`SELECT COUNT(1) FROM rpm_repo_dirs WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND mode = 'mirror' AND sync_running = 1`, repoID)
err = row.Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *Store) CreateRPMMirrorRun(repoID string, path string, startedAt int64) (string, error) { func (s *Store) CreateRPMMirrorRun(repoID string, path string, startedAt int64) (string, error) {
var id string var id string
var err error var err error
@@ -229,7 +250,7 @@ func (s *Store) CreateRPMMirrorRun(repoID string, path string, startedAt int64)
if err != nil { if err != nil {
return "", err return "", err
} }
_, err = s.DB.Exec(`INSERT INTO rpm_mirror_runs (id, repo_id, path, started_at, status) VALUES (?, ?, ?, ?, 'running')`, id, repoID, path, startedAt) _, err = s.DB.Exec(`INSERT INTO rpm_mirror_runs (public_id, repo_id, path, started_at, status) VALUES (?, (SELECT id FROM repos WHERE public_id = ?), ?, ?, 'running')`, id, repoID, path, startedAt)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -238,7 +259,7 @@ func (s *Store) CreateRPMMirrorRun(repoID string, path string, startedAt int64)
func (s *Store) FinishRPMMirrorRun(id string, finishedAt int64, status string, step string, total int64, done int64, failed int64, deleted int64, revision string, errMsg string) error { func (s *Store) FinishRPMMirrorRun(id string, finishedAt int64, status string, step string, total int64, done int64, failed int64, deleted int64, revision string, errMsg string) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE rpm_mirror_runs SET finished_at = ?, status = ?, step = ?, total = ?, done = ?, failed = ?, deleted = ?, revision = ?, error = ? WHERE id = ?`, _, err = s.DB.Exec(`UPDATE rpm_mirror_runs SET finished_at = ?, status = ?, step = ?, total = ?, done = ?, failed = ?, deleted = ?, revision = ?, error = ? WHERE public_id = ?`,
finishedAt, status, step, total, done, failed, deleted, revision, errMsg, id) finishedAt, status, step, total, done, failed, deleted, revision, errMsg, id)
return err return err
} }
@@ -251,7 +272,12 @@ func (s *Store) ListRPMMirrorRuns(repoID string, path string, limit int) ([]mode
if limit <= 0 { if limit <= 0 {
limit = 20 limit = 20
} }
rows, err = s.DB.Query(`SELECT id, repo_id, path, started_at, finished_at, status, step, total, done, failed, deleted, revision, error FROM rpm_mirror_runs WHERE repo_id = ? AND path = ? ORDER BY started_at DESC LIMIT ?`, repoID, path, limit) rows, err = s.DB.Query(`SELECT m.public_id, r.public_id, m.path, m.started_at, m.finished_at, m.status, m.step, m.total, m.done, m.failed, m.deleted, m.revision, m.error
FROM rpm_mirror_runs m
JOIN repos r ON r.id = m.repo_id
WHERE r.public_id = ? AND m.path = ?
ORDER BY m.started_at DESC
LIMIT ?`, repoID, path, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -274,7 +300,7 @@ func (s *Store) DeleteRPMMirrorRuns(repoID string, path string) (int64, error) {
var res sql.Result var res sql.Result
var count int64 var count int64
var err error var err error
res, err = s.DB.Exec(`DELETE FROM rpm_mirror_runs WHERE repo_id = ? AND path = ?`, repoID, path) res, err = s.DB.Exec(`DELETE FROM rpm_mirror_runs WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, repoID, path)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -299,12 +325,12 @@ func (s *Store) CleanupRPMMirrorRunsRetention(repoID string, path string, keepCo
cutoff = now - int64(keepDays*24*60*60) cutoff = now - int64(keepDays*24*60*60)
_, err = s.DB.Exec(` _, err = s.DB.Exec(`
DELETE FROM rpm_mirror_runs DELETE FROM rpm_mirror_runs
WHERE repo_id = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?)
AND path = ? AND path = ?
AND started_at < ? AND started_at < ?
AND id NOT IN ( AND id NOT IN (
SELECT id FROM rpm_mirror_runs SELECT id FROM rpm_mirror_runs
WHERE repo_id = ? AND path = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?
ORDER BY started_at DESC ORDER BY started_at DESC
LIMIT ? LIMIT ?
) )
@@ -323,7 +349,7 @@ func normalizeRPMRepoMode(mode string) string {
func (s *Store) DeleteRPMRepoDir(repoID string, path string) error { func (s *Store) DeleteRPMRepoDir(repoID string, path string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM rpm_repo_dirs WHERE repo_id = ? AND path = ?`, repoID, path) _, err = s.DB.Exec(`DELETE FROM rpm_repo_dirs WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, repoID, path)
return err return err
} }
@@ -331,7 +357,7 @@ func (s *Store) DeleteRPMRepoDirSubtree(repoID string, path string) error {
var prefix string var prefix string
var err error var err error
prefix = path + "/" prefix = path + "/"
_, err = s.DB.Exec(`DELETE FROM rpm_repo_dirs WHERE repo_id = ? AND (path = ? OR path LIKE (? || '%'))`, repoID, path, prefix) _, err = s.DB.Exec(`DELETE FROM rpm_repo_dirs WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND (path = ? OR path LIKE (? || '%'))`, repoID, path, prefix)
return err return err
} }
@@ -346,14 +372,19 @@ func (s *Store) MoveRPMRepoDir(repoID string, oldPath string, newPath string) er
return err return err
} }
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_, err = tx.Exec(`UPDATE rpm_repo_dirs SET path = ?, updated_at = ? WHERE repo_id = ? AND path = ?`, newPath, now, repoID, oldPath) oldPrefix = oldPath + "/"
newPrefix = newPath + "/"
_, err = tx.Exec(`DELETE FROM rpm_mirror_runs WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND (path = ? OR path LIKE (? || '%'))`, repoID, oldPath, oldPrefix)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return err return err
} }
oldPrefix = oldPath + "/" _, err = tx.Exec(`UPDATE rpm_repo_dirs SET path = ?, updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path = ?`, newPath, now, repoID, oldPath)
newPrefix = newPath + "/" if err != nil {
_, err = tx.Exec(`UPDATE rpm_repo_dirs SET path = (? || SUBSTR(path, ?)), updated_at = ? WHERE repo_id = ? AND path LIKE (? || '%')`, newPrefix, len(oldPrefix)+1, now, repoID, oldPrefix) _ = tx.Rollback()
return err
}
_, err = tx.Exec(`UPDATE rpm_repo_dirs SET path = (? || SUBSTR(path, ?)), updated_at = ? WHERE repo_id = (SELECT id FROM repos WHERE public_id = ?) AND path LIKE (? || '%')`, newPrefix, len(oldPrefix)+1, now, repoID, oldPrefix)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return err return err

View File

@@ -28,7 +28,7 @@ func (s *Store) CreateUser(user models.User, passwordHash string) (models.User,
user.AuthSource = "db" user.AuthSource = "db"
} }
_, err = s.DB.Exec(` _, err = s.DB.Exec(`
INSERT INTO users (id, username, display_name, email, password_hash, is_admin, disabled, auth_source, created_at, updated_at) INSERT INTO users (public_id, username, display_name, email, password_hash, is_admin, disabled, auth_source, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthSource, now, now) `, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthSource, now, now)
return user, err return user, err
@@ -41,7 +41,7 @@ func (s *Store) UpdateUser(user models.User) error {
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
user.UpdatedAt = nowUnix user.UpdatedAt = nowUnix
_, err = s.DB.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, updated_at = ? WHERE id = ?`, _, err = s.DB.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, updated_at = ? WHERE public_id = ?`,
user.DisplayName, user.Email, user.IsAdmin, user.Disabled, now, user.ID) user.DisplayName, user.Email, user.IsAdmin, user.Disabled, now, user.ID)
return err return err
} }
@@ -58,7 +58,7 @@ func (s *Store) UpdateUserWithPassword(user models.User, passwordHash string) er
if err != nil { if err != nil {
return err return err
} }
_, err = tx.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, password_hash = ?, updated_at = ? WHERE id = ?`, _, err = tx.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, password_hash = ?, updated_at = ? WHERE public_id = ?`,
user.DisplayName, user.Email, user.IsAdmin, user.Disabled, passwordHash, now, user.ID) user.DisplayName, user.Email, user.IsAdmin, user.Disabled, passwordHash, now, user.ID)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
@@ -73,7 +73,7 @@ func (s *Store) UpdateUserWithPassword(user models.User, passwordHash string) er
func (s *Store) SetUserPassword(userID, passwordHash string) error { func (s *Store) SetUserPassword(userID, passwordHash string) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?`, passwordHash, time.Now().UTC(), userID) _, err = s.DB.Exec(`UPDATE users SET password_hash = ?, updated_at = ? WHERE public_id = ?`, passwordHash, time.Now().UTC(), userID)
return err return err
} }
@@ -83,7 +83,7 @@ func (s *Store) GetUserByID(id string) (models.User, error) {
var created time.Time var created time.Time
var updated time.Time var updated time.Time
var err error var err error
row = s.DB.QueryRow(`SELECT id, username, display_name, email, is_admin, disabled, auth_source, created_at, updated_at FROM users WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT public_id, username, display_name, email, is_admin, disabled, auth_source, created_at, updated_at FROM users WHERE public_id = ?`, id)
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &created, &updated) err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &created, &updated)
if err != nil { if err != nil {
return user, err return user, err
@@ -100,7 +100,7 @@ func (s *Store) GetUserByUsername(username string) (models.User, string, error)
var err error var err error
var created time.Time var created time.Time
var updated time.Time var updated time.Time
row = s.DB.QueryRow(`SELECT id, username, display_name, email, is_admin, disabled, auth_source, password_hash, created_at, updated_at FROM users WHERE username = ?`, username) row = s.DB.QueryRow(`SELECT public_id, username, display_name, email, is_admin, disabled, auth_source, password_hash, created_at, updated_at FROM users WHERE username = ?`, username)
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &passwordHash, &created, &updated) err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &passwordHash, &created, &updated)
if err != nil { if err != nil {
return user, passwordHash.String, err return user, passwordHash.String, err
@@ -117,7 +117,7 @@ func (s *Store) ListUsers() ([]models.User, error) {
var u models.User var u models.User
var created time.Time var created time.Time
var updated time.Time var updated time.Time
rows, err = s.DB.Query(`SELECT id, username, display_name, email, is_admin, disabled, auth_source, created_at, updated_at FROM users ORDER BY username`) rows, err = s.DB.Query(`SELECT public_id, username, display_name, email, is_admin, disabled, auth_source, created_at, updated_at FROM users ORDER BY username`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -136,7 +136,7 @@ func (s *Store) ListUsers() ([]models.User, error) {
func (s *Store) DeleteUser(id string) error { func (s *Store) DeleteUser(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM users WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM users WHERE public_id = ?`, id)
return err return err
} }
@@ -520,7 +520,7 @@ func (s *Store) SetUserDisabled(id string, disabled bool) error {
var err error var err error
var now time.Time var now time.Time
now = time.Now().UTC() now = time.Now().UTC()
_, err = s.DB.Exec(`UPDATE users SET disabled = ?, updated_at = ? WHERE id = ?`, disabled, now, id) _, err = s.DB.Exec(`UPDATE users SET disabled = ?, updated_at = ? WHERE public_id = ?`, disabled, now, id)
return err return err
} }
@@ -546,7 +546,8 @@ func (s *Store) CreateAPIKey(userID string, name string, tokenHash string, prefi
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Disabled: false, Disabled: false,
} }
_, err = s.DB.Exec(`INSERT INTO api_keys (id, user_id, name, token_hash, token_prefix, created_at, last_used_at, expires_at, disabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO api_keys (public_id, user_id, name, token_hash, token_prefix, created_at, last_used_at, expires_at, disabled)
VALUES (?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?, ?, ?, ?)`,
key.ID, key.UserID, key.Name, tokenHash, key.Prefix, key.CreatedAt, key.LastUsedAt, key.ExpiresAt, key.Disabled) key.ID, key.UserID, key.Name, tokenHash, key.Prefix, key.CreatedAt, key.LastUsedAt, key.ExpiresAt, key.Disabled)
return key, err return key, err
} }
@@ -556,7 +557,11 @@ func (s *Store) ListAPIKeys(userID string) ([]models.APIKey, error) {
var err error var err error
var keys []models.APIKey var keys []models.APIKey
var key models.APIKey var key models.APIKey
rows, err = s.DB.Query(`SELECT id, user_id, name, token_prefix, created_at, last_used_at, expires_at, disabled FROM api_keys WHERE user_id = ? ORDER BY created_at DESC`, userID) rows, err = s.DB.Query(`SELECT k.public_id, u.public_id, k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled
FROM api_keys k
JOIN users u ON u.id = k.user_id
WHERE u.public_id = ?
ORDER BY k.created_at DESC`, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -573,25 +578,25 @@ func (s *Store) ListAPIKeys(userID string) ([]models.APIKey, error) {
func (s *Store) DeleteAPIKey(userID string, id string) error { func (s *Store) DeleteAPIKey(userID string, id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM api_keys WHERE id = ? AND user_id = ?`, id, userID) _, err = s.DB.Exec(`DELETE FROM api_keys WHERE public_id = ? AND user_id = (SELECT id FROM users WHERE public_id = ?)`, id, userID)
return err return err
} }
func (s *Store) SetAPIKeyDisabled(userID string, id string, disabled bool) error { func (s *Store) SetAPIKeyDisabled(userID string, id string, disabled bool) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE api_keys SET disabled = ? WHERE id = ? AND user_id = ?`, disabled, id, userID) _, err = s.DB.Exec(`UPDATE api_keys SET disabled = ? WHERE public_id = ? AND user_id = (SELECT id FROM users WHERE public_id = ?)`, disabled, id, userID)
return err return err
} }
func (s *Store) DeleteAPIKeyByID(id string) error { func (s *Store) DeleteAPIKeyByID(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM api_keys WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM api_keys WHERE public_id = ?`, id)
return err return err
} }
func (s *Store) SetAPIKeyDisabledByID(id string, disabled bool) error { func (s *Store) SetAPIKeyDisabledByID(id string, disabled bool) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE api_keys SET disabled = ? WHERE id = ?`, disabled, id) _, err = s.DB.Exec(`UPDATE api_keys SET disabled = ? WHERE public_id = ?`, disabled, id)
return err return err
} }
@@ -607,11 +612,11 @@ func (s *Store) ListAPIKeysAdmin(userID string, query string) ([]models.AdminAPI
if userID != "" && query != "" { if userID != "" && query != "" {
sqlQuery = ` sqlQuery = `
SELECT SELECT
k.id, k.user_id, u.username, u.display_name, u.email, k.public_id, u.public_id, u.username, u.display_name, u.email,
k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled
FROM api_keys k FROM api_keys k
JOIN users u ON u.id = k.user_id JOIN users u ON u.id = k.user_id
WHERE k.user_id = ? WHERE u.public_id = ?
AND (k.name LIKE ? OR k.token_prefix LIKE ? OR u.username LIKE ? OR u.display_name LIKE ? OR u.email LIKE ?) AND (k.name LIKE ? OR k.token_prefix LIKE ? OR u.username LIKE ? OR u.display_name LIKE ? OR u.email LIKE ?)
ORDER BY k.created_at DESC ORDER BY k.created_at DESC
` `
@@ -619,18 +624,18 @@ func (s *Store) ListAPIKeysAdmin(userID string, query string) ([]models.AdminAPI
} else if userID != "" { } else if userID != "" {
sqlQuery = ` sqlQuery = `
SELECT SELECT
k.id, k.user_id, u.username, u.display_name, u.email, k.public_id, u.public_id, u.username, u.display_name, u.email,
k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled
FROM api_keys k FROM api_keys k
JOIN users u ON u.id = k.user_id JOIN users u ON u.id = k.user_id
WHERE k.user_id = ? WHERE u.public_id = ?
ORDER BY k.created_at DESC ORDER BY k.created_at DESC
` `
rows, err = s.DB.Query(sqlQuery, userID) rows, err = s.DB.Query(sqlQuery, userID)
} else if query != "" { } else if query != "" {
sqlQuery = ` sqlQuery = `
SELECT SELECT
k.id, k.user_id, u.username, u.display_name, u.email, k.public_id, u.public_id, u.username, u.display_name, u.email,
k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled
FROM api_keys k FROM api_keys k
JOIN users u ON u.id = k.user_id JOIN users u ON u.id = k.user_id
@@ -641,7 +646,7 @@ func (s *Store) ListAPIKeysAdmin(userID string, query string) ([]models.AdminAPI
} else { } else {
sqlQuery = ` sqlQuery = `
SELECT SELECT
k.id, k.user_id, u.username, u.display_name, u.email, k.public_id, u.public_id, u.username, u.display_name, u.email,
k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled
FROM api_keys k FROM api_keys k
JOIN users u ON u.id = k.user_id JOIN users u ON u.id = k.user_id
@@ -680,7 +685,7 @@ func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
var row *sql.Row var row *sql.Row
var created time.Time var created time.Time
var updated time.Time var updated time.Time
var keyID string var keyID int64
var now time.Time var now time.Time
var nowUnix int64 var nowUnix int64
var err error var err error
@@ -688,7 +693,7 @@ func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
now = time.Now().UTC() now = time.Now().UTC()
currentUnix = now.Unix() currentUnix = now.Unix()
row = s.DB.QueryRow(` row = s.DB.QueryRow(`
SELECT u.id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, k.id SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, k.id
FROM api_keys k FROM api_keys k
JOIN users u ON u.id = k.user_id JOIN users u ON u.id = k.user_id
WHERE k.token_hash = ? WHERE k.token_hash = ?
@@ -711,7 +716,8 @@ func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
func (s *Store) CreateSession(userID, token string, expiresAt time.Time) error { func (s *Store) CreateSession(userID, token string, expiresAt time.Time) error {
var err error var err error
_, err = s.DB.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at) VALUES (?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at)
VALUES (?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?)`,
mustID(), userID, token, expiresAt, time.Now().UTC()) mustID(), userID, token, expiresAt, time.Now().UTC())
return err return err
} }
@@ -730,7 +736,7 @@ func (s *Store) GetSessionUser(token string) (models.User, time.Time, error) {
var created time.Time var created time.Time
var updated time.Time var updated time.Time
row = s.DB.QueryRow(` row = s.DB.QueryRow(`
SELECT u.id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, s.expires_at SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, s.expires_at
FROM sessions s JOIN users u ON u.id = s.user_id FROM sessions s JOIN users u ON u.id = s.user_id
WHERE s.token = ? AND u.disabled = 0 WHERE s.token = ? AND u.disabled = 0
`, token) `, token)
@@ -770,8 +776,8 @@ func (s *Store) CreateProject(project models.Project) (models.Project, error) {
if project.HomePage == "" { if project.HomePage == "" {
project.HomePage = "info" project.HomePage = "info"
} }
_, err = tx.Exec(`INSERT INTO projects (id, slug, name, description, home_page, created_by, updated_by, created_at, updated_at, created_at_unix, updated_at_unix) _, err = tx.Exec(`INSERT INTO projects (public_id, slug, name, description, home_page, created_by, updated_by, created_at, updated_at, created_at_unix, updated_at_unix)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?)`,
project.ID, project.ID,
project.Slug, project.Slug,
project.Name, project.Name,
@@ -788,7 +794,8 @@ func (s *Store) CreateProject(project models.Project) (models.Project, error) {
_ = tx.Rollback() _ = tx.Rollback()
return project, err return project, err
} }
_, err = tx.Exec(`INSERT INTO project_members (project_id, user_id, role, created_at) VALUES (?, ?, ?, ?)`, _, err = tx.Exec(`INSERT INTO project_members (project_id, user_id, role, created_at)
VALUES ((SELECT id FROM projects WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`,
project.ID, project.CreatedBy, "admin", now) project.ID, project.CreatedBy, "admin", now)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
@@ -807,7 +814,7 @@ func (s *Store) UpdateProject(project models.Project) error {
if project.HomePage == "" { if project.HomePage == "" {
project.HomePage = "info" project.HomePage = "info"
} }
_, err = s.DB.Exec(`UPDATE projects SET slug = ?, name = ?, description = ?, home_page = ?, updated_at = ?, updated_by = ?, updated_at_unix = ? WHERE id = ?`, _, err = s.DB.Exec(`UPDATE projects SET slug = ?, name = ?, description = ?, home_page = ?, updated_at = ?, updated_by = (SELECT id FROM users WHERE public_id = ?), updated_at_unix = ? WHERE public_id = ?`,
project.Slug, project.Slug,
project.Name, project.Name,
project.Description, project.Description,
@@ -824,14 +831,14 @@ func (s *Store) GetProject(id string) (models.Project, error) {
var project models.Project var project models.Project
var row *sql.Row var row *sql.Row
row = s.DB.QueryRow(` row = s.DB.QueryRow(`
SELECT p.id, p.slug, p.name, p.description, p.home_page, SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users c ON c.id = p.created_by
LEFT JOIN users u ON u.id = p.updated_by LEFT JOIN users u ON u.id = p.updated_by
WHERE p.id = ? WHERE p.public_id = ?
`, id) `, id)
return project, row.Scan( return project, row.Scan(
&project.ID, &project.ID,
@@ -853,8 +860,8 @@ func (s *Store) GetProjectBySlug(slug string) (models.Project, error) {
var row *sql.Row var row *sql.Row
var err error var err error
row = s.DB.QueryRow(` row = s.DB.QueryRow(`
SELECT p.id, p.slug, p.name, p.description, p.home_page, SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
@@ -887,8 +894,8 @@ func (s *Store) ListProjects() ([]models.Project, error) {
var projects []models.Project var projects []models.Project
var p models.Project var p models.Project
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT p.id, p.slug, p.name, p.description, p.home_page, SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
@@ -928,15 +935,15 @@ func (s *Store) ListProjectsForUser(userID string) ([]models.Project, error) {
var projects []models.Project var projects []models.Project
var p models.Project var p models.Project
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT p.id, p.slug, p.name, p.description, p.home_page, SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
JOIN project_members m ON m.project_id = p.id JOIN project_members m ON m.project_id = p.id
LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users c ON c.id = p.created_by
LEFT JOIN users u ON u.id = p.updated_by LEFT JOIN users u ON u.id = p.updated_by
WHERE m.user_id = ? WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?)
ORDER BY p.name ORDER BY p.name
`, userID) `, userID)
if err != nil { if err != nil {
@@ -978,8 +985,8 @@ func (s *Store) ListProjectsFiltered(limit int, offset int, query string) ([]mod
} }
if query == "" { if query == "" {
rows, err = s.DB.Query( rows, err = s.DB.Query(
`SELECT p.id, p.slug, p.name, p.description, p.home_page, `SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
@@ -991,8 +998,8 @@ func (s *Store) ListProjectsFiltered(limit int, offset int, query string) ([]mod
) )
} else { } else {
rows, err = s.DB.Query( rows, err = s.DB.Query(
`SELECT p.id, p.slug, p.name, p.description, p.home_page, `SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
@@ -1045,15 +1052,15 @@ func (s *Store) ListProjectsFilteredForUser(userID string, limit int, offset int
} }
if query == "" { if query == "" {
rows, err = s.DB.Query( rows, err = s.DB.Query(
`SELECT p.id, p.slug, p.name, p.description, p.home_page, `SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
JOIN project_members m ON m.project_id = p.id JOIN project_members m ON m.project_id = p.id
LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users c ON c.id = p.created_by
LEFT JOIN users u ON u.id = p.updated_by LEFT JOIN users u ON u.id = p.updated_by
WHERE m.user_id = ? WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?)
ORDER BY p.name LIMIT ? OFFSET ?`, ORDER BY p.name LIMIT ? OFFSET ?`,
userID, userID,
limit, limit,
@@ -1061,15 +1068,15 @@ func (s *Store) ListProjectsFilteredForUser(userID string, limit int, offset int
) )
} else { } else {
rows, err = s.DB.Query( rows, err = s.DB.Query(
`SELECT p.id, p.slug, p.name, p.description, p.home_page, `SELECT p.public_id, p.slug, p.name, p.description, p.home_page,
p.created_by, p.updated_by, c.public_id, u.public_id,
COALESCE(c.username, ''), COALESCE(u.username, ''), COALESCE(c.username, ''), COALESCE(u.username, ''),
p.created_at_unix, p.updated_at_unix p.created_at_unix, p.updated_at_unix
FROM projects p FROM projects p
JOIN project_members m ON m.project_id = p.id JOIN project_members m ON m.project_id = p.id
LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users c ON c.id = p.created_by
LEFT JOIN users u ON u.id = p.updated_by LEFT JOIN users u ON u.id = p.updated_by
WHERE m.user_id = ? AND (p.name LIKE ? OR p.slug LIKE ?) WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?) AND (p.name LIKE ? OR p.slug LIKE ?)
ORDER BY p.name LIMIT ? OFFSET ?`, ORDER BY p.name LIMIT ? OFFSET ?`,
userID, userID,
"%"+query+"%", "%"+query+"%",
@@ -1106,7 +1113,7 @@ func (s *Store) ListProjectsFilteredForUser(userID string, limit int, offset int
func (s *Store) DeleteProject(id string) error { func (s *Store) DeleteProject(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM projects WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM projects WHERE public_id = ?`, id)
return err return err
} }
@@ -1116,20 +1123,25 @@ func (s *Store) AddProjectMember(projectID, userID, role string) (models.Project
var now time.Time var now time.Time
now = time.Now().UTC() now = time.Now().UTC()
member = models.ProjectMember{ProjectID: projectID, UserID: userID, Role: role, CreatedAt: now.Unix()} member = models.ProjectMember{ProjectID: projectID, UserID: userID, Role: role, CreatedAt: now.Unix()}
_, err = s.DB.Exec(`INSERT INTO project_members (project_id, user_id, role, created_at) VALUES (?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO project_members (project_id, user_id, role, created_at)
VALUES ((SELECT id FROM projects WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`,
member.ProjectID, member.UserID, member.Role, now) member.ProjectID, member.UserID, member.Role, now)
return member, err return member, err
} }
func (s *Store) UpdateProjectMemberRole(projectID, userID, role string) error { func (s *Store) UpdateProjectMemberRole(projectID, userID, role string) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE project_members SET role = ? WHERE project_id = ? AND user_id = ?`, role, projectID, userID) _, err = s.DB.Exec(`UPDATE project_members SET role = ?
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
AND user_id = (SELECT id FROM users WHERE public_id = ?)`, role, projectID, userID)
return err return err
} }
func (s *Store) RemoveProjectMember(projectID, userID string) error { func (s *Store) RemoveProjectMember(projectID, userID string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM project_members WHERE project_id = ? AND user_id = ?`, projectID, userID) _, err = s.DB.Exec(`DELETE FROM project_members
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
AND user_id = (SELECT id FROM users WHERE public_id = ?)`, projectID, userID)
return err return err
} }
@@ -1139,7 +1151,12 @@ func (s *Store) ListProjectMembers(projectID string) ([]models.ProjectMember, er
var members []models.ProjectMember var members []models.ProjectMember
var m models.ProjectMember var m models.ProjectMember
var created time.Time var created time.Time
rows, err = s.DB.Query(`SELECT project_id, user_id, role, created_at FROM project_members WHERE project_id = ? ORDER BY role`, projectID) rows, err = s.DB.Query(`SELECT p.public_id, u.public_id, m.role, m.created_at
FROM project_members m
JOIN projects p ON p.id = m.project_id
JOIN users u ON u.id = m.user_id
WHERE m.project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY m.role`, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1159,7 +1176,9 @@ func (s *Store) GetProjectMemberRole(projectID, userID string) (string, error) {
var role string var role string
var row *sql.Row var row *sql.Row
var err error var err error
row = s.DB.QueryRow(`SELECT role FROM project_members WHERE project_id = ? AND user_id = ?`, projectID, userID) row = s.DB.QueryRow(`SELECT role FROM project_members
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
AND user_id = (SELECT id FROM users WHERE public_id = ?)`, projectID, userID)
err = row.Scan(&role) err = row.Scan(&role)
if err != nil { if err != nil {
return "", err return "", err
@@ -1182,7 +1201,8 @@ func (s *Store) CreateRepo(repo models.Repo) (models.Repo, error) {
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
repo.CreatedAt = nowUnix repo.CreatedAt = nowUnix
_, err = s.DB.Exec(`INSERT INTO repos (id, project_id, name, type, path, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO repos (public_id, project_id, name, type, path, created_by, created_at)
VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?)`,
repo.ID, repo.ProjectID, repo.Name, repo.Type, repo.Path, repo.CreatedBy, now) repo.ID, repo.ProjectID, repo.Name, repo.Type, repo.Path, repo.CreatedBy, now)
return repo, err return repo, err
} }
@@ -1191,7 +1211,9 @@ func (s *Store) RepoNameExists(projectID string, name string, repoType string) (
var count int var count int
var row *sql.Row var row *sql.Row
var err error var err error
row = s.DB.QueryRow(`SELECT COUNT(1) FROM repos WHERE project_id = ? AND name = ? AND type = ?`, projectID, name, repoType) row = s.DB.QueryRow(`SELECT COUNT(1) FROM repos
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
AND name = ? AND type = ?`, projectID, name, repoType)
err = row.Scan(&count) err = row.Scan(&count)
if err != nil { if err != nil {
return false, err return false, err
@@ -1204,7 +1226,11 @@ func (s *Store) GetRepo(id string) (models.Repo, error) {
var row *sql.Row var row *sql.Row
var created time.Time var created time.Time
var err error var err error
row = s.DB.QueryRow(`SELECT id, project_id, name, type, path, created_by, created_at FROM repos WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos r
JOIN projects p ON p.id = r.project_id
JOIN users u ON u.id = r.created_by
WHERE r.public_id = ?`, id)
err = row.Scan(&repo.ID, &repo.ProjectID, &repo.Name, &repo.Type, &repo.Path, &repo.CreatedBy, &created) err = row.Scan(&repo.ID, &repo.ProjectID, &repo.Name, &repo.Type, &repo.Path, &repo.CreatedBy, &created)
if err != nil { if err != nil {
return repo, err return repo, err
@@ -1213,12 +1239,33 @@ func (s *Store) GetRepo(id string) (models.Repo, error) {
return repo, nil return repo, nil
} }
func (s *Store) GetRepoStorageIDs(id string) (int64, int64, error) {
var row *sql.Row
var projectID int64
var repoID int64
var err error
row = s.DB.QueryRow(`SELECT p.id, r.id
FROM repos r
JOIN projects p ON p.id = r.project_id
WHERE r.public_id = ?`, id)
err = row.Scan(&projectID, &repoID)
if err != nil {
return 0, 0, err
}
return projectID, repoID, nil
}
func (s *Store) GetRepoByProjectNameType(projectID string, name string, repoType string) (models.Repo, error) { func (s *Store) GetRepoByProjectNameType(projectID string, name string, repoType string) (models.Repo, error) {
var repo models.Repo var repo models.Repo
var row *sql.Row var row *sql.Row
var err error var err error
var created time.Time var created time.Time
row = s.DB.QueryRow(`SELECT id, project_id, name, type, path, created_by, created_at FROM repos WHERE project_id = ? AND name = ? AND type = ?`, row = s.DB.QueryRow(`SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos r
JOIN projects p ON p.id = r.project_id
JOIN users u ON u.id = r.created_by
WHERE r.project_id = (SELECT id FROM projects WHERE public_id = ?)
AND r.name = ? AND r.type = ?`,
projectID, name, repoType) projectID, name, repoType)
err = row.Scan(&repo.ID, &repo.ProjectID, &repo.Name, &repo.Type, &repo.Path, &repo.CreatedBy, &created) err = row.Scan(&repo.ID, &repo.ProjectID, &repo.Name, &repo.Type, &repo.Path, &repo.CreatedBy, &created)
if err != nil { if err != nil {
@@ -1236,15 +1283,20 @@ func (s *Store) ListRepos(projectID string) ([]models.Repo, error) {
var isForeign int var isForeign int
var created time.Time var created time.Time
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT id, project_id, name, type, path, created_by, created_at, 0 AS is_foreign SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at, 0 AS is_foreign
FROM repos
WHERE project_id = ?
UNION ALL
SELECT r.id, r.project_id, r.name, r.type, r.path, r.created_by, r.created_at, 1 AS is_foreign
FROM repos r FROM repos r
JOIN projects p ON p.id = r.project_id
JOIN users u ON u.id = r.created_by
WHERE r.project_id = (SELECT id FROM projects WHERE public_id = ?)
UNION ALL
SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at, 1 AS is_foreign
FROM repos r
JOIN projects p ON p.id = r.project_id
JOIN users u ON u.id = r.created_by
JOIN project_repos pr ON pr.repo_id = r.id JOIN project_repos pr ON pr.repo_id = r.id
WHERE pr.project_id = ? AND r.project_id <> ? WHERE pr.project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY name AND r.project_id <> (SELECT id FROM projects WHERE public_id = ?)
ORDER BY 3
`, projectID, projectID, projectID) `, projectID, projectID, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1269,10 +1321,12 @@ func (s *Store) ListReposOwned(projectID string) ([]models.Repo, error) {
var r models.Repo var r models.Repo
var created time.Time var created time.Time
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT id, project_id, name, type, path, created_by, created_at SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos FROM repos r
WHERE project_id = ? JOIN projects p ON p.id = r.project_id
ORDER BY name JOIN users u ON u.id = r.created_by
WHERE r.project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY r.name
`, projectID) `, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1296,9 +1350,11 @@ func (s *Store) ListAllRepos() ([]models.Repo, error) {
var r models.Repo var r models.Repo
var created time.Time var created time.Time
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT id, project_id, name, type, path, created_by, created_at SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos FROM repos r
ORDER BY name JOIN projects p ON p.id = r.project_id
JOIN users u ON u.id = r.created_by
ORDER BY r.name
`) `)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1324,6 +1380,7 @@ func (s *Store) ListReposByProjectIDs(projectIDs []string) ([]models.Repo, error
var placeholders []string var placeholders []string
var args []interface{} var args []interface{}
var i int var i int
var selected []interface{}
if len(projectIDs) == 0 { if len(projectIDs) == 0 {
return []models.Repo{}, nil return []models.Repo{}, nil
} }
@@ -1331,13 +1388,19 @@ func (s *Store) ListReposByProjectIDs(projectIDs []string) ([]models.Repo, error
args = make([]interface{}, len(projectIDs)) args = make([]interface{}, len(projectIDs))
for i = 0; i < len(projectIDs); i++ { for i = 0; i < len(projectIDs); i++ {
placeholders[i] = "?" placeholders[i] = "?"
args[i] = projectIDs[i] selected = append(selected, projectIDs[i])
}
args = make([]interface{}, len(selected))
for i = 0; i < len(selected); i++ {
args[i] = selected[i]
} }
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT id, project_id, name, type, path, created_by, created_at SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos FROM repos r
WHERE project_id IN (`+strings.Join(placeholders, ",")+`) JOIN projects p ON p.id = r.project_id
ORDER BY name JOIN users u ON u.id = r.created_by
WHERE p.public_id IN (`+strings.Join(placeholders, ",")+`)
ORDER BY r.name
`, args...) `, args...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1359,7 +1422,10 @@ func (s *Store) ListProjectIDsForUser(userID string) ([]string, error) {
var err error var err error
var ids []string var ids []string
var id string var id string
rows, err = s.DB.Query(`SELECT project_id FROM project_members WHERE user_id = ?`, userID) rows, err = s.DB.Query(`SELECT p.public_id
FROM project_members m
JOIN projects p ON p.id = m.project_id
WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?)`, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1376,26 +1442,29 @@ func (s *Store) ListProjectIDsForUser(userID string) ([]string, error) {
func (s *Store) UpdateRepo(repo models.Repo) error { func (s *Store) UpdateRepo(repo models.Repo) error {
var err error var err error
_, err = s.DB.Exec(`UPDATE repos SET name = ?, path = ? WHERE id = ?`, repo.Name, repo.Path, repo.ID) _, err = s.DB.Exec(`UPDATE repos SET name = ?, path = ? WHERE public_id = ?`, repo.Name, repo.Path, repo.ID)
return err return err
} }
func (s *Store) DeleteRepo(id string) error { func (s *Store) DeleteRepo(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM repos WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM repos WHERE public_id = ?`, id)
return err return err
} }
func (s *Store) AttachRepoToProject(projectID string, repoID string) error { func (s *Store) AttachRepoToProject(projectID string, repoID string) error {
var err error var err error
_, err = s.DB.Exec(`INSERT INTO project_repos (project_id, repo_id, created_at) VALUES (?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO project_repos (project_id, repo_id, created_at)
VALUES ((SELECT id FROM projects WHERE public_id = ?), (SELECT id FROM repos WHERE public_id = ?), ?)`,
projectID, repoID, time.Now().UTC()) projectID, repoID, time.Now().UTC())
return err return err
} }
func (s *Store) DetachRepoFromProject(projectID string, repoID string) error { func (s *Store) DetachRepoFromProject(projectID string, repoID string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM project_repos WHERE project_id = ? AND repo_id = ?`, projectID, repoID) _, err = s.DB.Exec(`DELETE FROM project_repos
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
AND repo_id = (SELECT id FROM repos WHERE public_id = ?)`, projectID, repoID)
return err return err
} }
@@ -1405,9 +1474,12 @@ func (s *Store) GetRepoProjectIDs(repoID string) ([]string, error) {
var ids []string var ids []string
var id string var id string
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT project_id FROM repos WHERE id = ? SELECT p.public_id FROM repos r JOIN projects p ON p.id = r.project_id WHERE r.public_id = ?
UNION UNION
SELECT project_id FROM project_repos WHERE repo_id = ? SELECT p.public_id
FROM project_repos pr
JOIN projects p ON p.id = pr.project_id
WHERE pr.repo_id = (SELECT id FROM repos WHERE public_id = ?)
`, repoID, repoID) `, repoID, repoID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1437,20 +1509,35 @@ func (s *Store) ListAvailableReposForProject(projectID string, query string, lim
} }
if query == "" { if query == "" {
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT id, project_id, name, type, path, created_by, created_at SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos FROM repos r
WHERE project_id <> ? AND id NOT IN (SELECT repo_id FROM project_repos WHERE project_id = ?) JOIN projects p ON p.id = r.project_id
ORDER BY name JOIN users u ON u.id = r.created_by
WHERE p.public_id <> ?
AND r.id NOT IN (
SELECT pr.repo_id
FROM project_repos pr
JOIN projects px ON px.id = pr.project_id
WHERE px.public_id = ?
)
ORDER BY r.name
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`, projectID, projectID, limit, offset) `, projectID, projectID, limit, offset)
} else { } else {
rows, err = s.DB.Query(` rows, err = s.DB.Query(`
SELECT id, project_id, name, type, path, created_by, created_at SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at
FROM repos FROM repos r
WHERE project_id <> ? JOIN projects p ON p.id = r.project_id
AND id NOT IN (SELECT repo_id FROM project_repos WHERE project_id = ?) JOIN users u ON u.id = r.created_by
AND name LIKE ? WHERE p.public_id <> ?
ORDER BY name AND r.id NOT IN (
SELECT pr.repo_id
FROM project_repos pr
JOIN projects px ON px.id = pr.project_id
WHERE px.public_id = ?
)
AND r.name LIKE ?
ORDER BY r.name
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`, projectID, projectID, "%"+query+"%", limit, offset) `, projectID, projectID, "%"+query+"%", limit, offset)
} }
@@ -1490,9 +1577,9 @@ func (s *Store) CreateIssue(issue models.Issue) (models.Issue, error) {
issue.Status = "open" issue.Status = "open"
} }
_, err = s.DB.Exec(` _, err = s.DB.Exec(`
INSERT INTO issues (id, project_id, title, body, status, created_by, assignee_id, created_at, updated_at) INSERT INTO issues (public_id, project_id, title, body, status, created_by, assignee_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)
`, issue.ID, issue.ProjectID, issue.Title, issue.Body, issue.Status, issue.CreatedBy, nullIfEmpty(issue.AssigneeID), now, now) `, issue.ID, issue.ProjectID, issue.Title, issue.Body, issue.Status, issue.CreatedBy, issue.AssigneeID, now, now)
return issue, err return issue, err
} }
@@ -1503,8 +1590,8 @@ func (s *Store) UpdateIssue(issue models.Issue) error {
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
issue.UpdatedAt = nowUnix issue.UpdatedAt = nowUnix
_, err = s.DB.Exec(`UPDATE issues SET title = ?, body = ?, status = ?, assignee_id = ?, updated_at = ? WHERE id = ?`, _, err = s.DB.Exec(`UPDATE issues SET title = ?, body = ?, status = ?, assignee_id = (SELECT id FROM users WHERE public_id = ?), updated_at = ? WHERE public_id = ?`,
issue.Title, issue.Body, issue.Status, nullIfEmpty(issue.AssigneeID), now, issue.ID) issue.Title, issue.Body, issue.Status, issue.AssigneeID, now, issue.ID)
return err return err
} }
@@ -1514,7 +1601,12 @@ func (s *Store) GetIssue(id string) (models.Issue, error) {
var created time.Time var created time.Time
var updated time.Time var updated time.Time
var err error var err error
row = s.DB.QueryRow(`SELECT id, project_id, title, body, status, created_by, COALESCE(assignee_id, ''), created_at, updated_at FROM issues WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT i.public_id, p.public_id, i.title, i.body, i.status, cu.public_id, COALESCE(au.public_id, ''), i.created_at, i.updated_at
FROM issues i
JOIN projects p ON p.id = i.project_id
JOIN users cu ON cu.id = i.created_by
LEFT JOIN users au ON au.id = i.assignee_id
WHERE i.public_id = ?`, id)
err = row.Scan(&issue.ID, &issue.ProjectID, &issue.Title, &issue.Body, &issue.Status, &issue.CreatedBy, &issue.AssigneeID, &created, &updated) err = row.Scan(&issue.ID, &issue.ProjectID, &issue.Title, &issue.Body, &issue.Status, &issue.CreatedBy, &issue.AssigneeID, &created, &updated)
if err != nil { if err != nil {
return issue, err return issue, err
@@ -1531,7 +1623,13 @@ func (s *Store) ListIssues(projectID string) ([]models.Issue, error) {
var issue models.Issue var issue models.Issue
var created time.Time var created time.Time
var updated time.Time var updated time.Time
rows, err = s.DB.Query(`SELECT id, project_id, title, body, status, created_by, COALESCE(assignee_id, ''), created_at, updated_at FROM issues WHERE project_id = ? ORDER BY created_at DESC`, projectID) rows, err = s.DB.Query(`SELECT i.public_id, p.public_id, i.title, i.body, i.status, cu.public_id, COALESCE(au.public_id, ''), i.created_at, i.updated_at
FROM issues i
JOIN projects p ON p.id = i.project_id
JOIN users cu ON cu.id = i.created_by
LEFT JOIN users au ON au.id = i.assignee_id
WHERE i.project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY i.created_at DESC`, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1563,7 +1661,8 @@ func (s *Store) AddIssueComment(comment models.IssueComment) (models.IssueCommen
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
comment.CreatedAt = nowUnix comment.CreatedAt = nowUnix
_, err = s.DB.Exec(`INSERT INTO issue_comments (id, issue_id, body, created_by, created_at) VALUES (?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO issue_comments (public_id, issue_id, body, created_by, created_at)
VALUES (?, (SELECT id FROM issues WHERE public_id = ?), ?, (SELECT id FROM users WHERE public_id = ?), ?)`,
comment.ID, comment.IssueID, comment.Body, comment.CreatedBy, now) comment.ID, comment.IssueID, comment.Body, comment.CreatedBy, now)
return comment, err return comment, err
} }
@@ -1583,7 +1682,8 @@ func (s *Store) CreateWikiPage(page models.WikiPage) (models.WikiPage, error) {
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
page.UpdatedAt = nowUnix page.UpdatedAt = nowUnix
_, err = s.DB.Exec(`INSERT INTO wiki_pages (id, project_id, title, slug, body, created_by, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO wiki_pages (public_id, project_id, title, slug, body, created_by, updated_at)
VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?)`,
page.ID, page.ProjectID, page.Title, page.Slug, page.Body, page.CreatedBy, now) page.ID, page.ProjectID, page.Title, page.Slug, page.Body, page.CreatedBy, now)
return page, err return page, err
} }
@@ -1595,7 +1695,7 @@ func (s *Store) UpdateWikiPage(page models.WikiPage) error {
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
page.UpdatedAt = nowUnix page.UpdatedAt = nowUnix
_, err = s.DB.Exec(`UPDATE wiki_pages SET title = ?, body = ?, updated_at = ? WHERE id = ?`, page.Title, page.Body, now, page.ID) _, err = s.DB.Exec(`UPDATE wiki_pages SET title = ?, body = ?, updated_at = ? WHERE public_id = ?`, page.Title, page.Body, now, page.ID)
return err return err
} }
@@ -1605,7 +1705,12 @@ func (s *Store) ListWikiPages(projectID string) ([]models.WikiPage, error) {
var pages []models.WikiPage var pages []models.WikiPage
var page models.WikiPage var page models.WikiPage
var updated time.Time var updated time.Time
rows, err = s.DB.Query(`SELECT id, project_id, title, slug, body, created_by, updated_at FROM wiki_pages WHERE project_id = ? ORDER BY title`, projectID) rows, err = s.DB.Query(`SELECT w.public_id, p.public_id, w.title, w.slug, w.body, u.public_id, w.updated_at
FROM wiki_pages w
JOIN projects p ON p.id = w.project_id
JOIN users u ON u.id = w.created_by
WHERE w.project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY w.title`, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1626,7 +1731,11 @@ func (s *Store) GetWikiPage(id string) (models.WikiPage, error) {
var row *sql.Row var row *sql.Row
var updated time.Time var updated time.Time
var err error var err error
row = s.DB.QueryRow(`SELECT id, project_id, title, slug, body, created_by, updated_at FROM wiki_pages WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT w.public_id, p.public_id, w.title, w.slug, w.body, u.public_id, w.updated_at
FROM wiki_pages w
JOIN projects p ON p.id = w.project_id
JOIN users u ON u.id = w.created_by
WHERE w.public_id = ?`, id)
err = row.Scan(&page.ID, &page.ProjectID, &page.Title, &page.Slug, &page.Body, &page.CreatedBy, &updated) err = row.Scan(&page.ID, &page.ProjectID, &page.Title, &page.Slug, &page.Body, &page.CreatedBy, &updated)
if err != nil { if err != nil {
return page, err return page, err
@@ -1650,7 +1759,8 @@ func (s *Store) CreateUpload(upload models.Upload) (models.Upload, error) {
now = time.Now().UTC() now = time.Now().UTC()
nowUnix = now.Unix() nowUnix = now.Unix()
upload.CreatedAt = nowUnix upload.CreatedAt = nowUnix
_, err = s.DB.Exec(`INSERT INTO uploads (id, project_id, filename, content_type, size, storage_path, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO uploads (public_id, project_id, filename, content_type, size, storage_path, created_by, created_at)
VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?)`,
upload.ID, upload.ProjectID, upload.Filename, upload.ContentType, upload.Size, upload.StoragePath, upload.CreatedBy, now) upload.ID, upload.ProjectID, upload.Filename, upload.ContentType, upload.Size, upload.StoragePath, upload.CreatedBy, now)
return upload, err return upload, err
} }
@@ -1660,7 +1770,11 @@ func (s *Store) GetUpload(id string) (models.Upload, error) {
var row *sql.Row var row *sql.Row
var created time.Time var created time.Time
var err error var err error
row = s.DB.QueryRow(`SELECT id, project_id, filename, content_type, size, storage_path, created_by, created_at FROM uploads WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT up.public_id, p.public_id, up.filename, up.content_type, up.size, up.storage_path, u.public_id, up.created_at
FROM uploads up
JOIN projects p ON p.id = up.project_id
JOIN users u ON u.id = up.created_by
WHERE up.public_id = ?`, id)
err = row.Scan(&upload.ID, &upload.ProjectID, &upload.Filename, &upload.ContentType, &upload.Size, &upload.StoragePath, &upload.CreatedBy, &created) err = row.Scan(&upload.ID, &upload.ProjectID, &upload.Filename, &upload.ContentType, &upload.Size, &upload.StoragePath, &upload.CreatedBy, &created)
if err != nil { if err != nil {
return upload, err return upload, err
@@ -1675,7 +1789,12 @@ func (s *Store) ListUploads(projectID string) ([]models.Upload, error) {
var uploads []models.Upload var uploads []models.Upload
var upload models.Upload var upload models.Upload
var created time.Time var created time.Time
rows, err = s.DB.Query(`SELECT id, project_id, filename, content_type, size, storage_path, created_by, created_at FROM uploads WHERE project_id = ? ORDER BY created_at DESC`, projectID) rows, err = s.DB.Query(`SELECT up.public_id, p.public_id, up.filename, up.content_type, up.size, up.storage_path, u.public_id, up.created_at
FROM uploads up
JOIN projects p ON p.id = up.project_id
JOIN users u ON u.id = up.created_by
WHERE up.project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY up.created_at DESC`, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -15,7 +15,7 @@ func (s *Store) ListTLSListeners() ([]models.TLSListener, error) {
var httpAddrs string var httpAddrs string
var httpsAddrs string var httpsAddrs string
var certAllowlist string var certAllowlist string
rows, err = s.DB.Query(`SELECT id, name, enabled, http_addrs, https_addrs, auth_policy, apply_policy_api, apply_policy_git, apply_policy_rpm, apply_policy_v2, client_cert_allowlist, tls_server_cert_source, tls_cert_file, tls_key_file, tls_pki_server_cert_id, tls_client_auth, tls_client_ca_file, tls_pki_client_ca_id, tls_min_version, created_at, updated_at FROM tls_listeners ORDER BY name`) rows, err = s.DB.Query(`SELECT l.public_id, l.name, l.enabled, l.http_addrs, l.https_addrs, l.auth_policy, l.apply_policy_api, l.apply_policy_git, l.apply_policy_rpm, l.apply_policy_v2, l.client_cert_allowlist, l.tls_server_cert_source, l.tls_cert_file, l.tls_key_file, l.tls_pki_server_cert_id, l.tls_client_auth, l.tls_client_ca_file, l.tls_pki_client_ca_id, l.tls_min_version, l.created_at, l.updated_at FROM tls_listeners l ORDER BY l.name`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -44,7 +44,7 @@ func (s *Store) GetTLSListener(id string) (models.TLSListener, error) {
var httpsAddrs string var httpsAddrs string
var certAllowlist string var certAllowlist string
var err error var err error
row = s.DB.QueryRow(`SELECT id, name, enabled, http_addrs, https_addrs, auth_policy, apply_policy_api, apply_policy_git, apply_policy_rpm, apply_policy_v2, client_cert_allowlist, tls_server_cert_source, tls_cert_file, tls_key_file, tls_pki_server_cert_id, tls_client_auth, tls_client_ca_file, tls_pki_client_ca_id, tls_min_version, created_at, updated_at FROM tls_listeners WHERE id = ?`, id) row = s.DB.QueryRow(`SELECT l.public_id, l.name, l.enabled, l.http_addrs, l.https_addrs, l.auth_policy, l.apply_policy_api, l.apply_policy_git, l.apply_policy_rpm, l.apply_policy_v2, l.client_cert_allowlist, l.tls_server_cert_source, l.tls_cert_file, l.tls_key_file, l.tls_pki_server_cert_id, l.tls_client_auth, l.tls_client_ca_file, l.tls_pki_client_ca_id, l.tls_min_version, l.created_at, l.updated_at FROM tls_listeners l WHERE l.public_id = ?`, id)
err = row.Scan(&item.ID, &item.Name, &item.Enabled, &httpAddrs, &httpsAddrs, &item.AuthPolicy, &item.ApplyPolicyAPI, &item.ApplyPolicyGit, &item.ApplyPolicyRPM, &item.ApplyPolicyV2, &certAllowlist, &item.TLSServerCertSource, &item.TLSCertFile, &item.TLSKeyFile, &item.TLSPKIServerCertID, &item.TLSClientAuth, &item.TLSClientCAFile, &item.TLSPKIClientCAID, &item.TLSMinVersion, &item.CreatedAt, &item.UpdatedAt) err = row.Scan(&item.ID, &item.Name, &item.Enabled, &httpAddrs, &httpsAddrs, &item.AuthPolicy, &item.ApplyPolicyAPI, &item.ApplyPolicyGit, &item.ApplyPolicyRPM, &item.ApplyPolicyV2, &certAllowlist, &item.TLSServerCertSource, &item.TLSCertFile, &item.TLSKeyFile, &item.TLSPKIServerCertID, &item.TLSClientAuth, &item.TLSClientCAFile, &item.TLSPKIClientCAID, &item.TLSMinVersion, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
@@ -69,7 +69,7 @@ func (s *Store) CreateTLSListener(item models.TLSListener) (models.TLSListener,
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.CreatedAt = now item.CreatedAt = now
item.UpdatedAt = now item.UpdatedAt = now
_, err = s.DB.Exec(`INSERT INTO tls_listeners (id, name, enabled, http_addrs, https_addrs, auth_policy, apply_policy_api, apply_policy_git, apply_policy_rpm, apply_policy_v2, client_cert_allowlist, tls_server_cert_source, tls_cert_file, tls_key_file, tls_pki_server_cert_id, tls_client_auth, tls_client_ca_file, tls_pki_client_ca_id, tls_min_version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, _, err = s.DB.Exec(`INSERT INTO tls_listeners (public_id, name, enabled, http_addrs, https_addrs, auth_policy, apply_policy_api, apply_policy_git, apply_policy_rpm, apply_policy_v2, client_cert_allowlist, tls_server_cert_source, tls_cert_file, tls_key_file, tls_pki_server_cert_id, tls_client_auth, tls_client_ca_file, tls_pki_client_ca_id, tls_min_version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.ID, item.Name, item.Enabled, strings.Join(item.HTTPAddrs, ","), strings.Join(item.HTTPSAddrs, ","), item.AuthPolicy, item.ApplyPolicyAPI, item.ApplyPolicyGit, item.ApplyPolicyRPM, item.ApplyPolicyV2, strings.Join(item.ClientCertAllowlist, ","), item.TLSServerCertSource, item.TLSCertFile, item.TLSKeyFile, item.TLSPKIServerCertID, item.TLSClientAuth, item.TLSClientCAFile, item.TLSPKIClientCAID, item.TLSMinVersion, item.CreatedAt, item.UpdatedAt) item.ID, item.Name, item.Enabled, strings.Join(item.HTTPAddrs, ","), strings.Join(item.HTTPSAddrs, ","), item.AuthPolicy, item.ApplyPolicyAPI, item.ApplyPolicyGit, item.ApplyPolicyRPM, item.ApplyPolicyV2, strings.Join(item.ClientCertAllowlist, ","), item.TLSServerCertSource, item.TLSCertFile, item.TLSKeyFile, item.TLSPKIServerCertID, item.TLSClientAuth, item.TLSClientCAFile, item.TLSPKIClientCAID, item.TLSMinVersion, item.CreatedAt, item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
@@ -82,13 +82,13 @@ func (s *Store) UpdateTLSListener(item models.TLSListener) error {
var now int64 var now int64
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
item.UpdatedAt = now item.UpdatedAt = now
_, err = s.DB.Exec(`UPDATE tls_listeners SET name = ?, enabled = ?, http_addrs = ?, https_addrs = ?, auth_policy = ?, apply_policy_api = ?, apply_policy_git = ?, apply_policy_rpm = ?, apply_policy_v2 = ?, client_cert_allowlist = ?, tls_server_cert_source = ?, tls_cert_file = ?, tls_key_file = ?, tls_pki_server_cert_id = ?, tls_client_auth = ?, tls_client_ca_file = ?, tls_pki_client_ca_id = ?, tls_min_version = ?, updated_at = ? WHERE id = ?`, _, err = s.DB.Exec(`UPDATE tls_listeners SET name = ?, enabled = ?, http_addrs = ?, https_addrs = ?, auth_policy = ?, apply_policy_api = ?, apply_policy_git = ?, apply_policy_rpm = ?, apply_policy_v2 = ?, client_cert_allowlist = ?, tls_server_cert_source = ?, tls_cert_file = ?, tls_key_file = ?, tls_pki_server_cert_id = ?, tls_client_auth = ?, tls_client_ca_file = ?, tls_pki_client_ca_id = ?, tls_min_version = ?, updated_at = ? WHERE public_id = ?`,
item.Name, item.Enabled, strings.Join(item.HTTPAddrs, ","), strings.Join(item.HTTPSAddrs, ","), item.AuthPolicy, item.ApplyPolicyAPI, item.ApplyPolicyGit, item.ApplyPolicyRPM, item.ApplyPolicyV2, strings.Join(item.ClientCertAllowlist, ","), item.TLSServerCertSource, item.TLSCertFile, item.TLSKeyFile, item.TLSPKIServerCertID, item.TLSClientAuth, item.TLSClientCAFile, item.TLSPKIClientCAID, item.TLSMinVersion, item.UpdatedAt, item.ID) item.Name, item.Enabled, strings.Join(item.HTTPAddrs, ","), strings.Join(item.HTTPSAddrs, ","), item.AuthPolicy, item.ApplyPolicyAPI, item.ApplyPolicyGit, item.ApplyPolicyRPM, item.ApplyPolicyV2, strings.Join(item.ClientCertAllowlist, ","), item.TLSServerCertSource, item.TLSCertFile, item.TLSKeyFile, item.TLSPKIServerCertID, item.TLSClientAuth, item.TLSClientCAFile, item.TLSPKIClientCAID, item.TLSMinVersion, item.UpdatedAt, item.ID)
return err return err
} }
func (s *Store) DeleteTLSListener(id string) error { func (s *Store) DeleteTLSListener(id string) error {
var err error var err error
_, err = s.DB.Exec(`DELETE FROM tls_listeners WHERE id = ?`, id) _, err = s.DB.Exec(`DELETE FROM tls_listeners WHERE public_id = ?`, id)
return err return err
} }

View File

@@ -161,6 +161,8 @@ func (s *HTTPServer) resolveRepo(repoName string) (models.Repo, models.Project,
var parts []string var parts []string
var project models.Project var project models.Project
var repo models.Repo var repo models.Repo
var projectStorageID int64
var repoStorageID int64
var err error var err error
var slug string var slug string
var name string var name string
@@ -186,12 +188,18 @@ func (s *HTTPServer) resolveRepo(repoName string) (models.Repo, models.Project,
if err != nil { if err != nil {
return repo, project, "", err return repo, project, "", err
} }
if repo.Path == "" { projectStorageID, repoStorageID, err = s.store.GetRepoStorageIDs(repo.ID)
repo.Path = filepath.Join(s.baseDir, project.ID, repo.Name) if err != nil {
return repo, project, "", err
} }
repo.Path = filepath.Join(s.baseDir, storageIDSegment(projectStorageID), storageIDSegment(repoStorageID))
return repo, project, image, nil return repo, project, image, nil
} }
func storageIDSegment(id int64) string {
return fmt.Sprintf("%016x", id)
}
func IsReservedImagePath(image string) bool { func IsReservedImagePath(image string) bool {
var cleaned string var cleaned string
var parts []string var parts []string

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,7 @@ type RPMRepoDir struct {
RepoID string `json:"repo_id"` RepoID string `json:"repo_id"`
Path string `json:"path"` Path string `json:"path"`
Mode string `json:"mode"` Mode string `json:"mode"`
AllowDelete bool `json:"allow_delete"`
RemoteURL string `json:"remote_url"` RemoteURL string `json:"remote_url"`
ConnectHost string `json:"connect_host"` ConnectHost string `json:"connect_host"`
HostHeader string `json:"host_header"` HostHeader string `json:"host_header"`

View File

@@ -1,8 +1,11 @@
package rpm package rpm
import "log" import "log"
import "os"
import "path/filepath"
import "strings" import "strings"
import "sync" import "sync"
import "time"
import repokit "repokit" import repokit "repokit"
@@ -24,6 +27,18 @@ func NewMetaManager() *MetaManager {
return mgr return mgr
} }
func (m *MetaManager) IsRunning(dir string) bool {
var state *metaState
var ok bool
m.mutex.Lock()
defer m.mutex.Unlock()
state, ok = m.states[dir]
if !ok || state == nil {
return false
}
return state.inProgress
}
func (m *MetaManager) Schedule(dir string) { func (m *MetaManager) Schedule(dir string) {
var state *metaState var state *metaState
var ok bool var ok bool
@@ -46,7 +61,15 @@ func (m *MetaManager) Schedule(dir string) {
func (m *MetaManager) run(dir string) { func (m *MetaManager) run(dir string) {
var err error var err error
var opts repokit.RpmRepoOptions var opts repokit.RpmRepoOptions
var state *metaState
var ok bool
var repodataDir string
var repomdPath string
var entries []os.DirEntry
var repomdInfo os.FileInfo
var statErr error
for { for {
log.Printf("rpm metadata: job begin dir=%s", dir)
opts = repokit.RpmDefaultRepoOptions() opts = repokit.RpmDefaultRepoOptions()
opts.LockMode = repokit.RpmLockFail opts.LockMode = repokit.RpmLockFail
opts.AllowMissingRepomd = true opts.AllowMissingRepomd = true
@@ -57,24 +80,55 @@ func (m *MetaManager) run(dir string) {
if err != nil { if err != nil {
if isLockError(err) { if isLockError(err) {
log.Printf("rpm metadata: lock busy dir=%s err=%v", dir, err) log.Printf("rpm metadata: lock busy dir=%s err=%v", dir, err)
m.states[dir].pending = true log.Printf("rpm metadata: job end dir=%s result=lock_busy", dir)
m.states[dir].inProgress = false state, ok = m.states[dir]
if ok {
state.pending = true
state.inProgress = false
}
m.mutex.Unlock() m.mutex.Unlock()
time.AfterFunc(2*time.Second, func() {
m.Schedule(dir)
})
return return
} }
log.Printf("rpm metadata: build failed dir=%s err=%v", dir, err) log.Printf("rpm metadata: build failed dir=%s err=%v", dir, err)
m.states[dir].inProgress = false log.Printf("rpm metadata: job end dir=%s result=failed err=%v", dir, err)
state, ok = m.states[dir]
if ok {
state.inProgress = false
}
m.mutex.Unlock() m.mutex.Unlock()
return return
} }
repodataDir = filepath.Join(dir, "repodata")
repomdPath = filepath.Join(repodataDir, "repomd.xml")
entries, err = os.ReadDir(repodataDir)
if err != nil {
log.Printf("rpm metadata: post-check dir=%s repodata_dir=%s read_err=%v", dir, repodataDir, err)
} else {
statErr = nil
repomdInfo = nil
repomdInfo, statErr = os.Stat(repomdPath)
if statErr != nil {
log.Printf("rpm metadata: post-check dir=%s repodata_entries=%d repomd_path=%s repomd_err=%v", dir, len(entries), repomdPath, statErr)
} else {
log.Printf("rpm metadata: post-check dir=%s repodata_entries=%d repomd_path=%s repomd_size=%d", dir, len(entries), repomdPath, repomdInfo.Size())
}
}
log.Printf("rpm metadata: build done dir=%s", dir) log.Printf("rpm metadata: build done dir=%s", dir)
if m.states[dir].pending { state, ok = m.states[dir]
m.states[dir].pending = false if ok && state.pending {
log.Printf("rpm metadata: job end dir=%s result=pending_rerun", dir)
state.pending = false
m.mutex.Unlock() m.mutex.Unlock()
continue continue
} }
m.states[dir].inProgress = false if ok {
state.inProgress = false
}
m.mutex.Unlock() m.mutex.Unlock()
log.Printf("rpm metadata: job end dir=%s result=success", dir)
return return
} }
} }

View File

@@ -2,7 +2,10 @@ package rpm
import "compress/gzip" import "compress/gzip"
import "context" import "context"
import "crypto/md5"
import "crypto/sha1"
import "crypto/sha256" import "crypto/sha256"
import "crypto/sha512"
import "crypto/tls" import "crypto/tls"
import "bytes" import "bytes"
import "encoding/hex" import "encoding/hex"
@@ -16,7 +19,9 @@ import "net/http"
import "net/url" import "net/url"
import "os" import "os"
import "path/filepath" import "path/filepath"
import "strconv"
import "strings" import "strings"
import "sync"
import "time" import "time"
import "codit/internal/db" import "codit/internal/db"
@@ -28,6 +33,8 @@ type MirrorManager struct {
logger *util.Logger logger *util.Logger
meta *MetaManager meta *MetaManager
stopCh chan struct{} stopCh chan struct{}
cancelMu sync.Mutex
cancelByKey map[string]context.CancelFunc
} }
type repomdDoc struct { type repomdDoc struct {
@@ -50,6 +57,7 @@ type primaryDoc struct {
type primaryPackage struct { type primaryPackage struct {
Location primaryLocation `xml:"location"` Location primaryLocation `xml:"location"`
Checksum primaryChecksum `xml:"checksum"` Checksum primaryChecksum `xml:"checksum"`
Time primaryTime `xml:"time"`
} }
type primaryLocation struct { type primaryLocation struct {
@@ -61,6 +69,18 @@ type primaryChecksum struct {
Value string `xml:",chardata"` Value string `xml:",chardata"`
} }
type mirrorChecksum struct {
Algo string
Value string
BuildTime int64
FileTime int64
}
type primaryTime struct {
File string `xml:"file,attr"`
Build string `xml:"build,attr"`
}
type mirrorHTTPConfig struct { type mirrorHTTPConfig struct {
BaseURL string BaseURL string
ConnectHost string ConnectHost string
@@ -78,10 +98,28 @@ func NewMirrorManager(store *db.Store, logger *util.Logger, meta *MetaManager) *
logger: logger, logger: logger,
meta: meta, meta: meta,
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
cancelByKey: make(map[string]context.CancelFunc),
} }
return m return m
} }
func (m *MirrorManager) CancelTask(repoID string, path string) bool {
var key string
var cancel context.CancelFunc
if m == nil {
return false
}
key = mirrorTaskKey(repoID, path)
m.cancelMu.Lock()
cancel = m.cancelByKey[key]
m.cancelMu.Unlock()
if cancel == nil {
return false
}
cancel()
return true
}
func (m *MirrorManager) Start() { func (m *MirrorManager) Start() {
var err error var err error
var tasks []models.RPMMirrorTask var tasks []models.RPMMirrorTask
@@ -161,16 +199,33 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
var revision string var revision string
var primaryHref string var primaryHref string
var primaryData []byte var primaryData []byte
var expected map[string]string var expected map[string]mirrorChecksum
var duplicateCount int
var runID string var runID string
var startedAt int64 var startedAt int64
var total int64 var total int64
var done int64 var done int64
var failed int64 var failed int64
var deleted int64 var deleted int64
var changed int64
var err error var err error
var syncCtx context.Context
var syncCancel context.CancelFunc
var canceled bool
var key string
localRoot = filepath.Join(task.RepoPath, filepath.FromSlash(task.MirrorPath)) localRoot = filepath.Join(task.RepoPath, filepath.FromSlash(task.MirrorPath))
startedAt = time.Now().UTC().Unix() startedAt = time.Now().UTC().Unix()
syncCtx, syncCancel = context.WithCancel(context.Background())
key = mirrorTaskKey(task.RepoID, task.MirrorPath)
m.cancelMu.Lock()
m.cancelByKey[key] = syncCancel
m.cancelMu.Unlock()
defer func() {
m.cancelMu.Lock()
delete(m.cancelByKey, key)
m.cancelMu.Unlock()
syncCancel()
}()
runID, err = m.store.CreateRPMMirrorRun(task.RepoID, task.MirrorPath, startedAt) runID, err = m.store.CreateRPMMirrorRun(task.RepoID, task.MirrorPath, startedAt)
if err != nil { if err != nil {
_ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, false, "", err.Error()) _ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, false, "", err.Error())
@@ -190,7 +245,7 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
m.logger.Write("rpm-mirror", util.LOG_INFO, "sync start repo=%s path=%s remote=%s", task.RepoID, task.MirrorPath, task.RemoteURL) m.logger.Write("rpm-mirror", util.LOG_INFO, "sync start repo=%s path=%s remote=%s", task.RepoID, task.MirrorPath, task.RemoteURL)
} }
_ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "fetch_repodata", 0, 0, 0, 0) _ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "fetch_repodata", 0, 0, 0, 0)
repomdData, err = mirrorFetch(client, cfg, "repodata/repomd.xml") repomdData, err = mirrorFetch(syncCtx, client, cfg, "repodata/repomd.xml")
if err != nil { if err != nil {
if m.logger != nil { if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=fetch_repodata err=%v", task.RepoID, task.MirrorPath, err) m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=fetch_repodata err=%v", task.RepoID, task.MirrorPath, err)
@@ -201,6 +256,9 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
} }
revision = sha256HexBytes(repomdData) revision = sha256HexBytes(repomdData)
if !task.Dirty && task.LastSyncedRevision != "" && task.LastSyncedRevision == revision { if !task.Dirty && task.LastSyncedRevision != "" && task.LastSyncedRevision == revision {
if m.meta != nil {
ensureRepodata(task, localRoot, m.meta, m.logger)
}
if m.logger != nil { if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_INFO, "sync done repo=%s path=%s status=no_change revision=%s", task.RepoID, task.MirrorPath, revision) m.logger.Write("rpm-mirror", util.LOG_INFO, "sync done repo=%s path=%s status=no_change revision=%s", task.RepoID, task.MirrorPath, revision)
} }
@@ -218,7 +276,7 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
return return
} }
_ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "fetch_primary", 0, 0, 0, 0) _ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "fetch_primary", 0, 0, 0, 0)
primaryData, err = mirrorFetch(client, cfg, primaryHref) primaryData, err = mirrorFetch(syncCtx, client, cfg, primaryHref)
if err != nil { if err != nil {
if m.logger != nil { if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=fetch_primary err=%v", task.RepoID, task.MirrorPath, err) m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=fetch_primary err=%v", task.RepoID, task.MirrorPath, err)
@@ -238,7 +296,7 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
return return
} }
} }
expected, err = parsePrimaryPackages(primaryData) expected, duplicateCount, err = parsePrimaryPackages(primaryData)
if err != nil { if err != nil {
if m.logger != nil { if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=parse_primary err=%v", task.RepoID, task.MirrorPath, err) m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=parse_primary err=%v", task.RepoID, task.MirrorPath, err)
@@ -247,16 +305,35 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
_ = m.store.FinishRPMMirrorRun(runID, time.Now().UTC().Unix(), "failed", "fetch_primary", 0, 0, 0, 0, "", err.Error()) _ = m.store.FinishRPMMirrorRun(runID, time.Now().UTC().Unix(), "failed", "fetch_primary", 0, 0, 0, 0, "", err.Error())
return return
} }
total, done, failed, deleted, err = m.applyMirror(task, localRoot, client, cfg, expected) if m.logger != nil {
if err != nil { m.logger.Write("rpm-mirror", util.LOG_INFO, "primary parsed repo=%s path=%s primary_href=%s packages=%d", task.RepoID, task.MirrorPath, primaryHref, len(expected))
if m.logger != nil { if duplicateCount > 0 {
m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=apply total=%d done=%d failed=%d deleted=%d err=%v", task.RepoID, task.MirrorPath, total, done, failed, deleted, err) m.logger.Write("rpm-mirror", util.LOG_WARN, "primary has duplicate package paths repo=%s path=%s primary_href=%s duplicates=%d", task.RepoID, task.MirrorPath, primaryHref, duplicateCount)
}
}
total, done, failed, deleted, changed, err = m.applyMirror(syncCtx, task, localRoot, client, cfg, expected)
if err != nil {
canceled = errors.Is(err, context.Canceled)
if m.logger != nil {
if canceled {
m.logger.Write("rpm-mirror", util.LOG_WARN, "sync canceled repo=%s path=%s step=apply total=%d done=%d failed=%d deleted=%d err=%v", task.RepoID, task.MirrorPath, total, done, failed, deleted, err)
} else {
m.logger.Write("rpm-mirror", util.LOG_ERROR, "sync failed repo=%s path=%s step=apply total=%d done=%d failed=%d deleted=%d err=%v", task.RepoID, task.MirrorPath, total, done, failed, deleted, err)
}
}
if canceled {
_ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, false, "", "sync canceled by user")
_ = m.store.FinishRPMMirrorRun(runID, time.Now().UTC().Unix(), "failed", "canceled", total, done, failed, deleted, "", "sync canceled by user")
} else {
_ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, false, "", err.Error())
_ = m.store.FinishRPMMirrorRun(runID, time.Now().UTC().Unix(), "failed", "apply", total, done, failed, deleted, "", err.Error())
} }
_ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, false, "", err.Error())
_ = m.store.FinishRPMMirrorRun(runID, time.Now().UTC().Unix(), "failed", "apply", total, done, failed, deleted, "", err.Error())
return return
} }
if m.meta != nil { if m.meta != nil && changed > 0 {
if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_INFO, "repodata schedule repo=%s path=%s reason=sync_changed changed=%d", task.RepoID, task.MirrorPath, changed)
}
m.meta.Schedule(localRoot) m.meta.Schedule(localRoot)
} }
_ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, true, revision, "") _ = m.store.FinishRPMMirrorTask(task.RepoID, task.MirrorPath, true, revision, "")
@@ -267,58 +344,91 @@ func (m *MirrorManager) syncOne(task models.RPMMirrorTask) {
} }
} }
func (m *MirrorManager) applyMirror(task models.RPMMirrorTask, localRoot string, client *http.Client, cfg mirrorHTTPConfig, expected map[string]string) (int64, int64, int64, int64, error) { func (m *MirrorManager) applyMirror(ctx context.Context, task models.RPMMirrorTask, localRoot string, client *http.Client, cfg mirrorHTTPConfig, expected map[string]mirrorChecksum) (int64, int64, int64, int64, int64, error) {
var local map[string]bool var local map[string]bool
var total int64 var total int64
var done int64 var done int64
var failed int64 var failed int64
var deleted int64 var deleted int64
var changed int64
var path string var path string
var checksum string var checksum mirrorChecksum
var fullPath string var fullPath string
var localSum string var localSum string
var needDownload bool var needDownload bool
var err error var err error
local, err = listLocalRPMs(localRoot) local, err = listLocalRPMs(localRoot)
if err != nil { if err != nil {
return 0, 0, 0, 0, err return 0, 0, 0, 0, 0, err
} }
total = int64(len(expected)) total = int64(len(expected))
_ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "apply", total, 0, 0, 0) _ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "apply", total, 0, 0, 0)
for path = range local { for path = range local {
if expected[path] != "" { select {
case <-ctx.Done():
return total, done, failed, deleted, changed, ctx.Err()
default:
}
if expected[path].Value != "" {
continue continue
} }
if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_DEBUG, "delete local stale repo=%s path=%s file=%s", task.RepoID, task.MirrorPath, path)
}
err = os.Remove(filepath.Join(localRoot, filepath.FromSlash(path))) err = os.Remove(filepath.Join(localRoot, filepath.FromSlash(path)))
if err == nil || os.IsNotExist(err) { if err == nil || os.IsNotExist(err) {
deleted = deleted + 1 deleted = deleted + 1
changed = changed + 1
} else {
if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_WARN, "delete local stale failed repo=%s path=%s file=%s err=%v", task.RepoID, task.MirrorPath, path, err)
}
} }
} }
for path, checksum = range expected { for path, checksum = range expected {
select {
case <-ctx.Done():
return total, done, failed, deleted, changed, ctx.Err()
default:
}
fullPath = filepath.Join(localRoot, filepath.FromSlash(path)) fullPath = filepath.Join(localRoot, filepath.FromSlash(path))
needDownload = true needDownload = true
_, err = os.Stat(fullPath) _, err = os.Stat(fullPath)
if err == nil { if err == nil {
localSum, err = sha256HexFile(fullPath) localSum, err = fileHexByAlgo(fullPath, checksum.Algo)
if err == nil && (checksum == "" || strings.EqualFold(localSum, checksum)) { if err == nil && (checksum.Value == "" || strings.EqualFold(localSum, checksum.Value)) {
needDownload = false needDownload = false
} }
} }
if needDownload { if needDownload {
err = mirrorDownload(client, cfg, path, fullPath, checksum) if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_DEBUG, "download start repo=%s path=%s file=%s checksum_type=%s checksum=%s", task.RepoID, task.MirrorPath, path, checksum.Algo, checksum.Value)
}
err = mirrorDownload(ctx, client, cfg, path, fullPath, checksum.Algo, checksum.Value)
if err != nil { if err != nil {
failed = failed + 1 failed = failed + 1
if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_WARN, "download failed repo=%s path=%s file=%s err=%v", task.RepoID, task.MirrorPath, path, err)
}
_ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "apply", total, done, failed, deleted) _ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "apply", total, done, failed, deleted)
continue continue
} }
changed = changed + 1
if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_DEBUG, "download done repo=%s path=%s file=%s", task.RepoID, task.MirrorPath, path)
}
} else {
if m.logger != nil {
m.logger.Write("rpm-mirror", util.LOG_DEBUG, "download skip repo=%s path=%s file=%s reason=up-to-date", task.RepoID, task.MirrorPath, path)
}
} }
done = done + 1 done = done + 1
_ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "apply", total, done, failed, deleted) _ = m.store.UpdateRPMMirrorTaskProgress(task.RepoID, task.MirrorPath, "apply", total, done, failed, deleted)
} }
if failed > 0 { if failed > 0 {
return total, done, failed, deleted, errors.New("some mirror files failed to sync") return total, done, failed, deleted, changed, errors.New("some mirror files failed to sync")
} }
return total, done, failed, deleted, nil return total, done, failed, deleted, changed, nil
} }
func buildMirrorHTTPConfig(task models.RPMMirrorTask) (mirrorHTTPConfig, error) { func buildMirrorHTTPConfig(task models.RPMMirrorTask) (mirrorHTTPConfig, error) {
@@ -391,14 +501,14 @@ func effectiveServerName(cfg mirrorHTTPConfig) string {
return cfg.DefaultServer return cfg.DefaultServer
} }
func mirrorFetch(client *http.Client, cfg mirrorHTTPConfig, rel string) ([]byte, error) { func mirrorFetch(ctx context.Context, client *http.Client, cfg mirrorHTTPConfig, rel string) ([]byte, error) {
var fullURL string var fullURL string
var req *http.Request var req *http.Request
var res *http.Response var res *http.Response
var body []byte var body []byte
var err error var err error
fullURL = joinRemoteURL(cfg.BaseURL, rel) fullURL = joinRemoteURL(cfg.BaseURL, rel)
req, err = http.NewRequest(http.MethodGet, fullURL, nil) req, err = http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -418,7 +528,7 @@ func mirrorFetch(client *http.Client, cfg mirrorHTTPConfig, rel string) ([]byte,
return body, nil return body, nil
} }
func mirrorDownload(client *http.Client, cfg mirrorHTTPConfig, rel string, dstPath string, checksum string) error { func mirrorDownload(ctx context.Context, client *http.Client, cfg mirrorHTTPConfig, rel string, dstPath string, checksumType string, checksum string) error {
var fullURL string var fullURL string
var req *http.Request var req *http.Request
var res *http.Response var res *http.Response
@@ -426,13 +536,16 @@ func mirrorDownload(client *http.Client, cfg mirrorHTTPConfig, rel string, dstPa
var out *os.File var out *os.File
var hash hashWriter var hash hashWriter
var copied int64 var copied int64
var actualSum string
var contentType string
var finalURL string
var err error var err error
err = os.MkdirAll(filepath.Dir(dstPath), 0o755) err = os.MkdirAll(filepath.Dir(dstPath), 0o755)
if err != nil { if err != nil {
return err return err
} }
fullURL = joinRemoteURL(cfg.BaseURL, rel) fullURL = joinRemoteURL(cfg.BaseURL, rel)
req, err = http.NewRequest(http.MethodGet, fullURL, nil) req, err = http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -445,13 +558,22 @@ func mirrorDownload(client *http.Client, cfg mirrorHTTPConfig, rel string, dstPa
if res.StatusCode < 200 || res.StatusCode >= 300 { if res.StatusCode < 200 || res.StatusCode >= 300 {
return errors.New("upstream request failed: " + res.Status) return errors.New("upstream request failed: " + res.Status)
} }
contentType = strings.TrimSpace(res.Header.Get("Content-Type"))
finalURL = ""
if res.Request != nil && res.Request.URL != nil {
finalURL = res.Request.URL.String()
}
tempPath = dstPath + ".mirror.tmp" tempPath = dstPath + ".mirror.tmp"
out, err = os.Create(tempPath) out, err = os.Create(tempPath)
if err != nil { if err != nil {
return err return err
} }
defer out.Close() defer out.Close()
hash = newHashWriter() hash, err = newHashWriter(checksumType)
if err != nil {
_ = os.Remove(tempPath)
return err
}
copied, err = io.Copy(io.MultiWriter(out, hash), res.Body) copied, err = io.Copy(io.MultiWriter(out, hash), res.Body)
_ = copied _ = copied
if err != nil { if err != nil {
@@ -464,9 +586,17 @@ func mirrorDownload(client *http.Client, cfg mirrorHTTPConfig, rel string, dstPa
return err return err
} }
if strings.TrimSpace(checksum) != "" { if strings.TrimSpace(checksum) != "" {
if !strings.EqualFold(hash.Sum(), strings.TrimSpace(checksum)) { actualSum = hash.Sum()
if !strings.EqualFold(actualSum, strings.TrimSpace(checksum)) {
_ = os.Remove(tempPath) _ = os.Remove(tempPath)
return errors.New("download checksum mismatch for " + rel) return errors.New(
"download checksum mismatch for " + rel +
" type=" + normalizeChecksumAlgo(checksumType) +
" expected=" + strings.TrimSpace(checksum) +
" actual=" + actualSum +
" bytes=" + int64ToString(copied) +
" content_type=" + contentType +
" url=" + finalURL)
} }
} }
err = os.Rename(tempPath, dstPath) err = os.Rename(tempPath, dstPath)
@@ -478,9 +608,26 @@ func mirrorDownload(client *http.Client, cfg mirrorHTTPConfig, rel string, dstPa
} }
func joinRemoteURL(base string, rel string) string { func joinRemoteURL(base string, rel string) string {
var baseURL *url.URL
var relURL *url.URL
var cleanRel string var cleanRel string
cleanRel = strings.TrimLeft(strings.ReplaceAll(rel, "\\", "/"), "/") var err error
return strings.TrimRight(base, "/") + "/" + cleanRel cleanRel = strings.ReplaceAll(rel, "\\", "/")
baseURL, err = url.Parse(strings.TrimSpace(base))
if err != nil || baseURL == nil {
cleanRel = strings.TrimLeft(cleanRel, "/")
return strings.TrimRight(base, "/") + "/" + cleanRel
}
// Treat base as a directory root for repository-relative href resolution.
if !strings.HasSuffix(baseURL.Path, "/") {
baseURL.Path = baseURL.Path + "/"
}
relURL, err = url.Parse(strings.TrimSpace(cleanRel))
if err != nil || relURL == nil {
cleanRel = strings.TrimLeft(cleanRel, "/")
return strings.TrimRight(base, "/") + "/" + cleanRel
}
return baseURL.ResolveReference(relURL).String()
} }
func parseRepomdPrimaryHref(data []byte) (string, error) { func parseRepomdPrimaryHref(data []byte) (string, error) {
@@ -503,18 +650,24 @@ func parseRepomdPrimaryHref(data []byte) (string, error) {
return "", errors.New("primary metadata not found in repomd") return "", errors.New("primary metadata not found in repomd")
} }
func parsePrimaryPackages(data []byte) (map[string]string, error) { func parsePrimaryPackages(data []byte) (map[string]mirrorChecksum, int, error) {
var doc primaryDoc var doc primaryDoc
var out map[string]string var out map[string]mirrorChecksum
var i int var i int
var path string var path string
var checksum string var checksum string
var checksumType string
var fileTime int64
var buildTime int64
var existing mirrorChecksum
var ok bool
var duplicates int
var err error var err error
err = xml.Unmarshal(data, &doc) err = xml.Unmarshal(data, &doc)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
out = make(map[string]string) out = make(map[string]mirrorChecksum)
for i = 0; i < len(doc.Packages); i++ { for i = 0; i < len(doc.Packages); i++ {
path = strings.TrimSpace(doc.Packages[i].Location.Href) path = strings.TrimSpace(doc.Packages[i].Location.Href)
if path == "" { if path == "" {
@@ -524,9 +677,23 @@ func parsePrimaryPackages(data []byte) (map[string]string, error) {
continue continue
} }
checksum = strings.TrimSpace(doc.Packages[i].Checksum.Value) checksum = strings.TrimSpace(doc.Packages[i].Checksum.Value)
out[path] = strings.ToLower(checksum) checksumType = strings.TrimSpace(doc.Packages[i].Checksum.Type)
fileTime = parseTimeAttr(doc.Packages[i].Time.File)
buildTime = parseTimeAttr(doc.Packages[i].Time.Build)
if existing, ok = out[path]; ok {
duplicates = duplicates + 1
if !shouldReplaceDuplicate(existing, buildTime, fileTime, checksum) {
continue
}
}
out[path] = mirrorChecksum{
Algo: normalizeChecksumAlgo(checksumType),
Value: strings.ToLower(checksum),
BuildTime: buildTime,
FileTime: fileTime,
}
} }
return out, nil return out, duplicates, nil
} }
func listLocalRPMs(root string) (map[string]bool, error) { func listLocalRPMs(root string) (map[string]bool, error) {
@@ -569,7 +736,7 @@ func sha256HexBytes(data []byte) string {
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
func sha256HexFile(path string) (string, error) { func fileHexByAlgo(path string, algo string) (string, error) {
var file *os.File var file *os.File
var hash hashWriter var hash hashWriter
var copied int64 var copied int64
@@ -579,7 +746,10 @@ func sha256HexFile(path string) (string, error) {
return "", err return "", err
} }
defer file.Close() defer file.Close()
hash = newHashWriter() hash, err = newHashWriter(algo)
if err != nil {
return "", err
}
copied, err = io.Copy(hash, file) copied, err = io.Copy(hash, file)
_ = copied _ = copied
if err != nil { if err != nil {
@@ -615,10 +785,29 @@ type shaWriter struct {
h hash.Hash h hash.Hash
} }
func newHashWriter() hashWriter { func newHashWriter(algo string) (hashWriter, error) {
var w *shaWriter var w *shaWriter
w = &shaWriter{h: sha256.New()} var normalized string
return w var h hash.Hash
normalized = normalizeChecksumAlgo(algo)
switch normalized {
case "", "sha256":
h = sha256.New()
case "sha", "sha1":
h = sha1.New()
case "sha224":
h = sha256.New224()
case "sha384":
h = sha512.New384()
case "sha512":
h = sha512.New()
case "md5":
h = md5.New()
default:
return nil, errors.New("unsupported checksum type: " + normalized)
}
w = &shaWriter{h: h}
return w, nil
} }
func (w *shaWriter) Write(p []byte) (int, error) { func (w *shaWriter) Write(p []byte) (int, error) {
@@ -630,3 +819,90 @@ func (w *shaWriter) Sum() string {
raw = w.h.Sum(nil) raw = w.h.Sum(nil)
return hex.EncodeToString(raw) return hex.EncodeToString(raw)
} }
func normalizeChecksumAlgo(algo string) string {
var out string
out = strings.ToLower(strings.TrimSpace(algo))
out = strings.ReplaceAll(out, "-", "")
out = strings.ReplaceAll(out, "_", "")
if out == "sha1" {
return "sha1"
}
if out == "sha" {
return "sha"
}
if out == "sha224" {
return "sha224"
}
if out == "sha256" {
return "sha256"
}
if out == "sha384" {
return "sha384"
}
if out == "sha512" {
return "sha512"
}
if out == "md5" {
return "md5"
}
return out
}
func int64ToString(v int64) string {
return strconv.FormatInt(v, 10)
}
func mirrorTaskKey(repoID string, path string) string {
return repoID + "\x00" + path
}
func ensureRepodata(task models.RPMMirrorTask, localRoot string, meta *MetaManager, logger *util.Logger) {
var repomdPath string
var statErr error
repomdPath = filepath.Join(localRoot, "repodata", "repomd.xml")
_, statErr = os.Stat(repomdPath)
if statErr == nil {
return
}
if logger != nil {
logger.Write("rpm-mirror", util.LOG_INFO, "repodata schedule repo=%s path=%s reason=missing repomd=%s", task.RepoID, task.MirrorPath, repomdPath)
}
meta.Schedule(localRoot)
}
func parseTimeAttr(value string) int64 {
var trimmed string
var parsed int64
var err error
trimmed = strings.TrimSpace(value)
if trimmed == "" {
return 0
}
parsed, err = strconv.ParseInt(trimmed, 10, 64)
if err != nil {
return 0
}
return parsed
}
func shouldReplaceDuplicate(existing mirrorChecksum, newBuildTime int64, newFileTime int64, newChecksum string) bool {
var existingChecksum string
if newBuildTime > existing.BuildTime {
return true
}
if newBuildTime < existing.BuildTime {
return false
}
if newFileTime > existing.FileTime {
return true
}
if newFileTime < existing.FileTime {
return false
}
existingChecksum = strings.TrimSpace(existing.Value)
if existingChecksum == "" && strings.TrimSpace(newChecksum) != "" {
return true
}
return false
}

View File

@@ -1,15 +1,11 @@
package rpm package rpm
import "bufio"
import "bytes"
import "errors"
import "io/fs" import "io/fs"
import "os/exec"
import "path/filepath" import "path/filepath"
import "sort" import "sort"
import "strconv"
import "strings" import "strings"
import "sync"
import repokit "repokit"
type PackageSummary struct { type PackageSummary struct {
Filename string `json:"filename"` Filename string `json:"filename"`
@@ -30,35 +26,23 @@ type PackageDetail struct {
Files []string `json:"files"` Files []string `json:"files"`
Requires []string `json:"requires"` Requires []string `json:"requires"`
Provides []string `json:"provides"` Provides []string `json:"provides"`
Changelogs []PackageChangeLog `json:"changelogs"`
} }
var rpmPath string type PackageChangeLog struct {
var rpmOnce sync.Once Author string `json:"author"`
var rpmErr error Date int64 `json:"date"`
Text string `json:"text"`
func ensureRPM() error {
rpmOnce.Do(func() {
var path string
path, rpmErr = exec.LookPath("rpm")
if rpmErr != nil {
return
}
rpmPath = path
})
return rpmErr
} }
func ListPackages(repoPath string) ([]PackageSummary, error) { func ListPackages(repoPath string) ([]PackageSummary, error) {
var err error
var packages []PackageSummary var packages []PackageSummary
var walkErr error var walkErr error
err = ensureRPM() var err error
if err != nil {
return nil, err
}
walkErr = filepath.WalkDir(repoPath, func(path string, entry fs.DirEntry, entryErr error) error { walkErr = filepath.WalkDir(repoPath, func(path string, entry fs.DirEntry, entryErr error) error {
var lower string var lower string
var rel string var rel string
var pkg *repokit.RpmPackage
var summary PackageSummary var summary PackageSummary
if entryErr != nil { if entryErr != nil {
return entryErr return entryErr
@@ -74,11 +58,11 @@ func ListPackages(repoPath string) ([]PackageSummary, error) {
if err != nil { if err != nil {
return err return err
} }
summary, err = querySummary(path) pkg, err = repokit.RpmPackageFromRpmBase(path, 0)
if err != nil { if err != nil {
return nil return nil
} }
summary.Filename = filepath.ToSlash(rel) summary = packageSummaryFromRepokit(pkg, filepath.ToSlash(rel))
packages = append(packages, summary) packages = append(packages, summary)
return nil return nil
}) })
@@ -86,137 +70,135 @@ func ListPackages(repoPath string) ([]PackageSummary, error) {
return nil, walkErr return nil, walkErr
} }
sort.Slice(packages, func(i int, j int) bool { sort.Slice(packages, func(i int, j int) bool {
if packages[i].Name == packages[j].Name {
return packages[i].Filename < packages[j].Filename
}
return packages[i].Name < packages[j].Name return packages[i].Name < packages[j].Name
}) })
return packages, nil return packages, nil
} }
func GetPackageDetail(repoPath string, filename string) (PackageDetail, error) { func GetPackageDetail(repoPath string, filename string) (PackageDetail, error) {
var err error
var detail PackageDetail var detail PackageDetail
var fullPath string var fullPath string
var data []string var pkg *repokit.RpmPackage
var fileList []string var err error
var requires []string
var provides []string
var buildTime int64
var size int64
err = ensureRPM()
if err != nil {
return detail, err
}
fullPath = filepath.Join(repoPath, filepath.FromSlash(filename)) fullPath = filepath.Join(repoPath, filepath.FromSlash(filename))
data, err = queryFields(fullPath, "%{NAME}\n%{VERSION}\n%{RELEASE}\n%{ARCH}\n%{SUMMARY}\n%{DESCRIPTION}\n%{LICENSE}\n%{URL}\n%{BUILDTIME}\n%{SIZE}\n") pkg, err = repokit.RpmPackageFromRpmBase(fullPath, 256)
if err != nil { if err != nil {
return detail, err return detail, err
} }
if len(data) < 10 { detail = packageDetailFromRepokit(pkg, filename)
return detail, errors.New("rpm query returned incomplete metadata")
}
buildTime, _ = strconv.ParseInt(strings.TrimSpace(data[8]), 10, 64)
size, _ = strconv.ParseInt(strings.TrimSpace(data[9]), 10, 64)
fileList, _ = queryList(fullPath)
requires, _ = queryLines(fullPath, "--requires")
provides, _ = queryLines(fullPath, "--provides")
detail = PackageDetail{
PackageSummary: PackageSummary{
Filename: filename,
Name: strings.TrimSpace(data[0]),
Version: strings.TrimSpace(data[1]),
Release: strings.TrimSpace(data[2]),
Arch: strings.TrimSpace(data[3]),
Summary: strings.TrimSpace(data[4]),
},
Description: strings.TrimSpace(data[5]),
License: strings.TrimSpace(data[6]),
URL: strings.TrimSpace(data[7]),
BuildTime: buildTime,
Size: size,
Files: fileList,
Requires: requires,
Provides: provides,
}
return detail, nil return detail, nil
} }
func querySummary(path string) (PackageSummary, error) { func packageSummaryFromRepokit(pkg *repokit.RpmPackage, filename string) PackageSummary {
var fields []string
var err error
var summary PackageSummary var summary PackageSummary
fields, err = queryFields(path, "%{NAME}\n%{VERSION}\n%{RELEASE}\n%{ARCH}\n%{SUMMARY}\n") summary = PackageSummary{
if err != nil { Filename: filename,
return summary, err Name: strings.TrimSpace(pkg.Name),
Version: strings.TrimSpace(pkg.Version),
Release: strings.TrimSpace(pkg.Release),
Arch: strings.TrimSpace(pkg.Arch),
Summary: strings.TrimSpace(pkg.Summary),
} }
if len(fields) < 5 { return summary
return summary, errors.New("rpm query returned incomplete metadata")
}
summary.Name = strings.TrimSpace(fields[0])
summary.Version = strings.TrimSpace(fields[1])
summary.Release = strings.TrimSpace(fields[2])
summary.Arch = strings.TrimSpace(fields[3])
summary.Summary = strings.TrimSpace(fields[4])
return summary, nil
} }
func queryFields(path string, format string) ([]string, error) { func packageDetailFromRepokit(pkg *repokit.RpmPackage, filename string) PackageDetail {
var output []byte
var err error
var list []string
output, err = runRPM(path, "-qp", "--qf", format)
if err != nil {
return nil, err
}
list = strings.Split(strings.TrimSuffix(string(output), "\n"), "\n")
return list, nil
}
func queryList(path string) ([]string, error) {
var output []byte
var err error
var scanner *bufio.Scanner
var files []string var files []string
output, err = runRPM(path, "-qlp") var file repokit.RpmPackageFile
if err != nil { var changelogs []PackageChangeLog
return nil, err var changelog repokit.RpmChangelogEntry
var detail PackageDetail
files = make([]string, 0, len(pkg.Files))
for _, file = range pkg.Files {
if file.FullPath == "" {
continue
}
files = append(files, file.FullPath)
} }
scanner = bufio.NewScanner(bytes.NewReader(output)) changelogs = make([]PackageChangeLog, 0, len(pkg.Changelogs))
for scanner.Scan() { for _, changelog = range pkg.Changelogs {
files = append(files, scanner.Text()) changelogs = append(changelogs, PackageChangeLog{
Author: strings.TrimSpace(changelog.Author),
Date: changelog.Date,
Text: strings.TrimSpace(changelog.Changelog),
})
} }
return files, nil sort.SliceStable(changelogs, func(i int, j int) bool {
return changelogs[i].Date > changelogs[j].Date
})
sort.Strings(files)
detail = PackageDetail{
PackageSummary: packageSummaryFromRepokit(pkg, filename),
Description: strings.TrimSpace(pkg.Description),
License: strings.TrimSpace(pkg.RpmLicense),
URL: strings.TrimSpace(pkg.Url),
BuildTime: pkg.TimeBuild,
Size: pkg.SizePackage,
Files: files,
Requires: dependencyListToStrings(pkg.Requires),
Provides: dependencyListToStrings(pkg.Provides),
Changelogs: changelogs,
}
return detail
} }
func queryLines(path string, flag string) ([]string, error) { func dependencyListToStrings(deps []repokit.RpmDependency) []string {
var output []byte
var err error
var scanner *bufio.Scanner
var lines []string var lines []string
output, err = runRPM(path, "-qp", flag) var dep repokit.RpmDependency
if err != nil { lines = make([]string, 0, len(deps))
return nil, err for _, dep = range deps {
lines = append(lines, dependencyToString(dep))
} }
scanner = bufio.NewScanner(bytes.NewReader(output)) return lines
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, nil
} }
func runRPM(path string, args ...string) ([]byte, error) { func dependencyToString(dep repokit.RpmDependency) string {
var err error var op string
var cmd *exec.Cmd var evr string
var output []byte var line string
var full []string op = normalizeDependencyOp(dep.Flags)
err = ensureRPM() evr = dependencyEVR(dep)
if err != nil { line = dep.Name
return nil, err if op == "" || evr == "" {
return line
} }
full = append([]string{}, args...) line = line + " " + op + " " + evr
full = append(full, path) return line
cmd = exec.Command(rpmPath, full...) }
output, err = cmd.Output()
if err != nil { func normalizeDependencyOp(flag string) string {
return nil, err switch strings.ToUpper(strings.TrimSpace(flag)) {
} case "LT":
return output, nil return "<"
case "GT":
return ">"
case "EQ":
return "="
case "LE":
return "<="
case "GE":
return ">="
default:
return strings.TrimSpace(flag)
}
}
func dependencyEVR(dep repokit.RpmDependency) string {
var value string
var version string
version = strings.TrimSpace(dep.Version)
if version == "" {
return ""
}
value = version
if strings.TrimSpace(dep.Release) != "" {
value = value + "-" + strings.TrimSpace(dep.Release)
}
if strings.TrimSpace(dep.Epoch) != "" && strings.TrimSpace(dep.Epoch) != "0" {
value = strings.TrimSpace(dep.Epoch) + ":" + value
}
return value
} }

View File

@@ -1,7 +1,8 @@
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL, display_name TEXT NOT NULL,
email TEXT NOT NULL, email TEXT NOT NULL,
@@ -9,12 +10,13 @@ CREATE TABLE IF NOT EXISTS users (
is_admin INTEGER NOT NULL DEFAULT 0, is_admin INTEGER NOT NULL DEFAULT 0,
auth_source TEXT NOT NULL DEFAULT 'db', auth_source TEXT NOT NULL DEFAULT 'db',
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL updated_at TIMESTAMP NOT NULL,
disabled INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT NOT NULL, user_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE, token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
@@ -22,19 +24,25 @@ CREATE TABLE IF NOT EXISTS sessions (
); );
CREATE TABLE IF NOT EXISTS projects ( CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
slug TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
created_by TEXT NOT NULL, home_page TEXT NOT NULL DEFAULT 'info',
created_by INTEGER NOT NULL,
updated_by INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL,
created_at_unix INTEGER NOT NULL DEFAULT 0,
updated_at_unix INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (created_by) REFERENCES users(id) FOREIGN KEY (created_by) REFERENCES users(id)
FOREIGN KEY (updated_by) REFERENCES users(id)
); );
CREATE TABLE IF NOT EXISTS project_members ( CREATE TABLE IF NOT EXISTS project_members (
project_id TEXT NOT NULL, project_id INTEGER NOT NULL,
user_id TEXT NOT NULL, user_id INTEGER NOT NULL,
role TEXT NOT NULL, role TEXT NOT NULL,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
PRIMARY KEY (project_id, user_id), PRIMARY KEY (project_id, user_id),
@@ -43,24 +51,27 @@ CREATE TABLE IF NOT EXISTS project_members (
); );
CREATE TABLE IF NOT EXISTS repos ( CREATE TABLE IF NOT EXISTS repos (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL, public_id TEXT NOT NULL UNIQUE,
project_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'git',
path TEXT NOT NULL, path TEXT NOT NULL,
created_by TEXT NOT NULL, created_by INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) FOREIGN KEY (created_by) REFERENCES users(id)
); );
CREATE TABLE IF NOT EXISTS issues ( CREATE TABLE IF NOT EXISTS issues (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL, public_id TEXT NOT NULL UNIQUE,
project_id INTEGER NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
body TEXT NOT NULL, body TEXT NOT NULL,
status TEXT NOT NULL, status TEXT NOT NULL,
created_by TEXT NOT NULL, created_by INTEGER NOT NULL,
assignee_id TEXT, assignee_id INTEGER,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
@@ -69,22 +80,24 @@ CREATE TABLE IF NOT EXISTS issues (
); );
CREATE TABLE IF NOT EXISTS issue_comments ( CREATE TABLE IF NOT EXISTS issue_comments (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id TEXT NOT NULL, public_id TEXT NOT NULL UNIQUE,
issue_id INTEGER NOT NULL,
body TEXT NOT NULL, body TEXT NOT NULL,
created_by TEXT NOT NULL, created_by INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE, FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) FOREIGN KEY (created_by) REFERENCES users(id)
); );
CREATE TABLE IF NOT EXISTS wiki_pages ( CREATE TABLE IF NOT EXISTS wiki_pages (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL, public_id TEXT NOT NULL UNIQUE,
project_id INTEGER NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
slug TEXT NOT NULL, slug TEXT NOT NULL,
body TEXT NOT NULL, body TEXT NOT NULL,
created_by TEXT NOT NULL, created_by INTEGER NOT NULL,
updated_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL,
UNIQUE (project_id, slug), UNIQUE (project_id, slug),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
@@ -92,20 +105,203 @@ CREATE TABLE IF NOT EXISTS wiki_pages (
); );
CREATE TABLE IF NOT EXISTS uploads ( CREATE TABLE IF NOT EXISTS uploads (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL, public_id TEXT NOT NULL UNIQUE,
project_id INTEGER NOT NULL,
filename TEXT NOT NULL, filename TEXT NOT NULL,
content_type TEXT NOT NULL, content_type TEXT NOT NULL,
size INTEGER NOT NULL, size INTEGER NOT NULL,
storage_path TEXT NOT NULL, storage_path TEXT NOT NULL,
created_by TEXT NOT NULL, created_by INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) FOREIGN KEY (created_by) REFERENCES users(id)
); );
CREATE TABLE IF NOT EXISTS project_repos (
project_id INTEGER NOT NULL,
repo_id INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (project_id, repo_id),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_prefix TEXT NOT NULL,
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL DEFAULT 0,
expires_at INTEGER NOT NULL DEFAULT 0,
disabled INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS pki_cas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL UNIQUE,
parent_ca_id INTEGER,
is_root INTEGER NOT NULL DEFAULT 0,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
serial_counter INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY(parent_ca_id) REFERENCES pki_cas(id)
);
CREATE TABLE IF NOT EXISTS pki_certs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
ca_id INTEGER,
serial_hex TEXT NOT NULL,
common_name TEXT NOT NULL,
san_dns TEXT NOT NULL DEFAULT '',
san_ips TEXT NOT NULL DEFAULT '',
is_ca INTEGER NOT NULL DEFAULT 0,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
not_before INTEGER NOT NULL,
not_after INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
revoked_at INTEGER NOT NULL DEFAULT 0,
revocation_reason TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
FOREIGN KEY(ca_id) REFERENCES pki_cas(id) ON DELETE CASCADE,
UNIQUE(ca_id, serial_hex)
);
CREATE TABLE IF NOT EXISTS tls_listeners (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL UNIQUE,
enabled INTEGER NOT NULL DEFAULT 1,
http_addrs TEXT NOT NULL DEFAULT '',
https_addrs TEXT NOT NULL DEFAULT '',
tls_server_cert_source TEXT NOT NULL DEFAULT 'files',
tls_cert_file TEXT NOT NULL DEFAULT '',
tls_key_file TEXT NOT NULL DEFAULT '',
tls_pki_server_cert_id TEXT NOT NULL DEFAULT '',
tls_client_auth TEXT NOT NULL DEFAULT 'none',
tls_client_ca_file TEXT NOT NULL DEFAULT '',
tls_pki_client_ca_id TEXT NOT NULL DEFAULT '',
tls_min_version TEXT NOT NULL DEFAULT '1.2',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
auth_policy TEXT NOT NULL DEFAULT 'default',
apply_policy_api INTEGER NOT NULL DEFAULT 0,
apply_policy_git INTEGER NOT NULL DEFAULT 0,
apply_policy_rpm INTEGER NOT NULL DEFAULT 0,
apply_policy_v2 INTEGER NOT NULL DEFAULT 0,
client_cert_allowlist TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS service_principals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
disabled INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS cert_principal_bindings (
fingerprint TEXT PRIMARY KEY,
principal_id INTEGER NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (principal_id) REFERENCES service_principals(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS principal_project_roles (
principal_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
role TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (principal_id, project_id),
FOREIGN KEY (principal_id) REFERENCES service_principals(id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS rpm_repo_dirs (
repo_id INTEGER NOT NULL,
path TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'local',
allow_delete INTEGER NOT NULL DEFAULT 0,
remote_url TEXT NOT NULL DEFAULT '',
connect_host TEXT NOT NULL DEFAULT '',
host_header TEXT NOT NULL DEFAULT '',
tls_server_name TEXT NOT NULL DEFAULT '',
tls_insecure_skip_verify INTEGER NOT NULL DEFAULT 0,
sync_interval_sec INTEGER NOT NULL DEFAULT 300,
sync_enabled INTEGER NOT NULL DEFAULT 1,
dirty INTEGER NOT NULL DEFAULT 1,
next_sync_at INTEGER NOT NULL DEFAULT 0,
sync_running INTEGER NOT NULL DEFAULT 0,
sync_status TEXT NOT NULL DEFAULT 'idle',
sync_error TEXT NOT NULL DEFAULT '',
sync_step TEXT NOT NULL DEFAULT '',
sync_total INTEGER NOT NULL DEFAULT 0,
sync_done INTEGER NOT NULL DEFAULT 0,
sync_failed INTEGER NOT NULL DEFAULT 0,
sync_deleted INTEGER NOT NULL DEFAULT 0,
last_sync_started_at INTEGER NOT NULL DEFAULT 0,
last_sync_finished_at INTEGER NOT NULL DEFAULT 0,
last_sync_success_at INTEGER NOT NULL DEFAULT 0,
last_synced_revision TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (repo_id, path),
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS rpm_mirror_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
repo_id INTEGER NOT NULL,
path TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'running',
step TEXT NOT NULL DEFAULT '',
total INTEGER NOT NULL DEFAULT 0,
done INTEGER NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
deleted INTEGER NOT NULL DEFAULT 0,
revision TEXT NOT NULL DEFAULT '',
error TEXT NOT NULL DEFAULT '',
FOREIGN KEY(repo_id, path) REFERENCES rpm_repo_dirs(repo_id, path) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_users_disabled ON users(disabled);
CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name); CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
CREATE INDEX IF NOT EXISTS idx_repos_project ON repos(project_id); CREATE INDEX IF NOT EXISTS idx_repos_project ON repos(project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_repos_project_name_type ON repos(project_id, name, type);
CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id); CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id);
CREATE INDEX IF NOT EXISTS idx_wiki_project ON wiki_pages(project_id); CREATE INDEX IF NOT EXISTS idx_wiki_project ON wiki_pages(project_id);
CREATE INDEX IF NOT EXISTS idx_uploads_project ON uploads(project_id); CREATE INDEX IF NOT EXISTS idx_uploads_project ON uploads(project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(token_hash);
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_expires_at ON api_keys(expires_at);
CREATE INDEX IF NOT EXISTS idx_api_keys_disabled ON api_keys(disabled);
CREATE INDEX IF NOT EXISTS idx_project_repos_project ON project_repos(project_id);
CREATE INDEX IF NOT EXISTS idx_project_repos_repo ON project_repos(repo_id);
CREATE INDEX IF NOT EXISTS idx_pki_cas_parent ON pki_cas(parent_ca_id);
CREATE INDEX IF NOT EXISTS idx_pki_certs_ca ON pki_certs(ca_id);
CREATE INDEX IF NOT EXISTS idx_cert_principal_bindings_principal_id ON cert_principal_bindings(principal_id);
CREATE INDEX IF NOT EXISTS idx_principal_project_roles_project_id ON principal_project_roles(project_id);

View File

@@ -1,13 +0,0 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS project_repos (
project_id TEXT NOT NULL,
repo_id TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (project_id, repo_id),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_project_repos_project ON project_repos(project_id);
CREATE INDEX IF NOT EXISTS idx_project_repos_repo ON project_repos(repo_id);

View File

@@ -1,10 +0,0 @@
ALTER TABLE projects ADD COLUMN updated_by TEXT NOT NULL DEFAULT '';
ALTER TABLE projects ADD COLUMN created_at_unix INTEGER NOT NULL DEFAULT 0;
ALTER TABLE projects ADD COLUMN updated_at_unix INTEGER NOT NULL DEFAULT 0;
UPDATE projects
SET
updated_by = created_by,
created_at_unix = COALESCE(CAST(strftime('%s', created_at) AS INTEGER), 0),
updated_at_unix = COALESCE(CAST(strftime('%s', updated_at) AS INTEGER), 0)
WHERE created_at_unix = 0 OR updated_at_unix = 0 OR updated_by = '';

View File

@@ -1,3 +0,0 @@
PRAGMA foreign_keys = ON;
ALTER TABLE repos ADD COLUMN type TEXT NOT NULL DEFAULT 'git';

View File

@@ -1,3 +0,0 @@
PRAGMA foreign_keys = ON;
CREATE UNIQUE INDEX IF NOT EXISTS idx_repos_project_name_type ON repos(project_id, name, type);

View File

@@ -1,13 +0,0 @@
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_prefix TEXT NOT NULL,
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(token_hash);
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);

View File

@@ -1 +0,0 @@
ALTER TABLE projects ADD COLUMN home_page TEXT NOT NULL DEFAULT 'info';

View File

@@ -1,3 +0,0 @@
ALTER TABLE api_keys ADD COLUMN expires_at INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_api_keys_expires_at ON api_keys(expires_at);

View File

@@ -1,5 +0,0 @@
ALTER TABLE users ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0;
ALTER TABLE api_keys ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_users_disabled ON users(disabled);
CREATE INDEX IF NOT EXISTS idx_api_keys_disabled ON api_keys(disabled);

View File

@@ -1,5 +0,0 @@
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);

View File

@@ -1,33 +0,0 @@
CREATE TABLE IF NOT EXISTS pki_cas (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
parent_ca_id TEXT,
is_root INTEGER NOT NULL DEFAULT 0,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
serial_counter INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY(parent_ca_id) REFERENCES pki_cas(id)
);
CREATE TABLE IF NOT EXISTS pki_certs (
id TEXT PRIMARY KEY,
ca_id TEXT NOT NULL,
serial_hex TEXT NOT NULL,
common_name TEXT NOT NULL,
san_dns TEXT NOT NULL DEFAULT '',
san_ips TEXT NOT NULL DEFAULT '',
is_ca INTEGER NOT NULL DEFAULT 0,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
not_before INTEGER NOT NULL,
not_after INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
revoked_at INTEGER NOT NULL DEFAULT 0,
revocation_reason TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
FOREIGN KEY(ca_id) REFERENCES pki_cas(id) ON DELETE CASCADE,
UNIQUE(ca_id, serial_hex)
);

View File

@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS tls_listeners (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
enabled INTEGER NOT NULL DEFAULT 1,
http_addrs TEXT NOT NULL DEFAULT '',
https_addrs TEXT NOT NULL DEFAULT '',
tls_server_cert_source TEXT NOT NULL DEFAULT 'files',
tls_cert_file TEXT NOT NULL DEFAULT '',
tls_key_file TEXT NOT NULL DEFAULT '',
tls_pki_server_cert_id TEXT NOT NULL DEFAULT '',
tls_client_auth TEXT NOT NULL DEFAULT 'none',
tls_client_ca_file TEXT NOT NULL DEFAULT '',
tls_pki_client_ca_id TEXT NOT NULL DEFAULT '',
tls_min_version TEXT NOT NULL DEFAULT '1.2',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);

View File

@@ -1,6 +0,0 @@
ALTER TABLE tls_listeners ADD COLUMN auth_policy TEXT NOT NULL DEFAULT 'default';
ALTER TABLE tls_listeners ADD COLUMN apply_policy_api INTEGER NOT NULL DEFAULT 1;
ALTER TABLE tls_listeners ADD COLUMN apply_policy_git INTEGER NOT NULL DEFAULT 1;
ALTER TABLE tls_listeners ADD COLUMN apply_policy_rpm INTEGER NOT NULL DEFAULT 1;
ALTER TABLE tls_listeners ADD COLUMN apply_policy_v2 INTEGER NOT NULL DEFAULT 1;
ALTER TABLE tls_listeners ADD COLUMN client_cert_allowlist TEXT NOT NULL DEFAULT '';

View File

@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS service_principals (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
disabled INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS cert_principal_bindings (
fingerprint TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (principal_id) REFERENCES service_principals(id) ON DELETE CASCADE
);

View File

@@ -1,11 +0,0 @@
ALTER TABLE service_principals ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS principal_project_roles (
principal_id TEXT NOT NULL,
project_id TEXT NOT NULL,
role TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (principal_id, project_id),
FOREIGN KEY (principal_id) REFERENCES service_principals(id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);

View File

@@ -1,31 +0,0 @@
CREATE TABLE IF NOT EXISTS pki_certs_new (
id TEXT PRIMARY KEY,
ca_id TEXT,
serial_hex TEXT NOT NULL,
common_name TEXT NOT NULL,
san_dns TEXT NOT NULL DEFAULT '',
san_ips TEXT NOT NULL DEFAULT '',
is_ca INTEGER NOT NULL DEFAULT 0,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
not_before INTEGER NOT NULL,
not_after INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
revoked_at INTEGER NOT NULL DEFAULT 0,
revocation_reason TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
FOREIGN KEY(ca_id) REFERENCES pki_cas(id) ON DELETE CASCADE,
UNIQUE(ca_id, serial_hex)
);
INSERT INTO pki_certs_new (
id, ca_id, serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem,
not_before, not_after, status, revoked_at, revocation_reason, created_at
)
SELECT
id, ca_id, serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem,
not_before, not_after, status, revoked_at, revocation_reason, created_at
FROM pki_certs;
DROP TABLE pki_certs;
ALTER TABLE pki_certs_new RENAME TO pki_certs;

View File

@@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS rpm_repo_dirs (
repo_id TEXT NOT NULL,
path TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'local',
remote_url TEXT NOT NULL DEFAULT '',
connect_host TEXT NOT NULL DEFAULT '',
host_header TEXT NOT NULL DEFAULT '',
tls_server_name TEXT NOT NULL DEFAULT '',
tls_insecure_skip_verify INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (repo_id, path),
FOREIGN KEY(repo_id) REFERENCES repos(id) ON DELETE CASCADE
);

View File

@@ -1,19 +0,0 @@
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_interval_sec INTEGER NOT NULL DEFAULT 300;
ALTER TABLE rpm_repo_dirs ADD COLUMN dirty INTEGER NOT NULL DEFAULT 1;
ALTER TABLE rpm_repo_dirs ADD COLUMN next_sync_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_running INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_status TEXT NOT NULL DEFAULT 'idle';
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_error TEXT NOT NULL DEFAULT '';
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_step TEXT NOT NULL DEFAULT '';
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_total INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_done INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_failed INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_deleted INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN last_sync_started_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN last_sync_finished_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN last_sync_success_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE rpm_repo_dirs ADD COLUMN last_synced_revision TEXT NOT NULL DEFAULT '';
UPDATE rpm_repo_dirs
SET dirty = 1, next_sync_at = 0
WHERE mode = 'mirror';

View File

@@ -1,19 +0,0 @@
CREATE TABLE IF NOT EXISTS rpm_mirror_runs (
id TEXT PRIMARY KEY,
repo_id TEXT NOT NULL,
path TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'running',
step TEXT NOT NULL DEFAULT '',
total INTEGER NOT NULL DEFAULT 0,
done INTEGER NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
deleted INTEGER NOT NULL DEFAULT 0,
revision TEXT NOT NULL DEFAULT '',
error TEXT NOT NULL DEFAULT '',
FOREIGN KEY(repo_id, path) REFERENCES rpm_repo_dirs(repo_id, path) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_rpm_mirror_runs_repo_path_started
ON rpm_mirror_runs (repo_id, path, started_at DESC);

View File

@@ -1,5 +0,0 @@
ALTER TABLE rpm_repo_dirs ADD COLUMN sync_enabled INTEGER NOT NULL DEFAULT 1;
UPDATE rpm_repo_dirs
SET sync_enabled = 1
WHERE mode = 'mirror';

View File

@@ -65,6 +65,7 @@ export interface RpmPackageDetail extends RpmPackageSummary {
files: string[] files: string[]
requires: string[] requires: string[]
provides: string[] provides: string[]
changelogs: { author: string; date: number; text: string }[]
} }
export interface DockerTagInfo { export interface DockerTagInfo {
@@ -104,6 +105,7 @@ export interface RpmRepoDirConfig {
repo_id: string repo_id: string
path: string path: string
mode: 'local' | 'mirror' | '' mode: 'local' | 'mirror' | ''
allow_delete: boolean
remote_url: string remote_url: string
connect_host: string connect_host: string
host_header: string host_header: string
@@ -665,6 +667,7 @@ export const api = {
type: string, type: string,
parent?: string, parent?: string,
mode?: 'local' | 'mirror', mode?: 'local' | 'mirror',
allow_delete?: boolean,
remote_url?: string, remote_url?: string,
connect_host?: string, connect_host?: string,
host_header?: string, host_header?: string,
@@ -674,18 +677,19 @@ export const api = {
) => ) =>
request<{ status: string }>(`/api/repos/${repoId}/rpm/subdirs`, { request<{ status: string }>(`/api/repos/${repoId}/rpm/subdirs`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ name, type, parent, mode, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec }) body: JSON.stringify({ name, type, parent, mode, allow_delete, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec })
}), }),
getRpmSubdir: (repoId: string, path: string) => { getRpmSubdir: (repoId: string, path: string) => {
const params = new URLSearchParams() const params = new URLSearchParams()
params.set('path', path) params.set('path', path)
return request<RpmRepoDirConfig>(`/api/repos/${repoId}/rpm/subdir?${params.toString()}`) return request<RpmRepoDirConfig>(`/api/repos/${repoId}/rpm/subdir?${params.toString()}`)
}, },
renameRpmSubdir: ( updateRpmSubdir: (
repoId: string, repoId: string,
path: string, path: string,
name: string, name?: string,
mode?: 'local' | 'mirror', mode?: 'local' | 'mirror',
allow_delete?: boolean,
remote_url?: string, remote_url?: string,
connect_host?: string, connect_host?: string,
host_header?: string, host_header?: string,
@@ -693,9 +697,9 @@ export const api = {
tls_insecure_skip_verify?: boolean, tls_insecure_skip_verify?: boolean,
sync_interval_sec?: number sync_interval_sec?: number
) => ) =>
request<{ status: string }>(`/api/repos/${repoId}/rpm/subdir/rename`, { request<{ status: string }>(`/api/repos/${repoId}/rpm/subdir/update`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ path, name, mode, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec }) body: JSON.stringify({ path, name, mode, allow_delete, remote_url, connect_host, host_header, tls_server_name, tls_insecure_skip_verify, sync_interval_sec })
}), }),
syncRpmSubdir: (repoId: string, path: string) => { syncRpmSubdir: (repoId: string, path: string) => {
const params = new URLSearchParams() const params = new URLSearchParams()
@@ -718,6 +722,13 @@ export const api = {
method: 'POST' method: 'POST'
}) })
}, },
rebuildRpmSubdirMetadata: (repoId: string, path: string) => {
const params = new URLSearchParams()
params.set('path', path)
return request<{ status: string }>(`/api/repos/${repoId}/rpm/subdir/rebuild-metadata?${params.toString()}`, {
method: 'POST'
})
},
listRpmMirrorRuns: (repoId: string, path: string, limit?: number) => { listRpmMirrorRuns: (repoId: string, path: string, limit?: number) => {
const params = new URLSearchParams() const params = new URLSearchParams()
params.set('path', path) params.set('path', path)

View File

@@ -64,11 +64,42 @@ export default function CommitDetailPage() {
}) })
}, [repoId]) }, [repoId])
const handleFileDiff = async (file: string) => { const extractFileDiffFromCommitDiff = (raw: string, file: string) => {
if (!repoId || !hash) return const text = (raw || '').replace(/\r\n/g, '\n')
const res = await api.getRepoFileDiff(repoId, hash, file) if (!text) return ''
const lines = text.split('\n')
const blocks: string[] = []
let current: string[] = []
let i = 0
let line = ''
for (i = 0; i < lines.length; i += 1) {
line = lines[i]
if (line.startsWith('diff --git ')) {
if (current.length > 0) {
blocks.push(current.join('\n'))
}
current = [line]
} else if (current.length > 0) {
current.push(line)
}
}
if (current.length > 0) {
blocks.push(current.join('\n'))
}
for (i = 0; i < blocks.length; i += 1) {
const block = blocks[i]
const header = block.split('\n', 1)[0] || ''
if (header.includes(` a/${file} `) || header.endsWith(` a/${file}`) || header.includes(` b/${file} `) || header.endsWith(` b/${file}`)) {
return block
}
}
return ''
}
const handleFileDiff = (file: string) => {
const extracted = extractFileDiffFromCommitDiff(diff, file)
setSelectedFile(file) setSelectedFile(file)
setFileDiff(res.diff || '') setFileDiff(extracted)
} }
const renderSplitDiff = (raw: string) => { const renderSplitDiff = (raw: string) => {

View File

@@ -33,6 +33,7 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
const [defaultBranch, setDefaultBranch] = useState<string>('') const [defaultBranch, setDefaultBranch] = useState<string>('')
const [tree, setTree] = useState<RepoTreeEntry[]>([]) const [tree, setTree] = useState<RepoTreeEntry[]>([])
const [treeError, setTreeError] = useState<string | null>(null) const [treeError, setTreeError] = useState<string | null>(null)
const [treeReloadTick, setTreeReloadTick] = useState(0)
const [fileQuery, setFileQuery] = useState('') const [fileQuery, setFileQuery] = useState('')
const [path, setPath] = useState('') const [path, setPath] = useState('')
const [pathSegments, setPathSegments] = useState<string[]>([]) const [pathSegments, setPathSegments] = useState<string[]>([])
@@ -142,7 +143,7 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
if (!ref && branches.length === 0) return if (!ref && branches.length === 0) return
if (!repo) return if (!repo) return
if (repo && repo.type && repo.type !== 'git') return if (repo && repo.type && repo.type !== 'git') return
const key = `${repoId}:${ref}:${path}` const key = `${repoId}:${ref}:${path}:${treeReloadTick}`
if (lastTreeKey.current === key) return if (lastTreeKey.current === key) return
lastTreeKey.current = key lastTreeKey.current = key
api.listRepoTree(repoId, ref || undefined, path) api.listRepoTree(repoId, ref || undefined, path)
@@ -163,7 +164,7 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
setSelectedCommit(null) setSelectedCommit(null)
} }
}) })
}, [repoId, ref, path, branches]) }, [repoId, ref, path, branches, treeReloadTick])
useEffect(() => { useEffect(() => {
if (!repoId || !ref) { if (!repoId || !ref) {
@@ -270,6 +271,10 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
} }
const handleBreadcrumb = (nextPath: string) => { const handleBreadcrumb = (nextPath: string) => {
if (nextPath === path) {
setTreeReloadTick((prev) => prev + 1)
return
}
setPath(nextPath) setPath(nextPath)
if (nextPath === '') { if (nextPath === '') {
setPathSegments([]) setPathSegments([])

View File

@@ -54,12 +54,13 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
const [rpmMetaContent, setRpmMetaContent] = useState('') const [rpmMetaContent, setRpmMetaContent] = useState('')
const [rpmMetaError, setRpmMetaError] = useState<string | null>(null) const [rpmMetaError, setRpmMetaError] = useState<string | null>(null)
const [rpmMetaLoading, setRpmMetaLoading] = useState(false) const [rpmMetaLoading, setRpmMetaLoading] = useState(false)
const [rpmTab, setRpmTab] = useState<'meta' | 'files'>('meta') const [rpmTab, setRpmTab] = useState<'meta' | 'files' | 'changelog'>('meta')
const [sidebarOpen, setSidebarOpen] = useState(true) const [sidebarOpen, setSidebarOpen] = useState(true)
const [subdirOpen, setSubdirOpen] = useState(false) const [subdirOpen, setSubdirOpen] = useState(false)
const [subdirName, setSubdirName] = useState('') const [subdirName, setSubdirName] = useState('')
const [subdirType, setSubdirType] = useState<'container' | 'repo'>('container') const [subdirType, setSubdirType] = useState<'container' | 'repo'>('container')
const [subdirMode, setSubdirMode] = useState<'local' | 'mirror'>('local') const [subdirMode, setSubdirMode] = useState<'local' | 'mirror'>('local')
const [subdirAllowDelete, setSubdirAllowDelete] = useState(false)
const [subdirSyncIntervalSec, setSubdirSyncIntervalSec] = useState('300') const [subdirSyncIntervalSec, setSubdirSyncIntervalSec] = useState('300')
const [subdirRemoteURL, setSubdirRemoteURL] = useState('') const [subdirRemoteURL, setSubdirRemoteURL] = useState('')
const [subdirConnectHost, setSubdirConnectHost] = useState('') const [subdirConnectHost, setSubdirConnectHost] = useState('')
@@ -86,6 +87,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
const [renameNewName, setRenameNewName] = useState('') const [renameNewName, setRenameNewName] = useState('')
const [renameIsRepoDir, setRenameIsRepoDir] = useState(false) const [renameIsRepoDir, setRenameIsRepoDir] = useState(false)
const [renameMode, setRenameMode] = useState<'local' | 'mirror'>('local') const [renameMode, setRenameMode] = useState<'local' | 'mirror'>('local')
const [renameAllowDelete, setRenameAllowDelete] = useState(false)
const [renameSyncIntervalSec, setRenameSyncIntervalSec] = useState('300') const [renameSyncIntervalSec, setRenameSyncIntervalSec] = useState('300')
const [renameRemoteURL, setRenameRemoteURL] = useState('') const [renameRemoteURL, setRenameRemoteURL] = useState('')
const [renameConnectHost, setRenameConnectHost] = useState('') const [renameConnectHost, setRenameConnectHost] = useState('')
@@ -112,8 +114,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
const [clearRunsConfirmOpen, setClearRunsConfirmOpen] = useState(false) const [clearRunsConfirmOpen, setClearRunsConfirmOpen] = useState(false)
const [rpmPath, setRpmPath] = useState('') const [rpmPath, setRpmPath] = useState('')
const [rpmPathSegments, setRpmPathSegments] = useState<string[]>([]) const [rpmPathSegments, setRpmPathSegments] = useState<string[]>([])
const [rpmFileQuery, setRpmFileQuery] = useState('')
const [rpmTree, setRpmTree] = useState<RpmTreeEntry[]>([]) const [rpmTree, setRpmTree] = useState<RpmTreeEntry[]>([])
const [rpmTreeError, setRpmTreeError] = useState<string | null>(null) const [rpmTreeError, setRpmTreeError] = useState<string | null>(null)
const [rpmTreeReloadTick, setRpmTreeReloadTick] = useState(0)
const [rpmSelectedEntry, setRpmSelectedEntry] = useState<RpmTreeEntry | null>(null) const [rpmSelectedEntry, setRpmSelectedEntry] = useState<RpmTreeEntry | null>(null)
const [canWrite, setCanWrite] = useState(false) const [canWrite, setCanWrite] = useState(false)
const initRepoRef = useRef<string | null>(null) const initRepoRef = useRef<string | null>(null)
@@ -185,7 +189,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setRpmTreeError(message) setRpmTreeError(message)
setRpmTree([]) setRpmTree([])
}) })
}, [repoId, repo, rpmPath]) }, [repoId, repo, rpmPath, rpmTreeReloadTick])
const handleSelectRpm = async (pkg: RpmPackageSummary) => { const handleSelectRpm = async (pkg: RpmPackageSummary) => {
if (!repoId) return if (!repoId) return
@@ -193,7 +197,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setRpmDetail(null) setRpmDetail(null)
setRpmDetailLoading(true) setRpmDetailLoading(true)
setRpmError(null) setRpmError(null)
setRpmTab('meta')
try { try {
const detail = await api.getRpmPackage(repoId, pkg.filename) const detail = await api.getRpmPackage(repoId, pkg.filename)
setRpmSelected({ setRpmSelected({
@@ -264,9 +267,12 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setSubdirError(null) setSubdirError(null)
setSubdirSaving(true) setSubdirSaving(true)
try { try {
syncIntervalSec = Number(subdirSyncIntervalSec) syncIntervalSec = 0
if (!Number.isFinite(syncIntervalSec) || syncIntervalSec <= 0) { if (subdirType === 'repo' && subdirMode === 'mirror') {
syncIntervalSec = 300 syncIntervalSec = Number(subdirSyncIntervalSec)
if (!Number.isFinite(syncIntervalSec) || syncIntervalSec <= 0) {
syncIntervalSec = 300
}
} }
const parent = rpmPath const parent = rpmPath
await api.createRpmSubdir( await api.createRpmSubdir(
@@ -275,6 +281,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
subdirType, subdirType,
parent, parent,
subdirMode, subdirMode,
subdirAllowDelete,
subdirRemoteURL.trim(), subdirRemoteURL.trim(),
subdirConnectHost.trim(), subdirConnectHost.trim(),
subdirHostHeader.trim(), subdirHostHeader.trim(),
@@ -295,6 +302,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setSubdirName('') setSubdirName('')
setSubdirType('container') setSubdirType('container')
setSubdirMode('local') setSubdirMode('local')
setSubdirAllowDelete(false)
setSubdirSyncIntervalSec('300') setSubdirSyncIntervalSec('300')
setSubdirRemoteURL('') setSubdirRemoteURL('')
setSubdirConnectHost('') setSubdirConnectHost('')
@@ -359,21 +367,25 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setRenameError(null) setRenameError(null)
setRenaming(true) setRenaming(true)
try { try {
syncIntervalSec = Number(renameSyncIntervalSec) syncIntervalSec = 0
if (!Number.isFinite(syncIntervalSec) || syncIntervalSec <= 0) { if (renameIsRepoDir && renameMode === 'mirror') {
syncIntervalSec = 300 syncIntervalSec = Number(renameSyncIntervalSec)
if (!Number.isFinite(syncIntervalSec) || syncIntervalSec <= 0) {
syncIntervalSec = 300
}
} }
await api.renameRpmSubdir( await api.updateRpmSubdir(
repoId, repoId,
renamePath, renamePath,
renameNewName.trim(), renameNewName.trim(),
renameIsRepoDir ? renameMode : undefined, renameIsRepoDir ? renameMode : undefined,
renameIsRepoDir ? renameAllowDelete : undefined,
renameIsRepoDir ? renameRemoteURL.trim() : undefined, renameIsRepoDir ? renameRemoteURL.trim() : undefined,
renameIsRepoDir ? renameConnectHost.trim() : undefined, renameIsRepoDir ? renameConnectHost.trim() : undefined,
renameIsRepoDir ? renameHostHeader.trim() : undefined, renameIsRepoDir ? renameHostHeader.trim() : undefined,
renameIsRepoDir ? renameTLSServerName.trim() : undefined, renameIsRepoDir ? renameTLSServerName.trim() : undefined,
renameIsRepoDir ? renameTLSInsecureSkipVerify : undefined, renameIsRepoDir ? renameTLSInsecureSkipVerify : undefined,
renameIsRepoDir ? syncIntervalSec : undefined renameIsRepoDir && renameMode === 'mirror' ? syncIntervalSec : undefined
) )
setRenameOpen(false) setRenameOpen(false)
setRenamePath('') setRenamePath('')
@@ -381,6 +393,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setRenameNewName('') setRenameNewName('')
setRenameIsRepoDir(false) setRenameIsRepoDir(false)
setRenameMode('local') setRenameMode('local')
setRenameAllowDelete(false)
setRenameSyncIntervalSec('300') setRenameSyncIntervalSec('300')
setRenameRemoteURL('') setRenameRemoteURL('')
setRenameConnectHost('') setRenameConnectHost('')
@@ -450,26 +463,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
} }
} }
const handleStatusSyncNow = async () => {
if (!repoId || !statusPath || statusMode !== 'mirror') return
setStatusSyncBusy(true)
setStatusError(null)
try {
await api.syncRpmSubdir(repoId, statusPath)
setStatusSyncEnabled(true)
setStatusSyncStatus('scheduled')
setStatusSyncStep('queued')
setStatusSyncError('')
const runs = await api.listRpmMirrorRuns(repoId, statusPath, 10)
setStatusRuns(Array.isArray(runs) ? runs : [])
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to schedule sync'
setStatusError(message)
} finally {
setStatusSyncBusy(false)
}
}
const handleStatusToggleSyncEnabled = async () => { const handleStatusToggleSyncEnabled = async () => {
if (!repoId || !statusPath || statusMode !== 'mirror') return if (!repoId || !statusPath || statusMode !== 'mirror') return
setStatusSyncBusy(true) setStatusSyncBusy(true)
@@ -477,13 +470,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
try { try {
if (statusSyncEnabled) { if (statusSyncEnabled) {
await api.suspendRpmSubdir(repoId, statusPath) await api.suspendRpmSubdir(repoId, statusPath)
setStatusSyncEnabled(false)
setStatusSyncStatus('suspended')
} else { } else {
await api.resumeRpmSubdir(repoId, statusPath) await api.resumeRpmSubdir(repoId, statusPath)
setStatusSyncEnabled(true)
setStatusSyncStatus('scheduled')
} }
await loadStatus(statusPath)
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Failed to change mirror sync state' const message = err instanceof Error ? err.message : 'Failed to change mirror sync state'
setStatusError(message) setStatusError(message)
@@ -513,21 +503,41 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
} }
} }
const handleStatusRebuildMetadata = async () => {
if (!repoId || !statusPath || statusMode !== 'mirror') return
setStatusSyncBusy(true)
setStatusError(null)
try {
await api.rebuildRpmSubdirMetadata(repoId, statusPath)
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to schedule metadata rebuild'
setStatusError(message)
} finally {
setStatusSyncBusy(false)
}
}
const handleRpmBack = () => { const handleRpmBack = () => {
if (!rpmPath) return if (!rpmPath) return
const nextSegments = rpmPathSegments.slice(0, -1) const nextSegments = rpmPathSegments.slice(0, -1)
setRpmPath(nextSegments.join('/')) setRpmPath(nextSegments.join('/'))
setRpmPathSegments(nextSegments) setRpmPathSegments(nextSegments)
setRpmFileQuery('')
setRpmSelectedEntry(null) setRpmSelectedEntry(null)
} }
const handleRpmBreadcrumb = (nextPath: string) => { const handleRpmBreadcrumb = (nextPath: string) => {
if (nextPath === rpmPath) {
setRpmTreeReloadTick((prev) => prev + 1)
return
}
setRpmPath(nextPath) setRpmPath(nextPath)
if (nextPath === '') { if (nextPath === '') {
setRpmPathSegments([]) setRpmPathSegments([])
} else { } else {
setRpmPathSegments(nextPath.split('/').filter(Boolean)) setRpmPathSegments(nextPath.split('/').filter(Boolean))
} }
setRpmFileQuery('')
setRpmSelectedEntry(null) setRpmSelectedEntry(null)
} }
@@ -597,6 +607,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
if (entry.type === 'dir') { if (entry.type === 'dir') {
setRpmPath(entry.path) setRpmPath(entry.path)
setRpmPathSegments(entry.path.split('/').filter(Boolean)) setRpmPathSegments(entry.path.split('/').filter(Boolean))
setRpmFileQuery('')
setRpmSelectedEntry(null) setRpmSelectedEntry(null)
return return
} }
@@ -639,6 +650,84 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
} }
const rpmPathParts = rpmPathSegments const rpmPathParts = rpmPathSegments
const normalizedQuery = rpmFileQuery.trim().toLowerCase()
const globToRegex = (query: string) => {
let i = 0
let out = '^'
while (i < query.length) {
const ch = query[i]
if (ch === '*') {
out += '.*'
i += 1
continue
}
if (ch === '?') {
out += '.'
i += 1
continue
}
if (ch === '[') {
const classStart = i
i += 1
if (i >= query.length) {
out += '\\['
break
}
let negate = false
if (query[i] === '!' || query[i] === '^') {
negate = true
i += 1
}
let classText = ''
let sawClass = false
while (i < query.length) {
const next = query[i]
if (next === ']') {
sawClass = true
i += 1
break
}
if (next === '\\') {
classText += '\\\\'
i += 1
continue
}
classText += next
i += 1
}
if (!sawClass) {
out += '\\['
i = classStart + 1
continue
}
out += negate ? `[^${classText}]` : `[${classText}]`
continue
}
if ('.+^$(){}|\\'.includes(ch)) {
out += `\\${ch}`
i += 1
continue
}
out += ch
i += 1
}
out += '$'
return out
}
const matchesFileQuery = (name: string, query: string) => {
if (!query) return true
if (query.includes('*') || query.includes('?') || query.includes('[')) {
try {
return new RegExp(globToRegex(query), 'i').test(name)
} catch {
return name.toLowerCase().includes(query)
}
}
return name.toLowerCase().includes(query)
}
const filteredTree = normalizedQuery
? rpmTree.filter((entry) => matchesFileQuery(entry.name, normalizedQuery))
: rpmTree
return ( return (
<Box> <Box>
@@ -694,6 +783,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setSubdirName('') setSubdirName('')
setSubdirType('container') setSubdirType('container')
setSubdirMode('local') setSubdirMode('local')
setSubdirAllowDelete(false)
setSubdirSyncIntervalSec('300') setSubdirSyncIntervalSec('300')
setSubdirRemoteURL('') setSubdirRemoteURL('')
setSubdirConnectHost('') setSubdirConnectHost('')
@@ -745,6 +835,14 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
) )
})} })}
</Box> </Box>
<TextField
size="small"
placeholder="Search files"
value={rpmFileQuery}
onChange={(event) => setRpmFileQuery(event.target.value)}
fullWidth
sx={{ mb: 1, px: 0.5 }}
/>
{rpmTreeError ? ( {rpmTreeError ? (
<Alert severity="warning" sx={{ mb: 1 }}> <Alert severity="warning" sx={{ mb: 1 }}>
{rpmTreeError} {rpmTreeError}
@@ -765,7 +863,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
) : null} ) : null}
{rpmTree.map((entry) => ( {filteredTree.map((entry) => (
<ListItem key={entry.path} disablePadding> <ListItem key={entry.path} disablePadding>
<ListItemButton onClick={() => handleRpmEntry(entry)}> <ListItemButton onClick={() => handleRpmEntry(entry)}>
<ListItemText <ListItemText
@@ -814,6 +912,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
setRenameNewName(entry.name) setRenameNewName(entry.name)
setRenameIsRepoDir(Boolean(entry.is_repo_dir)) setRenameIsRepoDir(Boolean(entry.is_repo_dir))
setRenameMode(entry.repo_mode === 'mirror' ? 'mirror' : 'local') setRenameMode(entry.repo_mode === 'mirror' ? 'mirror' : 'local')
setRenameAllowDelete(false)
setRenameSyncIntervalSec('300') setRenameSyncIntervalSec('300')
setRenameRemoteURL('') setRenameRemoteURL('')
setRenameConnectHost('') setRenameConnectHost('')
@@ -824,6 +923,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
try { try {
const cfg = await api.getRpmSubdir(repoId, entry.path) const cfg = await api.getRpmSubdir(repoId, entry.path)
setRenameMode(cfg.mode === 'mirror' ? 'mirror' : 'local') setRenameMode(cfg.mode === 'mirror' ? 'mirror' : 'local')
setRenameAllowDelete(Boolean(cfg.allow_delete))
setRenameSyncIntervalSec(String(cfg.sync_interval_sec || 300)) setRenameSyncIntervalSec(String(cfg.sync_interval_sec || 300))
setRenameRemoteURL(cfg.remote_url || '') setRenameRemoteURL(cfg.remote_url || '')
setRenameConnectHost(cfg.connect_host || '') setRenameConnectHost(cfg.connect_host || '')
@@ -880,9 +980,9 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
) : null} ) : null}
</ListItem> </ListItem>
))} ))}
{!rpmTree.length && !rpmTreeError ? ( {!filteredTree.length && !rpmTreeError ? (
<Typography variant="body2" color="text.secondary" sx={{ px: 1, py: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ px: 1, py: 1 }}>
No files found. {normalizedQuery ? 'No matching files.' : 'No files found.'}
</Typography> </Typography>
) : null} ) : null}
</List> </List>
@@ -910,6 +1010,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
<Tabs value={rpmTab} onChange={(_, value) => setRpmTab(value)}> <Tabs value={rpmTab} onChange={(_, value) => setRpmTab(value)}>
<Tab label="Metadata" value="meta" /> <Tab label="Metadata" value="meta" />
<Tab label="Files" value="files" /> <Tab label="Files" value="files" />
<Tab label="Change Log" value="changelog" />
</Tabs> </Tabs>
{rpmDetailLoading ? ( {rpmDetailLoading ? (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
@@ -952,10 +1053,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
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>
<Typography variant="body2">Size: {rpmDetail.size ? `${rpmDetail.size} bytes` : 'n/a'}</Typography> <Typography variant="body2">Size: {rpmDetail.size ? `${rpmDetail.size} bytes` : 'n/a'}</Typography>
{rpmDetail.requires.length ? ( {Array.isArray(rpmDetail.requires) && rpmDetail.requires.length ? (
<Typography variant="body2">Requires: {rpmDetail.requires.join(', ')}</Typography> <Typography variant="body2">Requires: {rpmDetail.requires.join(', ')}</Typography>
) : null} ) : null}
{rpmDetail.provides.length ? ( {Array.isArray(rpmDetail.provides) && rpmDetail.provides.length ? (
<Typography variant="body2">Provides: {rpmDetail.provides.join(', ')}</Typography> <Typography variant="body2">Provides: {rpmDetail.provides.join(', ')}</Typography>
) : null} ) : null}
{rpmDetail.description ? ( {rpmDetail.description ? (
@@ -968,13 +1069,36 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
{rpmDetail && rpmTab === 'files' ? ( {rpmDetail && rpmTab === 'files' ? (
<Box sx={{ mt: 1, maxHeight: '60vh', overflow: 'auto' }}> <Box sx={{ mt: 1, maxHeight: '60vh', overflow: 'auto' }}>
<List dense> <List dense>
{rpmDetail.files.map((file) => ( {(Array.isArray(rpmDetail.files) ? rpmDetail.files : []).map((file) => (
<ListItem key={file}> <ListItem key={file}>
<ListItemText primary={file} /> <ListItemText primary={file} />
</ListItem> </ListItem>
))} ))}
</List> </List>
</Box> </Box>
) : null}
{rpmDetail && rpmTab === 'changelog' ? (
<Box sx={{ mt: 1, maxHeight: '60vh', overflow: 'auto' }}>
{Array.isArray(rpmDetail.changelogs) && rpmDetail.changelogs.length ? (
<List dense>
{rpmDetail.changelogs.map((item, index) => (
<ListItem key={`${item.date}-${index}`} sx={{ display: 'block' }}>
<Typography variant="body2" color="text.secondary">
{item.date ? new Date(item.date * 1000).toLocaleString() : 'n/a'}
{item.author ? ` · ${item.author}` : ''}
</Typography>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
{item.text || ''}
</Typography>
</ListItem>
))}
</List>
) : (
<Typography variant="body2" color="text.secondary">
No change log entries.
</Typography>
)}
</Box>
) : null} ) : null}
{!rpmDetail && !rpmDetailLoading && rpmError ? ( {!rpmDetail && !rpmDetailLoading && rpmError ? (
<Alert severity="warning" sx={{ mt: 1 }}> <Alert severity="warning" sx={{ mt: 1 }}>
@@ -1058,7 +1182,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
<MenuItem value="mirror">mirror</MenuItem> <MenuItem value="mirror">mirror</MenuItem>
</TextField> </TextField>
) : null} ) : null}
{subdirType === 'repo' ? ( {subdirType === 'repo' && subdirMode === 'mirror' ? (
<TextField <TextField
label="Sync Interval (seconds)" label="Sync Interval (seconds)"
value={subdirSyncIntervalSec} value={subdirSyncIntervalSec}
@@ -1097,6 +1221,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
helperText="Optional SNI/verify server name override" helperText="Optional SNI/verify server name override"
fullWidth fullWidth
/> />
<FormControlLabel
control={<Checkbox checked={subdirAllowDelete} onChange={(event) => setSubdirAllowDelete(event.target.checked)} />}
label="Allow delete (files/container dirs) in mirror subtree"
/>
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
@@ -1204,10 +1332,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
</Typography> </Typography>
) : null} ) : null}
<Box sx={{ pt: 1 }}> <Box sx={{ pt: 1 }}>
<Button size="small" variant="outlined" onClick={handleStatusSyncNow} disabled={statusSyncBusy}> <Button size="small" variant="outlined" onClick={handleStatusToggleSyncEnabled} disabled={statusSyncBusy}>
{statusSyncBusy ? 'Scheduling...' : 'Sync now'}
</Button>
<Button size="small" variant="outlined" onClick={handleStatusToggleSyncEnabled} disabled={statusSyncBusy} sx={{ ml: 1 }}>
{statusSyncEnabled ? 'Suspend' : 'Resume'} {statusSyncEnabled ? 'Suspend' : 'Resume'}
</Button> </Button>
<Button <Button
@@ -1220,6 +1345,15 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
> >
Clear runs Clear runs
</Button> </Button>
<Button
size="small"
variant="outlined"
onClick={handleStatusRebuildMetadata}
disabled={statusSyncBusy}
sx={{ ml: 1 }}
>
Rebuild metadata
</Button>
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
@@ -1306,7 +1440,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
<MenuItem value="mirror">mirror</MenuItem> <MenuItem value="mirror">mirror</MenuItem>
</TextField> </TextField>
) : null} ) : null}
{renameIsRepoDir ? ( {renameIsRepoDir && renameMode === 'mirror' ? (
<TextField <TextField
label="Sync Interval (seconds)" label="Sync Interval (seconds)"
value={renameSyncIntervalSec} value={renameSyncIntervalSec}
@@ -1343,6 +1477,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
onChange={(event) => setRenameTLSServerName(event.target.value)} onChange={(event) => setRenameTLSServerName(event.target.value)}
fullWidth fullWidth
/> />
<FormControlLabel
control={<Checkbox checked={renameAllowDelete} onChange={(event) => setRenameAllowDelete(event.target.checked)} />}
label="Allow delete (files/container dirs) in mirror subtree"
/>
<FormControlLabel <FormControlLabel
control={<Checkbox checked={renameTLSInsecureSkipVerify} onChange={(event) => setRenameTLSInsecureSkipVerify(event.target.checked)} />} control={<Checkbox checked={renameTLSInsecureSkipVerify} onChange={(event) => setRenameTLSInsecureSkipVerify(event.target.checked)} />}
label="Skip TLS certificate verification" label="Skip TLS certificate verification"