github.com/hoffie/larasync@v0.0.0-20151025221940-0384d2bddcef/repository/clientRepository.go (about) 1 package repository 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "time" 11 12 "github.com/hoffie/larasync/helpers" 13 "github.com/hoffie/larasync/helpers/atomic" 14 "github.com/hoffie/larasync/helpers/crypto" 15 "github.com/hoffie/larasync/helpers/path" 16 "github.com/hoffie/larasync/repository/chunker" 17 "github.com/hoffie/larasync/repository/nib" 18 "github.com/hoffie/larasync/repository/tracker" 19 ) 20 21 // ClientRepository is a Repository from a client-side view; it has all the keys 22 // and a work dir (comapred to the base Repository) 23 type ClientRepository struct { 24 *Repository 25 stateConfig *StateConfig 26 nibTracker tracker.NIBTracker 27 } 28 29 // NewClient returns a new ClientRepository instance 30 func NewClient(path string) *ClientRepository { 31 repo := New(path) 32 return &ClientRepository{ 33 Repository: repo, 34 } 35 } 36 37 // NIBTracker returns the 38 func (r *ClientRepository) NIBTracker() (tracker.NIBTracker, error) { 39 if r.nibTracker == nil { 40 repo := r.Repository 41 tracker, err := tracker.NewDatabaseNIBTracker( 42 filepath.Join(repo.GetManagementDir(), "nib_tracker.db"), 43 repo.Path, 44 ) 45 if err != nil { 46 return nil, err 47 } 48 r.nibTracker = tracker 49 } 50 return r.nibTracker, nil 51 } 52 53 // StateConfig returns this repository's state config; it is currently used 54 // in client repositories only and stores things like the default server. 55 func (r *ClientRepository) StateConfig() (*StateConfig, error) { 56 if r.stateConfig != nil { 57 return r.stateConfig, nil 58 } 59 path := r.subPathFor(stateConfigFileName) 60 r.stateConfig = NewStateConfig(path) 61 err := r.stateConfig.Load() 62 if err != nil && !os.IsNotExist(err) { 63 return nil, err 64 } 65 return r.stateConfig, nil 66 } 67 68 // writeFileToChunks takes a file path and saves its contents to the 69 // storage in encrypted form with a content-addressing id. 70 func (r *ClientRepository) writeFileToChunks(path string) ([]string, error) { 71 return r.splitFileToChunks(path, r.writeCryptoContainerObject) 72 } 73 74 // getFileChunkIDs analyzes the given file and returns its content ids. 75 // This function does not write anything to disk. 76 func (r *ClientRepository) getFileChunkIDs(path string) ([]string, error) { 77 return r.splitFileToChunks(path, func(string, []byte) error { return nil }) 78 } 79 80 // splitFileToChunks takes a file path and splits its contents into chunks 81 // identified by their content ids. 82 func (r *ClientRepository) splitFileToChunks(path string, handler func(string, []byte) error) ([]string, error) { 83 chunker, err := chunker.New(path, chunkSize) 84 if err != nil { 85 return nil, err 86 } 87 defer chunker.Close() 88 var ids []string 89 for chunker.HasNext() { 90 chunk, err := chunker.Next() 91 if err != nil { 92 return nil, err 93 } 94 95 // hash for content-addressing 96 hexHash, err := r.hashChunk(chunk) 97 if err != nil { 98 return nil, err 99 } 100 101 ids = append(ids, hexHash) 102 103 err = handler(hexHash, chunk) 104 if err != nil { 105 return nil, err 106 } 107 } 108 return ids, nil 109 } 110 111 // fileToChunkIds returnes te current chunk hashes for the given path. 112 func (r *ClientRepository) fileToChunkIds(path string) ([]string, error) { 113 chunker, err := chunker.New(path, chunkSize) 114 if err != nil { 115 return nil, err 116 } 117 defer chunker.Close() 118 var ids []string 119 for chunker.HasNext() { 120 chunk, err := chunker.Next() 121 if err != nil { 122 return nil, err 123 } 124 125 // hash for content-addressing 126 hexHash, err := r.hashChunk(chunk) 127 if err != nil { 128 return nil, err 129 } 130 131 ids = append(ids, hexHash) 132 } 133 return ids, nil 134 } 135 136 // GetSigningPrivateKey exposes the signing private key as it is required 137 // in foreign packages such as api. 138 func (r *ClientRepository) GetSigningPrivateKey() ([PrivateKeySize]byte, error) { 139 return r.keys.SigningPrivateKey() 140 } 141 142 // CreateKeys handles creation of all required cryptographic keys. 143 func (r *ClientRepository) CreateKeys() error { 144 err := r.keys.CreateEncryptionKey() 145 if err != nil { 146 return err 147 } 148 149 err = r.keys.CreateSigningKey() 150 if err != nil { 151 return err 152 } 153 154 err = r.keys.CreateHashingKey() 155 if err != nil { 156 return err 157 } 158 159 return nil 160 } 161 162 // encryptWithRandomKey takes a piece of data, encrypts it with a random 163 // key and returns the result, prefixed by the random key encrypted by 164 // the repository encryption key. 165 func (r *ClientRepository) encryptWithRandomKey(data []byte) ([]byte, error) { 166 encryptionKey, err := r.keys.EncryptionKey() 167 if err != nil { 168 return nil, err 169 } 170 cryptoBox := crypto.NewBox(encryptionKey) 171 return cryptoBox.EncryptWithRandomKey(data) 172 } 173 174 // hashChunk takes a chunk of data and constructs its content-addressing 175 // hash. 176 func (r *ClientRepository) hashChunk(chunk []byte) (string, error) { 177 key, err := r.keys.HashingKey() 178 if err != nil { 179 return "", err 180 } 181 hasher := crypto.NewHasher(key) 182 return hasher.StringHash(chunk), nil 183 } 184 185 // writeCryptoContainerObject takes a piece of raw data and 186 // writes it to the object store in encrypted form. 187 func (r *ClientRepository) writeCryptoContainerObject(id string, data []byte) error { 188 // PERFORMANCE: avoid re-writing pre-existing metadata files by checking for 189 // existance first. 190 var enc []byte 191 enc, err := r.encryptWithRandomKey(data) 192 if err != nil { 193 return err 194 } 195 196 err = r.AddObject(id, bytes.NewReader(enc)) 197 if err != nil { 198 return err 199 } 200 201 return nil 202 } 203 204 // readEncryptedObject reads the object with the given id and returns its 205 // authenticated, unencrypted content. 206 func (r *ClientRepository) readEncryptedObject(id string) ([]byte, error) { 207 reader, err := r.objectStorage.Get(id) 208 if err != nil { 209 return nil, err 210 } 211 defer reader.Close() 212 encryptedContent, err := ioutil.ReadAll(reader) 213 if err != nil { 214 return nil, err 215 } 216 return r.decryptContent(encryptedContent) 217 } 218 219 // decryptContent is the counter-part of encryptWithRandomKey, i.e. 220 // it returns the plain text again. 221 func (r *ClientRepository) decryptContent(enc []byte) ([]byte, error) { 222 encryptionKey, err := r.keys.EncryptionKey() 223 if err != nil { 224 return nil, err 225 } 226 cryptoBox := crypto.NewBox(encryptionKey) 227 return cryptoBox.DecryptContent(enc) 228 } 229 230 // writeMetadata writes the metadata object for the given path 231 // to disk and returns its id. 232 func (r *ClientRepository) writeMetadata(absPath string) (string, error) { 233 relPath, err := r.getRepoRelativePath(absPath) 234 if err != nil { 235 return "", err 236 } 237 m := Metadata{ 238 RepoRelativePath: relPath, 239 Type: MetadataTypeFile, 240 } 241 raw := &bytes.Buffer{} 242 _, err = m.WriteTo(raw) 243 if err != nil { 244 return "", err 245 } 246 247 rawBytes := raw.Bytes() 248 249 hexHash, err := r.hashChunk(rawBytes) 250 if err != nil { 251 return "", err 252 } 253 254 err = r.writeCryptoContainerObject(hexHash, rawBytes) 255 if err != nil { 256 return "", err 257 } 258 return hexHash, nil 259 } 260 261 // pathToNIBID returns the NIB ID for the given relative path 262 func (r *ClientRepository) pathToNIBID(relPath string) (string, error) { 263 return r.hashChunk([]byte(relPath)) 264 } 265 266 // metadataByID returns the metadata object identified by the given object id. 267 func (r *ClientRepository) metadataByID(id string) (*Metadata, error) { 268 rawMetadata, err := r.readEncryptedObject(id) 269 if err != nil { 270 return nil, err 271 } 272 273 metadata := &Metadata{} 274 _, err = metadata.ReadFrom(bytes.NewReader(rawMetadata)) 275 if err != nil { 276 return nil, err 277 } 278 279 return metadata, nil 280 } 281 282 // CheckoutPath looks up the given path name in the internal repository state and 283 // writes the content from the repository state to the path in the working directory, 284 // possibly overwriting an existing version of the file. 285 func (r *ClientRepository) CheckoutPath(absPath string) error { 286 relPath, err := r.getRepoRelativePath(absPath) 287 if err != nil { 288 return err 289 } 290 291 id, err := r.pathToNIBID(relPath) 292 if err != nil { 293 return err 294 } 295 296 nibStore := r.nibStore 297 298 // nibStore.Get also handles signature verification 299 nib, err := nibStore.Get(id) 300 if err != nil { 301 return err 302 } 303 304 return r.checkoutNIB(nib) 305 } 306 307 // CheckoutAllPaths checks out all tracked paths. 308 func (r *ClientRepository) CheckoutAllPaths() error { 309 nibStore := r.nibStore 310 nibs, err := nibStore.GetAll() 311 if err != nil { 312 return err 313 } 314 for nib := range nibs { 315 err = r.checkoutNIB(nib) 316 if err != nil { 317 return err 318 } 319 } 320 return nil 321 } 322 323 // pathHasConflictingChanges checks whether the item pointed to by absPath has any 324 // changes not resolvable to a revision in the given NIB. 325 func (r *ClientRepository) pathHasConflictingChanges(nib *nib.NIB, absPath string) (bool, error) { 326 workdirContentIDs, err := r.getFileChunkIDs(absPath) 327 if os.IsNotExist(err) { 328 return false, nil 329 } 330 if err != nil { 331 return false, err 332 } 333 334 _, err = nib.LatestRevisionWithContent(workdirContentIDs) 335 return err != nil, nil 336 } 337 338 // checkoutNIB checks out the provided NIB's latest revision into the working directory. 339 func (r *ClientRepository) checkoutNIB(nib *nib.NIB) error { 340 rev, err := nib.LatestRevision() 341 if err != nil { 342 return err 343 } 344 345 return r.checkoutRevision(nib, rev) 346 } 347 348 // checkoutRevision checks out the provided Revision into the working directory. 349 func (r *ClientRepository) checkoutRevision(nib *nib.NIB, rev *nib.Revision) error { 350 metadata, err := r.metadataByID(rev.MetadataID) 351 if err != nil { 352 return err 353 } 354 355 relPath := metadata.RepoRelativePath 356 if relPath == "" { 357 return errors.New("metadata lacks path") 358 } 359 absPath := filepath.Join(r.Path, relPath) 360 361 targetDir := filepath.Dir(absPath) 362 363 err = os.MkdirAll(targetDir, defaultDirPerms) 364 if err != nil && !os.IsExist(err) { 365 return err 366 } 367 err = nil 368 369 if len(rev.ContentIDs) > 0 { 370 writer, err := atomic.NewWriter(absPath, ".lara.checkout.", defaultFilePerms) 371 defer writer.Close() 372 if err != nil { 373 writer.Abort() 374 return err 375 } 376 377 for _, contentID := range rev.ContentIDs { 378 content, err := r.readEncryptedObject(contentID) 379 _, err = writer.Write(content) 380 if err != nil { 381 writer.Abort() 382 return err 383 } 384 } 385 386 hasChanges, err := r.pathHasConflictingChanges(nib, absPath) 387 if err != nil { 388 writer.Abort() 389 return err 390 } 391 if hasChanges { 392 writer.Abort() 393 return ErrWorkDirConflict 394 } 395 } else if _, errExistCheck := os.Stat(absPath); errExistCheck == nil { 396 err = os.Remove(absPath) 397 } 398 399 return err 400 } 401 402 // AddItem adds a new file or directory to the repository. 403 func (r *ClientRepository) AddItem(absPath string) error { 404 stat, err := os.Stat(absPath) 405 if err != nil { 406 return err 407 } 408 isBelow, err := path.IsBelow(absPath, filepath.Join(r.Path, managementDirName)) 409 if err != nil { 410 return nil 411 } 412 if isBelow { 413 return ErrRefusingWorkOnDotLara 414 } 415 if stat.IsDir() { 416 return r.addDirectory(absPath) 417 } 418 return r.addFile(absPath) 419 } 420 421 // addFile adds the given file from the working directory 422 // to the repository 423 func (r *ClientRepository) addFile(absPath string) error { 424 metadataID, err := r.writeMetadata(absPath) 425 if err != nil { 426 return err 427 } 428 429 contentIDs, err := r.writeFileToChunks(absPath) 430 if err != nil { 431 return err 432 } 433 434 relPath, err := r.getRepoRelativePath(absPath) 435 if err != nil { 436 return err 437 } 438 nibID, err := r.pathToNIBID(relPath) 439 if err != nil { 440 return err 441 } 442 443 nibStore := r.nibStore 444 445 n := &nib.NIB{ID: nibID} 446 if nibStore.Exists(nibID) { 447 n, err = nibStore.Get(nibID) 448 if err != nil { 449 return err 450 } 451 } 452 453 rev := &nib.Revision{} 454 rev.MetadataID = metadataID 455 rev.ContentIDs = contentIDs 456 rev.UTCTimestamp = time.Now().UTC().Unix() 457 //FIXME: deviceID etc. 458 latestRev, err := n.LatestRevision() 459 if err != nil && err != nib.ErrNoRevision { 460 return err 461 } 462 if err == nib.ErrNoRevision || !latestRev.HasSameContent(rev) { 463 n.AppendRevision(rev) 464 } 465 err = r.notifyNIBTracker(nibID, relPath) 466 if err != nil { 467 return err 468 } 469 470 return nibStore.Add(n) 471 } 472 473 // notifyNIBTracker adds the passed relative path to the NIBTracker of 474 // this client repository. 475 func (r *ClientRepository) notifyNIBTracker(nibID string, relPath string) error { 476 tracker, err := r.NIBTracker() 477 if err != nil { 478 return err 479 } 480 return tracker.Add(relPath, nibID) 481 } 482 483 // addDirectory walks the given directory and calls AddItem on each entry 484 func (r *ClientRepository) addDirectory(absPath string) error { 485 files, err := ioutil.ReadDir(absPath) 486 if err != nil { 487 return err 488 } 489 for _, file := range files { 490 path := filepath.Join(absPath, file.Name()) 491 err = r.AddItem(path) 492 if err == ErrRefusingWorkOnDotLara { 493 continue 494 } else if err != nil { 495 return err 496 } 497 } 498 return nil 499 } 500 501 // DeleteItem removes the given item with the passed absolute path. 502 func (r *ClientRepository) DeleteItem(absPath string) error { 503 relPath, err := r.getRepoRelativePath(absPath) 504 if err != nil { 505 return err 506 } 507 508 nibID, err := r.pathToNIBID(relPath) 509 if err != nil { 510 return err 511 } 512 513 nib, err := r.nibStore.Get(nibID) 514 if os.IsNotExist(err) { 515 return r.deleteDirectory(absPath) 516 } else if err != nil { 517 return err 518 } 519 rev, err := nib.LatestRevision() 520 if err != nil { 521 return err 522 } 523 524 if !rev.IsDeletion() { 525 err = r.deleteFile(absPath) 526 } else { 527 err = r.deleteDirectory(absPath) 528 } 529 return err 530 } 531 532 // deleteDirectory checks the ClientRepositories path lookup and removes all 533 // files and directories in the given path. 534 func (r *ClientRepository) deleteDirectory(absPath string) error { 535 relPath, err := r.getRepoRelativePath(absPath) 536 if err != nil { 537 return err 538 } 539 paths, err := r.nibTracker.SearchPrefix(relPath) 540 if err != nil { 541 return err 542 } 543 544 for _, path := range paths { 545 err = r.DeleteItem(path.AbsPath()) 546 if err != nil { 547 return err 548 } 549 } 550 551 return path.CleanUpEmptyDirs(absPath) 552 } 553 554 // deleteFile removes the specific file from the repository. Returns an error 555 // if the file does not exist in the repository. 556 func (r *ClientRepository) deleteFile(absPath string) error { 557 relPath, err := r.getRepoRelativePath(absPath) 558 if err != nil { 559 return err 560 } 561 562 nibID, err := r.pathToNIBID(relPath) 563 if err != nil { 564 return err 565 } 566 567 nibItem, err := r.nibStore.Get(nibID) 568 if err != nil { 569 return err 570 } 571 572 latestRevision, err := nibItem.LatestRevision() 573 if err != nil && err != nib.ErrNoRevision { 574 return err 575 } 576 577 deleteFileIfExisting := func() error { 578 if r.revisionIsFile(absPath, latestRevision) && !latestRevision.IsDeletion() { 579 os.Remove(absPath) 580 } 581 582 stat, fileErr := os.Stat(absPath) 583 if fileErr != nil { 584 return nil 585 } 586 587 if !stat.IsDir() && latestRevision != nil { 588 ids, err := r.fileToChunkIds(absPath) 589 if err != nil { 590 return err 591 } 592 if helpers.StringsEqual(ids, latestRevision.ContentIDs) { 593 return os.Remove(absPath) 594 } 595 } 596 return nil 597 } 598 599 if err == nil && latestRevision != nil { 600 if latestRevision.IsDeletion() { 601 return deleteFileIfExisting() 602 } 603 deleteRevision := latestRevision.Clone() 604 deleteRevision.ContentIDs = []string{} 605 nibItem.AppendRevision(deleteRevision) 606 err = r.nibStore.Add(nibItem) 607 if err != nil { 608 return err 609 } 610 } 611 612 return deleteFileIfExisting() 613 } 614 615 // revisionIsFile returns if the given revision is represented by the passed revision. 616 func (r *ClientRepository) revisionIsFile(absPath string, rev *nib.Revision) bool { 617 stat, err := os.Stat(absPath) 618 if rev == nil || (err == nil && stat.IsDir()) { 619 return false 620 } 621 622 if os.IsNotExist(err) { 623 if rev.IsDeletion() { 624 return true 625 } 626 return false 627 } 628 629 ids, err := r.fileToChunkIds(absPath) 630 if err != nil { 631 return false 632 } 633 634 return helpers.StringsEqual(ids, rev.ContentIDs) 635 } 636 637 // SetAuthorization adds a authorization with the given publicKey and encrypts it with the 638 // passed encryptionKey to this repository. 639 func (r *ClientRepository) SetAuthorization( 640 publicKey [PublicKeySize]byte, 641 encKey [EncryptionKeySize]byte, 642 authorization *Authorization, 643 ) error { 644 return r.authorizationManager.Set(publicKey, encKey, authorization) 645 } 646 647 // NewAuthorization returns the currently valid Authorization object 648 // for this repository. If the privateKeys necessary for this are not 649 // stored in the keyStore an error is returned. 650 func (r *ClientRepository) NewAuthorization() (*Authorization, error) { 651 encryptionKey, err := r.keys.EncryptionKey() 652 if err != nil { 653 return nil, errors.New("Could not load encryption key.") 654 } 655 656 hashingKey, err := r.keys.HashingKey() 657 if err != nil { 658 return nil, errors.New("Could not load hashing key.") 659 } 660 661 signatureKey, err := r.keys.SigningPrivateKey() 662 if err != nil { 663 return nil, errors.New("Could not load private signing key.") 664 } 665 666 auth := &Authorization{ 667 EncryptionKey: encryptionKey, 668 HashingKey: hashingKey, 669 SigningKey: signatureKey, 670 } 671 672 return auth, nil 673 } 674 675 // SerializedAuthorization returns a new, serialized authorization package. 676 func (r *ClientRepository) SerializedAuthorization(encryptionKey [EncryptionKeySize]byte) ([]byte, error) { 677 auth, err := r.NewAuthorization() 678 if err != nil { 679 return nil, fmt.Errorf("authorization creation error (%s)", err) 680 } 681 682 authorizationBytes, err := r.SerializeAuthorization(encryptionKey, auth) 683 if err != nil { 684 return nil, fmt.Errorf("authorization encryption failure (%s)", err) 685 } 686 return authorizationBytes, nil 687 } 688 689 // TransactionsFrom returns all transactions which have been added since the given transactionID. 690 func (r *ClientRepository) TransactionsFrom(transactionID int64) ([]*Transaction, error) { 691 return r.transactionManager.From(transactionID) 692 }