feat: add basic authenticated endpoint to download the latest database backup
Build and Push / build (godump, amd64, linux) (push) Successful in 23s
Build and Push / build (godump, amd64, linux) (push) Successful in 23s
This commit is contained in:
@@ -70,6 +70,8 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/api/run/", s.requireAuthAPI(s.handleRunInstance))
|
||||
s.mux.HandleFunc("/api/download", s.requireAuthAPI(s.handleDownload))
|
||||
s.mux.HandleFunc("/api/delete", s.requireAuthAPI(s.handleDelete))
|
||||
|
||||
s.mux.HandleFunc("/download/", s.requireBasicAuth(s.handleLatestDownload))
|
||||
}
|
||||
|
||||
var sessionToken string
|
||||
@@ -110,6 +112,24 @@ func (s *Server) requireAuthAPI(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) requireBasicAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.cfg.Auth.Enabled {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != s.cfg.Auth.Username || pass != s.cfg.Auth.Password {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.cfg.Auth.Enabled {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
@@ -190,6 +210,70 @@ func (s *Server) resolveFilePath(instName, dbName, fileName string) (string, err
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleLatestDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Path should be /download/<instance>/<db> or /download/<instance>/<db>/
|
||||
path := strings.TrimPrefix(r.URL.Path, "/download/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) != 2 {
|
||||
http.Error(w, "Invalid path format. Expected /download/<servername>/<databasename>/", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
instName := parts[0]
|
||||
dbName := parts[1]
|
||||
|
||||
inst := s.manager.GetInstance(instName)
|
||||
if inst == nil {
|
||||
http.Error(w, "Instance not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
baseDir := filepath.Clean(inst.Config.BackupDir)
|
||||
dbPath := filepath.Join(baseDir, dbName)
|
||||
|
||||
files, err := os.ReadDir(dbPath)
|
||||
if err != nil {
|
||||
http.Error(w, "Database backups not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var latestFile os.FileInfo
|
||||
for _, entry := range files {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if latestFile == nil || info.ModTime().After(latestFile.ModTime()) {
|
||||
latestFile = info
|
||||
}
|
||||
}
|
||||
|
||||
if latestFile == nil {
|
||||
http.Error(w, "No backups found for this database", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
filePath, err := s.resolveFilePath(instName, dbName, latestFile.Name())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", latestFile.Name()))
|
||||
w.Header().Set("Content-Type", "application/x-gzip")
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
Reference in New Issue
Block a user