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 }