From e022c5ae1723e09b4700f4e8c7d1008e22bf4af2 Mon Sep 17 00:00:00 2001 From: pin2t Date: Thu, 30 Jul 2026 16:46:48 +0500 Subject: [PATCH] dbui: add export/import CSV feature and fix trash icon alignment - Move clear-bucket trash icon into its own header column, aligned with per-row trash icons - Add GET /api/export endpoint to return all rows for a bucket - Add POST /api/import endpoint for bulk import with skip/replace strategy - Add Export button that downloads bucket data as CSV (hex:-prefixed for binary, proper quote escaping) - Add Import button with modal dialog for strategy selection and CSV file upload --- dbui/dbui.go | 76 +++++++++++++++++++++++++ dbui/index.html | 145 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 220 insertions(+), 1 deletion(-) diff --git a/dbui/dbui.go b/dbui/dbui.go index c3415a2..e74155c 100644 --- a/dbui/dbui.go +++ b/dbui/dbui.go @@ -56,6 +56,8 @@ func handler(db *bbolt.DB) http.Handler { mux.HandleFunc("/api/put", func(w http.ResponseWriter, r *http.Request) { put(db, w, r) }) mux.HandleFunc("/api/delete", func(w http.ResponseWriter, r *http.Request) { del(db, w, r) }) mux.HandleFunc("/api/clearbucket", func(w http.ResponseWriter, r *http.Request) { clearBucket(db, w, r) }) + mux.HandleFunc("/api/export", func(w http.ResponseWriter, r *http.Request) { exportBucket(db, w, r) }) + mux.HandleFunc("/api/import", func(w http.ResponseWriter, r *http.Request) { importBucket(db, w, r) }) return mux } @@ -244,6 +246,80 @@ func clearBucket(db *bbolt.DB, w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"ok": true, "deleted": count}) } +func exportBucket(db *bbolt.DB, w http.ResponseWriter, r *http.Request) { + var bucket = r.URL.Query().Get("bucket") + if bucket == "" { + http.Error(w, "bucket is required", http.StatusBadRequest) + return + } + var rows = []kvRow{} + var err = db.View(func(tx *bbolt.Tx) error { + var b = tx.Bucket([]byte(bucket)) + if b == nil { return errNoBucket } + return b.ForEach(func(k, v []byte) error { + rows = append(rows, kvRow{encodeField(k), encodeField(v)}) + return nil + }) + }) + if err != nil { + if err == errNoBucket { http.Error(w, err.Error(), http.StatusNotFound); return } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, map[string]any{"rows": rows}) +} + +func importBucket(db *bbolt.DB, w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + Bucket string `json:"bucket"` + Strategy string `json:"strategy"` // "skip" or "replace" + Rows []kvRow `json:"rows"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if body.Bucket == "" { + http.Error(w, "bucket is required", http.StatusBadRequest) + return + } + if body.Strategy != "skip" && body.Strategy != "replace" { + http.Error(w, "strategy must be 'skip' or 'replace'", http.StatusBadRequest) + return + } + var imported, skipped int + var err = db.Update(func(tx *bbolt.Tx) error { + var b = tx.Bucket([]byte(body.Bucket)) + if b == nil { return errNoBucket } + for _, row := range body.Rows { + var key, kerr = decodeField(row.Key) + if kerr != nil { return kerr } + var value, verr = decodeField(row.Value) + if verr != nil { return verr } + var exists = b.Get(key) != nil + if exists && body.Strategy == "skip" { + skipped++ + continue + } + if err := b.Put(key, value); err != nil { return err } + imported++ + } + return nil + }) + if err != nil { + var code = http.StatusBadRequest + if err == errNoBucket { code = http.StatusNotFound } + http.Error(w, err.Error(), code) + return + } + logging.Info("database UI: imported %d keys into %s (%d skipped)", imported, body.Bucket, skipped) + writeJSON(w, map[string]any{"ok": true, "imported": imported, "skipped": skipped}) +} + func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) diff --git a/dbui/index.html b/dbui/index.html index fc45f7d..729d39b 100644 --- a/dbui/index.html +++ b/dbui/index.html @@ -75,6 +75,19 @@ #status.err { color: #dc2626; } #status.ok { color: #16a34a; } .empty { color: var(--muted); text-align: center; padding: 40px 0; } + .dialog-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.45); + display: flex; align-items: center; justify-content: center; z-index: 100; + } + .dialog-overlay[hidden] { display: none; } + .dialog { + background: var(--panel); border: 1px solid var(--line); border-radius: 8px; + padding: 20px 24px; min-width: 320px; max-width: 420px; + } + .dialog p { margin: 0 0 12px; } + .dialog label { display: block; margin-bottom: 6px; cursor: pointer; } + .dialog input[type="radio"] { min-width: 0; margin-right: 6px; } + .dialog-buttons { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; } @@ -95,7 +108,7 @@

bitnsbot database

- +
KeyValue
KeyValue