github.com/kayoticsully/syncthing@v0.8.9-0.20140724133906-c45a2fdc03f8/model/puller.go (about)

     1  // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
     2  // All rights reserved. Use of this source code is governed by an MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package model
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"os"
    11  	"path/filepath"
    12  	"time"
    13  
    14  	"github.com/calmh/syncthing/config"
    15  	"github.com/calmh/syncthing/events"
    16  	"github.com/calmh/syncthing/osutil"
    17  	"github.com/calmh/syncthing/protocol"
    18  	"github.com/calmh/syncthing/scanner"
    19  	"github.com/calmh/syncthing/versioner"
    20  )
    21  
    22  type requestResult struct {
    23  	node     protocol.NodeID
    24  	file     protocol.FileInfo
    25  	filepath string // full filepath name
    26  	offset   int64
    27  	data     []byte
    28  	err      error
    29  }
    30  
    31  type openFile struct {
    32  	filepath     string // full filepath name
    33  	temp         string // temporary filename
    34  	availability []protocol.NodeID
    35  	file         *os.File
    36  	err          error // error when opening or writing to file, all following operations are cancelled
    37  	outstanding  int   // number of requests we still have outstanding
    38  	done         bool  // we have sent all requests for this file
    39  }
    40  
    41  type activityMap map[protocol.NodeID]int
    42  
    43  func (m activityMap) leastBusyNode(availability []protocol.NodeID, isValid func(protocol.NodeID) bool) protocol.NodeID {
    44  	var low int = 2<<30 - 1
    45  	var selected protocol.NodeID
    46  	for _, node := range availability {
    47  		usage := m[node]
    48  		if usage < low && isValid(node) {
    49  			low = usage
    50  			selected = node
    51  		}
    52  	}
    53  	m[selected]++
    54  	return selected
    55  }
    56  
    57  func (m activityMap) decrease(node protocol.NodeID) {
    58  	m[node]--
    59  }
    60  
    61  var errNoNode = errors.New("no available source node")
    62  
    63  type puller struct {
    64  	cfg               *config.Configuration
    65  	repoCfg           config.RepositoryConfiguration
    66  	bq                blockQueue
    67  	slots             int
    68  	model             *Model
    69  	oustandingPerNode activityMap
    70  	openFiles         map[string]openFile
    71  	requestSlots      chan bool
    72  	blocks            chan bqBlock
    73  	requestResults    chan requestResult
    74  	versioner         versioner.Versioner
    75  }
    76  
    77  func newPuller(repoCfg config.RepositoryConfiguration, model *Model, slots int, cfg *config.Configuration) *puller {
    78  	p := &puller{
    79  		cfg:               cfg,
    80  		repoCfg:           repoCfg,
    81  		slots:             slots,
    82  		model:             model,
    83  		oustandingPerNode: make(activityMap),
    84  		openFiles:         make(map[string]openFile),
    85  		requestSlots:      make(chan bool, slots),
    86  		blocks:            make(chan bqBlock),
    87  		requestResults:    make(chan requestResult),
    88  	}
    89  
    90  	if len(repoCfg.Versioning.Type) > 0 {
    91  		factory, ok := versioner.Factories[repoCfg.Versioning.Type]
    92  		if !ok {
    93  			l.Fatalf("Requested versioning type %q that does not exist", repoCfg.Versioning.Type)
    94  		}
    95  		p.versioner = factory(repoCfg.Versioning.Params)
    96  	}
    97  
    98  	if slots > 0 {
    99  		// Read/write
   100  		if debug {
   101  			l.Debugf("starting puller; repo %q dir %q slots %d", repoCfg.ID, repoCfg.Directory, slots)
   102  		}
   103  		go p.run()
   104  	} else {
   105  		// Read only
   106  		if debug {
   107  			l.Debugf("starting puller; repo %q dir %q (read only)", repoCfg.ID, repoCfg.Directory)
   108  		}
   109  		go p.runRO()
   110  	}
   111  	return p
   112  }
   113  
   114  func (p *puller) run() {
   115  	changed := true
   116  	scanintv := time.Duration(p.cfg.Options.RescanIntervalS) * time.Second
   117  	lastscan := time.Now()
   118  	var prevVer uint64
   119  	var queued int
   120  
   121  	// Load up the request slots
   122  	for i := 0; i < cap(p.requestSlots); i++ {
   123  		p.requestSlots <- true
   124  	}
   125  
   126  	for {
   127  		// Run the pulling loop as long as there are blocks to fetch
   128  
   129  		prevVer, queued = p.queueNeededBlocks(prevVer)
   130  		if queued > 0 {
   131  
   132  		pull:
   133  			for {
   134  				select {
   135  				case res := <-p.requestResults:
   136  					p.model.setState(p.repoCfg.ID, RepoSyncing)
   137  					changed = true
   138  					p.requestSlots <- true
   139  					p.handleRequestResult(res)
   140  
   141  				case <-p.requestSlots:
   142  					b, ok := p.bq.get()
   143  
   144  					if !ok {
   145  						if debug {
   146  							l.Debugf("%q: pulling loop needs more blocks", p.repoCfg.ID)
   147  						}
   148  						prevVer, _ = p.queueNeededBlocks(prevVer)
   149  						b, ok = p.bq.get()
   150  					}
   151  
   152  					if !ok && len(p.openFiles) == 0 {
   153  						// Nothing queued, nothing outstanding
   154  						if debug {
   155  							l.Debugf("%q: pulling loop done", p.repoCfg.ID)
   156  						}
   157  						break pull
   158  					}
   159  
   160  					if !ok {
   161  						// Nothing queued, but there are still open files.
   162  						// Give the situation a moment to change.
   163  						if debug {
   164  							l.Debugf("%q: pulling loop paused", p.repoCfg.ID)
   165  						}
   166  						p.requestSlots <- true
   167  						time.Sleep(100 * time.Millisecond)
   168  						continue pull
   169  					}
   170  
   171  					if debug {
   172  						l.Debugf("queueing %q / %q offset %d copy %d", p.repoCfg.ID, b.file.Name, b.block.Offset, len(b.copy))
   173  					}
   174  					p.model.setState(p.repoCfg.ID, RepoSyncing)
   175  					changed = true
   176  					if p.handleBlock(b) {
   177  						// Block was fully handled, free up the slot
   178  						p.requestSlots <- true
   179  					}
   180  				}
   181  			}
   182  		}
   183  
   184  		if changed {
   185  			p.model.setState(p.repoCfg.ID, RepoCleaning)
   186  			p.fixupDirectories()
   187  			changed = false
   188  		}
   189  
   190  		p.model.setState(p.repoCfg.ID, RepoIdle)
   191  
   192  		// Do a rescan if it's time for it
   193  		if time.Since(lastscan) > scanintv {
   194  			if debug {
   195  				l.Debugf("%q: time for rescan", p.repoCfg.ID)
   196  			}
   197  
   198  			err := p.model.ScanRepo(p.repoCfg.ID)
   199  			if err != nil {
   200  				invalidateRepo(p.cfg, p.repoCfg.ID, err)
   201  				return
   202  			}
   203  			lastscan = time.Now()
   204  		}
   205  
   206  		time.Sleep(5 * time.Second)
   207  	}
   208  }
   209  
   210  func (p *puller) runRO() {
   211  	walkTicker := time.Tick(time.Duration(p.cfg.Options.RescanIntervalS) * time.Second)
   212  
   213  	for _ = range walkTicker {
   214  		if debug {
   215  			l.Debugf("%q: time for rescan", p.repoCfg.ID)
   216  		}
   217  		err := p.model.ScanRepo(p.repoCfg.ID)
   218  		if err != nil {
   219  			invalidateRepo(p.cfg, p.repoCfg.ID, err)
   220  			return
   221  		}
   222  	}
   223  }
   224  
   225  func (p *puller) fixupDirectories() {
   226  	var deleteDirs []string
   227  	var changed = 0
   228  
   229  	var walkFn = func(path string, info os.FileInfo, err error) error {
   230  		if err != nil {
   231  			return err
   232  		}
   233  
   234  		if !info.IsDir() {
   235  			return nil
   236  		}
   237  
   238  		rn, err := filepath.Rel(p.repoCfg.Directory, path)
   239  		if err != nil {
   240  			return nil
   241  		}
   242  
   243  		if rn == "." {
   244  			return nil
   245  		}
   246  
   247  		if filepath.Base(rn) == ".stversions" {
   248  			return filepath.SkipDir
   249  		}
   250  
   251  		cur := p.model.CurrentRepoFile(p.repoCfg.ID, rn)
   252  		if cur.Name != rn {
   253  			// No matching dir in current list; weird
   254  			if debug {
   255  				l.Debugf("missing dir: %s; %v", rn, cur)
   256  			}
   257  			return nil
   258  		}
   259  
   260  		if protocol.IsDeleted(cur.Flags) {
   261  			if debug {
   262  				l.Debugf("queue delete dir: %v", cur)
   263  			}
   264  
   265  			// We queue the directories to delete since we walk the
   266  			// tree in depth first order and need to remove the
   267  			// directories in the opposite order.
   268  
   269  			deleteDirs = append(deleteDirs, path)
   270  			return nil
   271  		}
   272  
   273  		if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(cur.Flags) && !scanner.PermsEqual(cur.Flags, uint32(info.Mode())) {
   274  			err := os.Chmod(path, os.FileMode(cur.Flags)&os.ModePerm)
   275  			if err != nil {
   276  				l.Warnf("Restoring folder flags: %q: %v", path, err)
   277  			} else {
   278  				changed++
   279  				if debug {
   280  					l.Debugf("restored dir flags: %o -> %v", info.Mode()&os.ModePerm, cur)
   281  				}
   282  			}
   283  		}
   284  
   285  		return nil
   286  	}
   287  
   288  	for {
   289  		deleteDirs = nil
   290  		changed = 0
   291  		filepath.Walk(p.repoCfg.Directory, walkFn)
   292  
   293  		var deleted = 0
   294  		// Delete any queued directories
   295  		for i := len(deleteDirs) - 1; i >= 0; i-- {
   296  			dir := deleteDirs[i]
   297  			if debug {
   298  				l.Debugln("delete dir:", dir)
   299  			}
   300  			err := os.Remove(dir)
   301  			if err == nil {
   302  				deleted++
   303  			} else {
   304  				l.Warnln("Delete dir:", err)
   305  			}
   306  		}
   307  
   308  		if debug {
   309  			l.Debugf("changed %d, deleted %d dirs", changed, deleted)
   310  		}
   311  
   312  		if changed+deleted == 0 {
   313  			return
   314  		}
   315  	}
   316  }
   317  
   318  func (p *puller) handleRequestResult(res requestResult) {
   319  	p.oustandingPerNode.decrease(res.node)
   320  	f := res.file
   321  
   322  	of, ok := p.openFiles[f.Name]
   323  	if !ok {
   324  		// no entry in openFiles means there was an error and we've cancelled the operation
   325  		return
   326  	}
   327  
   328  	if res.err != nil {
   329  		// This request resulted in an error
   330  		of.err = res.err
   331  		if debug {
   332  			l.Debugf("pull: not writing %q / %q offset %d: %v; (done=%v, outstanding=%d)", p.repoCfg.ID, f.Name, res.offset, res.err, of.done, of.outstanding)
   333  		}
   334  	} else if of.err == nil {
   335  		// This request was sucessfull and nothing has failed previously either
   336  		_, of.err = of.file.WriteAt(res.data, res.offset)
   337  		if debug {
   338  			l.Debugf("pull: wrote %q / %q offset %d len %d outstanding %d done %v", p.repoCfg.ID, f.Name, res.offset, len(res.data), of.outstanding, of.done)
   339  		}
   340  	}
   341  
   342  	of.outstanding--
   343  	p.openFiles[f.Name] = of
   344  
   345  	if of.done && of.outstanding == 0 {
   346  		p.closeFile(f)
   347  	}
   348  }
   349  
   350  // handleBlock fulfills the block request by copying, ignoring or fetching
   351  // from the network. Returns true if the block was fully handled
   352  // synchronously, i.e. if the slot can be reused.
   353  func (p *puller) handleBlock(b bqBlock) bool {
   354  	f := b.file
   355  
   356  	// For directories, making sure they exist is enough.
   357  	// Deleted directories we mark as handled and delete later.
   358  	if protocol.IsDirectory(f.Flags) {
   359  		if !protocol.IsDeleted(f.Flags) {
   360  			path := filepath.Join(p.repoCfg.Directory, f.Name)
   361  			_, err := os.Stat(path)
   362  			if err != nil && os.IsNotExist(err) {
   363  				if debug {
   364  					l.Debugf("create dir: %v", f)
   365  				}
   366  				err = os.MkdirAll(path, os.FileMode(f.Flags&0777))
   367  				if err != nil {
   368  					l.Warnf("Create folder: %q: %v", path, err)
   369  				}
   370  			}
   371  		} else if debug {
   372  			l.Debugf("ignore delete dir: %v", f)
   373  		}
   374  		p.model.updateLocal(p.repoCfg.ID, f)
   375  		return true
   376  	}
   377  
   378  	if len(b.copy) > 0 && len(b.copy) == len(b.file.Blocks) && b.last {
   379  		// We are supposed to copy the entire file, and then fetch nothing.
   380  		// We don't actually need to make the copy.
   381  		if debug {
   382  			l.Debugln("taking shortcut:", f)
   383  		}
   384  		fp := filepath.Join(p.repoCfg.Directory, f.Name)
   385  		t := time.Unix(f.Modified, 0)
   386  		err := os.Chtimes(fp, t, t)
   387  		if debug && err != nil {
   388  			l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   389  		}
   390  		if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
   391  			err = os.Chmod(fp, os.FileMode(f.Flags&0777))
   392  			if debug && err != nil {
   393  				l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   394  			}
   395  		}
   396  
   397  		events.Default.Log(events.ItemStarted, map[string]string{
   398  			"repo": p.repoCfg.ID,
   399  			"item": f.Name,
   400  		})
   401  
   402  		p.model.updateLocal(p.repoCfg.ID, f)
   403  		return true
   404  	}
   405  
   406  	of, ok := p.openFiles[f.Name]
   407  	of.done = b.last
   408  
   409  	if !ok {
   410  		if debug {
   411  			l.Debugf("pull: %q: opening file %q", p.repoCfg.ID, f.Name)
   412  		}
   413  
   414  		events.Default.Log(events.ItemStarted, map[string]string{
   415  			"repo": p.repoCfg.ID,
   416  			"item": f.Name,
   417  		})
   418  
   419  		of.availability = p.model.repoFiles[p.repoCfg.ID].Availability(f.Name)
   420  		of.filepath = filepath.Join(p.repoCfg.Directory, f.Name)
   421  		of.temp = filepath.Join(p.repoCfg.Directory, defTempNamer.TempName(f.Name))
   422  
   423  		dirName := filepath.Dir(of.filepath)
   424  		_, err := os.Stat(dirName)
   425  		if err != nil {
   426  			err = os.MkdirAll(dirName, 0777)
   427  		}
   428  		if err != nil {
   429  			l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   430  		}
   431  
   432  		of.file, of.err = os.Create(of.temp)
   433  		if of.err != nil {
   434  			if debug {
   435  				l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
   436  			}
   437  			if !b.last {
   438  				p.openFiles[f.Name] = of
   439  			}
   440  			return true
   441  		}
   442  		osutil.HideFile(of.temp)
   443  	}
   444  
   445  	if of.err != nil {
   446  		// We have already failed this file.
   447  		if debug {
   448  			l.Debugf("pull: error: %q / %q has already failed: %v", p.repoCfg.ID, f.Name, of.err)
   449  		}
   450  		if b.last {
   451  			delete(p.openFiles, f.Name)
   452  		}
   453  
   454  		return true
   455  	}
   456  
   457  	p.openFiles[f.Name] = of
   458  
   459  	switch {
   460  	case len(b.copy) > 0:
   461  		p.handleCopyBlock(b)
   462  		return true
   463  
   464  	case b.block.Size > 0:
   465  		return p.handleRequestBlock(b)
   466  
   467  	default:
   468  		p.handleEmptyBlock(b)
   469  		return true
   470  	}
   471  }
   472  
   473  func (p *puller) handleCopyBlock(b bqBlock) {
   474  	// We have blocks to copy from the existing file
   475  	f := b.file
   476  	of := p.openFiles[f.Name]
   477  
   478  	if debug {
   479  		l.Debugf("pull: copying %d blocks for %q / %q", len(b.copy), p.repoCfg.ID, f.Name)
   480  	}
   481  
   482  	var exfd *os.File
   483  	exfd, of.err = os.Open(of.filepath)
   484  	if of.err != nil {
   485  		if debug {
   486  			l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
   487  		}
   488  		of.file.Close()
   489  		of.file = nil
   490  
   491  		p.openFiles[f.Name] = of
   492  		return
   493  	}
   494  	defer exfd.Close()
   495  
   496  	for _, b := range b.copy {
   497  		bs := make([]byte, b.Size)
   498  		_, of.err = exfd.ReadAt(bs, b.Offset)
   499  		if of.err == nil {
   500  			_, of.err = of.file.WriteAt(bs, b.Offset)
   501  		}
   502  		if of.err != nil {
   503  			if debug {
   504  				l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
   505  			}
   506  			exfd.Close()
   507  			of.file.Close()
   508  			of.file = nil
   509  
   510  			p.openFiles[f.Name] = of
   511  			return
   512  		}
   513  	}
   514  }
   515  
   516  // handleRequestBlock tries to pull a block from the network. Returns true if
   517  // the block could _not_ be fetched (i.e. it was fully handled, matching the
   518  // return criteria of handleBlock)
   519  func (p *puller) handleRequestBlock(b bqBlock) bool {
   520  	f := b.file
   521  	of, ok := p.openFiles[f.Name]
   522  	if !ok {
   523  		panic("bug: request for non-open file")
   524  	}
   525  
   526  	node := p.oustandingPerNode.leastBusyNode(of.availability, p.model.ConnectedTo)
   527  	if node == (protocol.NodeID{}) {
   528  		of.err = errNoNode
   529  		if of.file != nil {
   530  			of.file.Close()
   531  			of.file = nil
   532  			os.Remove(of.temp)
   533  			if debug {
   534  				l.Debugf("pull: no source for %q / %q; closed", p.repoCfg.ID, f.Name)
   535  			}
   536  		}
   537  		if b.last {
   538  			if debug {
   539  				l.Debugf("pull: no source for %q / %q; deleting", p.repoCfg.ID, f.Name)
   540  			}
   541  			delete(p.openFiles, f.Name)
   542  		} else {
   543  			if debug {
   544  				l.Debugf("pull: no source for %q / %q; await more blocks", p.repoCfg.ID, f.Name)
   545  			}
   546  			p.openFiles[f.Name] = of
   547  		}
   548  		return true
   549  	}
   550  
   551  	of.outstanding++
   552  	p.openFiles[f.Name] = of
   553  
   554  	go func(node protocol.NodeID, b bqBlock) {
   555  		if debug {
   556  			l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repoCfg.ID, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
   557  		}
   558  
   559  		bs, err := p.model.requestGlobal(node, p.repoCfg.ID, f.Name, b.block.Offset, int(b.block.Size), nil)
   560  		p.requestResults <- requestResult{
   561  			node:     node,
   562  			file:     f,
   563  			filepath: of.filepath,
   564  			offset:   b.block.Offset,
   565  			data:     bs,
   566  			err:      err,
   567  		}
   568  	}(node, b)
   569  
   570  	return false
   571  }
   572  
   573  func (p *puller) handleEmptyBlock(b bqBlock) {
   574  	f := b.file
   575  	of := p.openFiles[f.Name]
   576  
   577  	if b.last {
   578  		if of.err == nil {
   579  			of.file.Close()
   580  		}
   581  	}
   582  
   583  	if protocol.IsDeleted(f.Flags) {
   584  		if debug {
   585  			l.Debugf("pull: delete %q", f.Name)
   586  		}
   587  		os.Remove(of.temp)
   588  		os.Chmod(of.filepath, 0666)
   589  		if p.versioner != nil {
   590  			if debug {
   591  				l.Debugln("pull: deleting with versioner")
   592  			}
   593  			if err := p.versioner.Archive(p.repoCfg.Directory, of.filepath); err == nil {
   594  				p.model.updateLocal(p.repoCfg.ID, f)
   595  			} else if debug {
   596  				l.Debugln("pull: error:", err)
   597  			}
   598  		} else if err := os.Remove(of.filepath); err == nil || os.IsNotExist(err) {
   599  			p.model.updateLocal(p.repoCfg.ID, f)
   600  		}
   601  	} else {
   602  		if debug {
   603  			l.Debugf("pull: no blocks to fetch and nothing to copy for %q / %q", p.repoCfg.ID, f.Name)
   604  		}
   605  		t := time.Unix(f.Modified, 0)
   606  		if os.Chtimes(of.temp, t, t) != nil {
   607  			delete(p.openFiles, f.Name)
   608  			return
   609  		}
   610  		if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) && os.Chmod(of.temp, os.FileMode(f.Flags&0777)) != nil {
   611  			delete(p.openFiles, f.Name)
   612  			return
   613  		}
   614  		osutil.ShowFile(of.temp)
   615  		if osutil.Rename(of.temp, of.filepath) == nil {
   616  			p.model.updateLocal(p.repoCfg.ID, f)
   617  		}
   618  	}
   619  	delete(p.openFiles, f.Name)
   620  }
   621  
   622  func (p *puller) queueNeededBlocks(prevVer uint64) (uint64, int) {
   623  	curVer := p.model.LocalVersion(p.repoCfg.ID)
   624  	if curVer == prevVer {
   625  		return curVer, 0
   626  	}
   627  
   628  	if debug {
   629  		l.Debugf("%q: checking for more needed blocks", p.repoCfg.ID)
   630  	}
   631  
   632  	queued := 0
   633  	for _, f := range p.model.NeedFilesRepo(p.repoCfg.ID) {
   634  		if _, ok := p.openFiles[f.Name]; ok {
   635  			continue
   636  		}
   637  		lf := p.model.CurrentRepoFile(p.repoCfg.ID, f.Name)
   638  		have, need := scanner.BlockDiff(lf.Blocks, f.Blocks)
   639  		if debug {
   640  			l.Debugf("need:\n  local: %v\n  global: %v\n  haveBlocks: %v\n  needBlocks: %v", lf, f, have, need)
   641  		}
   642  		queued++
   643  		p.bq.put(bqAdd{
   644  			file: f,
   645  			have: have,
   646  			need: need,
   647  		})
   648  	}
   649  	if debug && queued > 0 {
   650  		l.Debugf("%q: queued %d items", p.repoCfg.ID, queued)
   651  	}
   652  
   653  	if queued > 0 {
   654  		return prevVer, queued
   655  	} else {
   656  		return curVer, 0
   657  	}
   658  }
   659  
   660  func (p *puller) closeFile(f protocol.FileInfo) {
   661  	if debug {
   662  		l.Debugf("pull: closing %q / %q", p.repoCfg.ID, f.Name)
   663  	}
   664  
   665  	of := p.openFiles[f.Name]
   666  	of.file.Close()
   667  	defer os.Remove(of.temp)
   668  
   669  	delete(p.openFiles, f.Name)
   670  
   671  	fd, err := os.Open(of.temp)
   672  	if err != nil {
   673  		if debug {
   674  			l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   675  		}
   676  		return
   677  	}
   678  	hb, _ := scanner.Blocks(fd, scanner.StandardBlockSize)
   679  	fd.Close()
   680  
   681  	if l0, l1 := len(hb), len(f.Blocks); l0 != l1 {
   682  		if debug {
   683  			l.Debugf("pull: %q / %q: nblocks %d != %d", p.repoCfg.ID, f.Name, l0, l1)
   684  		}
   685  		return
   686  	}
   687  
   688  	for i := range hb {
   689  		if bytes.Compare(hb[i].Hash, f.Blocks[i].Hash) != 0 {
   690  			l.Debugf("pull: %q / %q: block %d hash mismatch\n\thave: %x\n\twant: %x", p.repoCfg.ID, f.Name, i, hb[i].Hash, f.Blocks[i].Hash)
   691  			return
   692  		}
   693  	}
   694  
   695  	t := time.Unix(f.Modified, 0)
   696  	err = os.Chtimes(of.temp, t, t)
   697  	if debug && err != nil {
   698  		l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   699  	}
   700  	if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
   701  		err = os.Chmod(of.temp, os.FileMode(f.Flags&0777))
   702  		if debug && err != nil {
   703  			l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   704  		}
   705  	}
   706  
   707  	osutil.ShowFile(of.temp)
   708  
   709  	if p.versioner != nil {
   710  		err := p.versioner.Archive(p.repoCfg.Directory, of.filepath)
   711  		if err != nil {
   712  			if debug {
   713  				l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   714  			}
   715  			return
   716  		}
   717  	}
   718  
   719  	if debug {
   720  		l.Debugf("pull: rename %q / %q: %q", p.repoCfg.ID, f.Name, of.filepath)
   721  	}
   722  	if err := osutil.Rename(of.temp, of.filepath); err == nil {
   723  		p.model.updateLocal(p.repoCfg.ID, f)
   724  	} else {
   725  		l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
   726  	}
   727  }
   728  
   729  func invalidateRepo(cfg *config.Configuration, repoID string, err error) {
   730  	for i := range cfg.Repositories {
   731  		repo := &cfg.Repositories[i]
   732  		if repo.ID == repoID {
   733  			repo.Invalid = err.Error()
   734  			return
   735  		}
   736  	}
   737  }