Files
remark42/backend/app/rest/api/migrator.go
T
UmputunandGitHub ba6bab5b91 Feature/cmd (#195)
* support flags commands, move to cmd

* fix target name

* test for happy path importer

* add export cmd

* fix wrong import, lint warns

* increase test timeout

* add sellp to allow main test server to start

* implement all cmds

* handle backup/restore errors

* fix import status check, hide secret from logs

* backup cmd err tests

* randimize test port

* avoid dup code in Last controller

* add target to make all bin archives

* remove container in make

* add smiple scripts to simplify commands, update readme

* add docs on dockerless, enforce app user

* add restore info

* move last to lastCommentsScope const
2018-08-26 21:45:04 -05:00

161 lines
4.6 KiB
Go

package api
import (
"compress/gzip"
"context"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/didip/tollbooth"
"github.com/didip/tollbooth_chi"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/umputun/remark/backend/app/migrator"
"github.com/umputun/remark/backend/app/rest"
"github.com/umputun/remark/backend/app/rest/cache"
"github.com/umputun/remark/backend/app/store/keys"
)
// Migrator rest runs on unexposed port and available for local requests only
type Migrator struct {
Version string
Cache cache.LoadingCache
NativeImporter migrator.Importer
DisqusImporter migrator.Importer
WordPressImporter migrator.Importer
NativeExported migrator.Exporter
KeyStore keys.Store
httpServer *http.Server
lock sync.Mutex
}
// Run the listener and request's router, activate rest server
// this server doesn't have any authentication and SHOULDN'T BE EXPOSED in any way
func (m *Migrator) Run(port int) {
log.Printf("[INFO] activate import server on port %d", port)
router := m.routes()
m.lock.Lock()
m.httpServer = &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port), Handler: router}
m.lock.Unlock()
err := m.httpServer.ListenAndServe()
log.Printf("[WARN] http server terminated, %s", err)
}
// Shutdown import http server
func (m *Migrator) Shutdown() {
log.Print("[WARN] shutdown import server")
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
m.lock.Lock()
if m.httpServer != nil {
if err := m.httpServer.Shutdown(ctx); err != nil {
log.Printf("[DEBUG] importer shutdown error, %s", err)
}
}
m.lock.Unlock()
log.Print("[DEBUG] shutdown import server completed")
}
func (m *Migrator) routes() chi.Router {
router := chi.NewRouter()
router.Use(middleware.RealIP, Recoverer)
router.Use(middleware.Throttle(1000), middleware.Timeout(15*time.Minute))
router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
router.Use(AppInfo("remark42-migrator", m.Version), Ping, Logger(nil, LogAll))
router.Post("/api/v1/admin/import", m.importCtrl)
router.Get("/api/v1/admin/export", m.exportCtrl)
return router
}
// POST /import?secret=key&site=site-id&provider=disqus|remark|wordpress
// imports comments from post body.
func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
secret := r.URL.Query().Get("secret")
skey, err := m.KeyStore.Get(siteID)
if err != nil {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, JSON{"status": "error", "details": "secret key store"})
return
}
if strings.TrimSpace(secret) == "" || secret != skey {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, JSON{"status": "error", "details": "secret key"})
return
}
var importer migrator.Importer
switch r.URL.Query().Get("provider") {
case "disqus":
importer = m.DisqusImporter
case "wordpress":
importer = m.WordPressImporter
default:
importer = m.NativeImporter
}
log.Printf("[DEBUG] import request for site=%s", siteID)
size, err := importer.Import(r.Body, siteID)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "import failed")
return
}
m.Cache.Flush(cache.Flusher(siteID).Scopes(siteID))
render.Status(r, http.StatusCreated)
render.JSON(w, r, JSON{"status": "ok", "size": size})
}
// GET /export?site=site-id&secret=12345
// exports all comments for siteID as gz file
func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
secret := r.URL.Query().Get("secret")
skey, err := m.KeyStore.Get(siteID)
if err != nil {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, JSON{"status": "error", "details": "secret key store"})
return
}
if strings.TrimSpace(secret) == "" || secret != skey {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, JSON{"status": "error", "details": "secret key"})
return
}
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
log.Printf("[DEBUG] import request for site=%s to %s", siteID, exportFile)
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
gzWriter := gzip.NewWriter(w)
defer func() {
if e := gzWriter.Close(); e != nil {
log.Printf("[WARN] can't close gzip writer, %s", e)
}
}()
if _, err := m.NativeExported.Export(gzWriter, siteID); err != nil {
log.Printf("[WARN] can't export, %+v", err)
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed")
return
}
}