package migrator import ( "bytes" "context" "encoding/json" "io" "sync/atomic" log "github.com/go-pkgz/lgr" "github.com/go-pkgz/syncs" "github.com/pkg/errors" "github.com/umputun/remark/backend/app/store" "github.com/umputun/remark/backend/app/store/service" ) const nativeVersion = 1 const defaultConcurrent = 8 // Native implements exporter and importer for internal store format // {"version": 1, comments:[{...}\n,{}], meta: {meta}} // each comments starts from the new line type Native struct { DataStore Store Concurrent int } type meta struct { Version int `json:"version"` Users []service.UserMetaData `json:"users"` Posts []service.PostMetaData `json:"posts"` } // Export all comments to writer as json strings. Each comment is one string, separated by "\n" // The final file is a valid json func (n *Native) Export(w io.Writer, siteID string) (size int, err error) { if err = n.exportMeta(siteID, w); err != nil { return 0, errors.Wrapf(err, "failed to export meta for site %s", siteID) } topics, err := n.DataStore.List(siteID, 0, 0) if err != nil { return 0, err } log.Printf("[DEBUG] exporting %d topics", len(topics)) commentsCount := 0 for i := len(topics) - 1; i >= 0; i-- { // topics from List sorted in opposite direction topic := topics[i] comments, e := n.DataStore.Find(store.Locator{SiteID: siteID, URL: topic.URL}, "time") if e != nil { return commentsCount, e } for _, comment := range comments { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) if err = enc.Encode(comment); err != nil { return commentsCount, errors.Wrapf(err, "can't marshal %v", comments) } if _, err = w.Write(buf.Bytes()); err != nil { return commentsCount, errors.Wrap(err, "can't write comment data") } commentsCount++ } } log.Printf("[DEBUG] exported %d comments", commentsCount) return commentsCount, nil } // exportMeta appends user and post metas to exported stream func (n *Native) exportMeta(siteID string, w io.Writer) (err error) { m := meta{Version: nativeVersion} m.Users, m.Posts, err = n.DataStore.Metas(siteID) if err != nil { return errors.Wrap(err, "can't get meta") } if err = json.NewEncoder(w).Encode(m); err != nil { return errors.Wrap(err, "can't encode meta") } return nil } // Import comments from json strings produced by Remark.Export func (n *Native) Import(reader io.Reader, siteID string) (size int, err error) { m := meta{} dec := json.NewDecoder(reader) if err = dec.Decode(&m); err != nil { return 0, errors.Wrapf(err, "failed to import meta for site %s", siteID) } if m.Version != nativeVersion && m.Version != 0 { // this version allows back compatibility with 0 version return 0, errors.Errorf("unexpected import file version %d", m.Version) } if err = n.DataStore.DeleteAll(siteID); err != nil { return 0, err } var failed, total, comments int64 concurrent := defaultConcurrent if n.Concurrent > 0 { concurrent = n.Concurrent } grp := syncs.NewSizedGroup(concurrent, syncs.Preemptive) for { comment := store.Comment{} err = dec.Decode(&comment) if err == io.EOF { break } total++ if err != nil { atomic.AddInt64(&failed, 1) failed++ continue } // write comments in parallel grp.Go(func(context.Context) { if _, e := n.DataStore.Create(comment); e != nil { atomic.AddInt64(&failed, 1) log.Printf("[WARN] can't write %+v to store, %s", comment, e) return } num := atomic.AddInt64(&comments, 1) if num%1000 == 0 { log.Printf("[DEBUG] imported %d comments", num) } }) } grp.Wait() if failed > 0 { return int(comments), errors.Errorf("failed to save %d comments", failed) } log.Printf("[INFO] imported %d comments from %d records", comments, total) err = n.DataStore.SetMetas(siteID, m.Users, m.Posts) return int(comments), err }