github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/logsync/logsync.go (about)

     1  // Package logsync synchronizes logs between logbooks across networks
     2  package logsync
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"strings"
    11  
    12  	golog "github.com/ipfs/go-log"
    13  	host "github.com/libp2p/go-libp2p-core/host"
    14  	peer "github.com/libp2p/go-libp2p-core/peer"
    15  	"github.com/qri-io/qri/dsref"
    16  	"github.com/qri-io/qri/logbook"
    17  	"github.com/qri-io/qri/logbook/oplog"
    18  	"github.com/qri-io/qri/profile"
    19  )
    20  
    21  var (
    22  	// ErrNoLogsync indicates no logsync pointer has been allocated where one is expected
    23  	ErrNoLogsync = fmt.Errorf("logsync: does not exist")
    24  
    25  	log = golog.Logger("logsync")
    26  )
    27  
    28  // Logsync fulfills requests from clients, logsync wraps a logbook.Book, pushing
    29  // and pulling logs from remote sources to its logbook
    30  type Logsync struct {
    31  	book       *logbook.Book
    32  	p2pHandler *p2pHandler
    33  
    34  	pushPreCheck   Hook
    35  	pushFinalCheck Hook
    36  	pushed         Hook
    37  	pullPreCheck   Hook
    38  	pulled         Hook
    39  	removePreCheck Hook
    40  	removed        Hook
    41  }
    42  
    43  // Options encapsulates runtime configuration for a remote
    44  type Options struct {
    45  	// to send & push over libp2p connections, provide a libp2p host
    46  	Libp2pHost host.Host
    47  
    48  	// called before accepting a log, returning an error cancel receiving
    49  	PushPreCheck Hook
    50  	// called after log data has been received, before it's stored in the logbook
    51  	PushFinalCheck Hook
    52  	// called after a log has been merged into the logbook
    53  	Pushed Hook
    54  	// called before a pull is accepted
    55  	PullPreCheck Hook
    56  	// called after a log is pulled
    57  	Pulled Hook
    58  	// called before removing
    59  	RemovePreCheck Hook
    60  	// called after removing
    61  	Removed Hook
    62  }
    63  
    64  // New creates a remote from a logbook and optional configuration functions
    65  func New(book *logbook.Book, opts ...func(*Options)) *Logsync {
    66  	o := &Options{}
    67  	for _, opt := range opts {
    68  		opt(o)
    69  	}
    70  
    71  	logsync := &Logsync{
    72  		book: book,
    73  
    74  		pushPreCheck:   o.PushPreCheck,
    75  		pushFinalCheck: o.PushFinalCheck,
    76  		pushed:         o.Pushed,
    77  		pullPreCheck:   o.PullPreCheck,
    78  		pulled:         o.Pulled,
    79  		removePreCheck: o.RemovePreCheck,
    80  		removed:        o.Removed,
    81  	}
    82  
    83  	if o.Libp2pHost != nil {
    84  		logsync.p2pHandler = newp2pHandler(logsync, o.Libp2pHost)
    85  	}
    86  
    87  	return logsync
    88  }
    89  
    90  // Hook is a function called at specified points in the sync lifecycle
    91  type Hook func(ctx context.Context, author profile.Author, ref dsref.Ref, l *oplog.Log) error
    92  
    93  // Author is the local author of lsync's logbook
    94  func (lsync *Logsync) Author() profile.Author {
    95  	if lsync == nil {
    96  		return nil
    97  	}
    98  	return profile.NewAuthorFromProfile(lsync.book.Owner())
    99  }
   100  
   101  // NewPush prepares a Push from the local logsync to a remote destination
   102  // doing a push places a local log on the remote
   103  func (lsync *Logsync) NewPush(ref dsref.Ref, remoteAddr string) (*Push, error) {
   104  	if lsync == nil {
   105  		return nil, ErrNoLogsync
   106  	}
   107  
   108  	rem, err := lsync.remoteClient(context.TODO(), remoteAddr)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	return &Push{
   114  		book:   lsync.book,
   115  		remote: rem,
   116  		ref:    ref,
   117  	}, nil
   118  }
   119  
   120  // NewPull creates a Pull from the local logsync to a remote destination
   121  // doing a pull fetches a log from the remote to the local logbook
   122  func (lsync *Logsync) NewPull(ref dsref.Ref, remoteAddr string) (*Pull, error) {
   123  	if lsync == nil {
   124  		return nil, ErrNoLogsync
   125  	}
   126  	log.Debugw("NewPull", "ref", ref, "remoteAddr", remoteAddr)
   127  
   128  	rem, err := lsync.remoteClient(context.TODO(), remoteAddr)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	return &Pull{
   134  		book:   lsync.book,
   135  		remote: rem,
   136  		ref:    ref,
   137  	}, nil
   138  }
   139  
   140  // DoRemove asks a remote to remove a log
   141  func (lsync *Logsync) DoRemove(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   142  	if lsync == nil {
   143  		return ErrNoLogsync
   144  	}
   145  
   146  	rem, err := lsync.remoteClient(ctx, remoteAddr)
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	if err = rem.del(ctx, lsync.Author(), ref); err != nil {
   152  		return err
   153  	}
   154  
   155  	versions, err := lsync.book.Items(ctx, ref, 0, -1, "")
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	// record remove as delete of all versions on the remote
   161  	_, _, err = lsync.book.WriteRemoteDelete(ctx, lsync.book.Owner(), ref.InitID, len(versions), remoteAddr)
   162  	return err
   163  }
   164  
   165  func (lsync *Logsync) remoteClient(ctx context.Context, remoteAddr string) (rem remote, err error) {
   166  	if strings.HasPrefix(remoteAddr, "http") {
   167  		return &httpClient{URL: remoteAddr}, nil
   168  	}
   169  
   170  	// if we're given a logbook authorId, convert it to the active public key ID
   171  	if l, err := lsync.book.Log(ctx, remoteAddr); err == nil {
   172  		remoteAddr = l.Author()
   173  	}
   174  
   175  	// if a valid base58 peerID is passed, we're doing a p2p dsync
   176  	if id, err := peer.IDB58Decode(remoteAddr); err == nil {
   177  		if lsync.p2pHandler == nil {
   178  			return nil, fmt.Errorf("no p2p host provided to perform p2p logsync")
   179  		}
   180  		return &p2pClient{remotePeerID: id, p2pHandler: lsync.p2pHandler}, nil
   181  	}
   182  	return nil, fmt.Errorf("unrecognized remote address string: %q", remoteAddr)
   183  }
   184  
   185  // remote is an internal interface for methods available on foreign logbooks
   186  // the logsync struct contains the canonical implementation of a remote
   187  // interface. network clients wrap the remote interface with network behaviours,
   188  // using Logsync methods to do the "real work" and echoing that back across the
   189  // client protocol
   190  type remote interface {
   191  	addr() string
   192  	put(ctx context.Context, author profile.Author, ref dsref.Ref, r io.Reader) error
   193  	get(ctx context.Context, author profile.Author, ref dsref.Ref) (sender profile.Author, data io.Reader, err error)
   194  	del(ctx context.Context, author profile.Author, ref dsref.Ref) error
   195  }
   196  
   197  // assert at compile-time that Logsync is a remote
   198  var _ remote = (*Logsync)(nil)
   199  
   200  // addr is only used by clients. this should never be called
   201  func (lsync *Logsync) addr() string {
   202  	panic("cannot get the address of logsync itself")
   203  }
   204  
   205  func (lsync *Logsync) put(ctx context.Context, author profile.Author, ref dsref.Ref, r io.Reader) error {
   206  	if lsync == nil {
   207  		return ErrNoLogsync
   208  	}
   209  
   210  	if lsync.pushPreCheck != nil {
   211  		if err := lsync.pushPreCheck(ctx, author, ref, nil); err != nil {
   212  			return err
   213  		}
   214  	}
   215  
   216  	data, err := ioutil.ReadAll(r)
   217  	if err != nil {
   218  		return err
   219  	}
   220  	if len(data) == 0 {
   221  		return fmt.Errorf("no data provided to merge")
   222  	}
   223  
   224  	lg := &oplog.Log{}
   225  	if err := lg.UnmarshalFlatbufferBytes(data); err != nil {
   226  		return err
   227  	}
   228  
   229  	// Get the ref that is in use within the logbook data
   230  	logRef, err := logbook.DsrefAliasForLog(lg)
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	// Validate that data in the logbook matches the ref being synced
   236  	if logRef.Username != ref.Username || logRef.Name != ref.Name || logRef.ProfileID != ref.ProfileID {
   237  		return fmt.Errorf("ref contained in log data does not match")
   238  	}
   239  
   240  	if lsync.pushFinalCheck != nil {
   241  		if err := lsync.pushFinalCheck(ctx, author, logRef, lg); err != nil {
   242  			return err
   243  		}
   244  	}
   245  
   246  	if err := lsync.book.MergeLog(ctx, author.AuthorPubKey(), lg); err != nil {
   247  		return err
   248  	}
   249  
   250  	if lsync.pushed != nil {
   251  		if err := lsync.pushed(ctx, author, ref, lg); err != nil {
   252  			log.Debugf("pushed hook error=%q", err)
   253  		}
   254  	}
   255  	return nil
   256  }
   257  
   258  func (lsync *Logsync) get(ctx context.Context, author profile.Author, ref dsref.Ref) (profile.Author, io.Reader, error) {
   259  	log.Debugf("logsync.get author.AuthorID=%q ref=%q", author.AuthorID, ref)
   260  	if lsync == nil {
   261  		return nil, nil, ErrNoLogsync
   262  	}
   263  
   264  	if lsync.pullPreCheck != nil {
   265  		if err := lsync.pullPreCheck(ctx, author, ref, nil); err != nil {
   266  			log.Debugf("pullPreCheck error=%q author=%q ref=%q", err, author, ref)
   267  			return nil, nil, err
   268  		}
   269  	}
   270  
   271  	if _, err := lsync.book.ResolveRef(ctx, &ref); err != nil {
   272  		log.Debugf("book.ResolveRef error=%q ref=%q ", err, ref)
   273  		return nil, nil, err
   274  	}
   275  
   276  	l, err := lsync.book.UserDatasetBranchesLog(ctx, ref.InitID)
   277  	if err != nil {
   278  		log.Debugf("book.UserDatasetBranchesLog error=%q initID=%q", err, ref.InitID)
   279  		return lsync.Author(), nil, err
   280  	}
   281  
   282  	data, err := lsync.book.LogBytes(l, lsync.book.Owner().PrivKey)
   283  	if err != nil {
   284  		log.Debugf("LogBytes error=%q initID=%q", err, ref.InitID)
   285  		return nil, nil, err
   286  	}
   287  
   288  	if lsync.pulled != nil {
   289  		if err := lsync.pulled(ctx, author, ref, l); err != nil {
   290  			log.Debugf("pulled hook error=%q", err)
   291  		}
   292  	}
   293  
   294  	return lsync.Author(), bytes.NewReader(data), nil
   295  }
   296  
   297  func (lsync *Logsync) del(ctx context.Context, sender profile.Author, ref dsref.Ref) error {
   298  	if lsync == nil {
   299  		return ErrNoLogsync
   300  	}
   301  
   302  	if lsync.removePreCheck != nil {
   303  		if err := lsync.removePreCheck(ctx, sender, ref, nil); err != nil {
   304  			return err
   305  		}
   306  	}
   307  
   308  	l, err := lsync.book.BranchRef(ctx, ref)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	// eventually access control will dictate which logs can be written by whom.
   314  	// For now we only allow users to delete logs they've written
   315  	// book will need access to a store of public keys before we can verify
   316  	// signatures non-same-senders
   317  	// if err := l.Verify(sender.AuthorPubKey()); err != nil {
   318  	// 	return err
   319  	// }
   320  
   321  	root := l
   322  	for {
   323  		p := root.Parent()
   324  		if p == nil {
   325  			break
   326  		}
   327  		root = p
   328  	}
   329  
   330  	if err := lsync.book.RemoveLog(ctx, ref); err != nil {
   331  		return err
   332  	}
   333  
   334  	if lsync.removed != nil {
   335  		if err := lsync.removed(ctx, sender, ref, nil); err != nil {
   336  			log.Debugf("removed hook error=%q", err)
   337  		}
   338  	}
   339  
   340  	return nil
   341  }
   342  
   343  // Push is a request to place a log on a remote
   344  type Push struct {
   345  	ref    dsref.Ref
   346  	book   *logbook.Book
   347  	remote remote
   348  }
   349  
   350  // Do executes a push
   351  func (p *Push) Do(ctx context.Context) error {
   352  	// eagerly write a push to the logbook. The log the remote receives will include
   353  	// the push operation. If anything goes wrong, rollback the write
   354  	l, rollback, err := p.book.WriteRemotePush(ctx, p.book.Owner(), p.ref.InitID, 1, p.remote.addr())
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	data, err := p.book.LogBytes(l, p.book.Owner().PrivKey)
   360  	if err != nil {
   361  		if rollbackErr := rollback(ctx); rollbackErr != nil {
   362  			log.Errorf("rolling back dataset log: %q", rollbackErr)
   363  		}
   364  		return err
   365  	}
   366  
   367  	buf := bytes.NewBuffer(data)
   368  	author := profile.NewAuthorFromProfile(p.book.Owner())
   369  	err = p.remote.put(ctx, author, p.ref, buf)
   370  	if err != nil {
   371  		if rollbackErr := rollback(ctx); rollbackErr != nil {
   372  			log.Errorf("rolling back dataset log: %q", rollbackErr)
   373  		}
   374  		return err
   375  	}
   376  	return nil
   377  }
   378  
   379  // Pull is a request to fetch a log
   380  type Pull struct {
   381  	book   *logbook.Book
   382  	ref    dsref.Ref
   383  	remote remote
   384  
   385  	// set to true to merge these logs into the local store on successful pull
   386  	Merge bool
   387  }
   388  
   389  // Do executes the pull
   390  func (p *Pull) Do(ctx context.Context) (*oplog.Log, error) {
   391  	log.Debugw("pull.Do", "ref", p.ref)
   392  	author := profile.NewAuthorFromProfile(p.book.Owner())
   393  	sender, r, err := p.remote.get(ctx, author, p.ref)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	data, err := ioutil.ReadAll(r)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	l := &oplog.Log{}
   403  	if err := l.UnmarshalFlatbufferBytes(data); err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	if p.Merge {
   408  		if err := p.book.MergeLog(ctx, sender.AuthorPubKey(), l); err != nil {
   409  			return nil, err
   410  		}
   411  	}
   412  
   413  	return l, nil
   414  }