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  }