github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/simplefs/archive.go (about)

     1  // Copyright 2024 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package simplefs
     5  
     6  import (
     7  	"archive/zip"
     8  	"bytes"
     9  	"compress/gzip"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"fmt"
    14  	"hash"
    15  	"io"
    16  	"io/fs"
    17  	"os"
    18  	"path"
    19  	"path/filepath"
    20  	"sort"
    21  	"sync"
    22  	"time"
    23  
    24  	"golang.org/x/time/rate"
    25  
    26  	"github.com/keybase/client/go/libkb"
    27  	"github.com/keybase/client/go/protocol/keybase1"
    28  	"github.com/pkg/errors"
    29  	"golang.org/x/net/context"
    30  	"gopkg.in/src-d/go-billy.v4"
    31  )
    32  
    33  func loadArchiveStateFromJsonGz(ctx context.Context, simpleFS *SimpleFS, filePath string) (state *keybase1.SimpleFSArchiveState, err error) {
    34  	f, err := os.Open(filePath)
    35  	if err != nil {
    36  		simpleFS.log.CErrorf(ctx, "loadArchiveStateFromJsonGz: opening state file error: %v", err)
    37  		return nil, err
    38  	}
    39  	defer f.Close()
    40  	gzReader, err := gzip.NewReader(f)
    41  	if err != nil {
    42  		simpleFS.log.CErrorf(ctx, "loadArchiveStateFromJsonGz: creating gzip reader error: %v", err)
    43  		return nil, err
    44  	}
    45  	decoder := json.NewDecoder(gzReader)
    46  	err = decoder.Decode(&state)
    47  	if err != nil {
    48  		simpleFS.log.CErrorf(ctx, "loadArchiveStateFromJsonGz: decoding state file error: %v", err)
    49  		return nil, err
    50  	}
    51  	return state, nil
    52  }
    53  
    54  func writeArchiveStateIntoJsonGz(ctx context.Context, simpleFS *SimpleFS, filePath string, s *keybase1.SimpleFSArchiveState) error {
    55  	err := os.MkdirAll(filepath.Dir(filePath), 0755)
    56  	if err != nil {
    57  		simpleFS.log.CErrorf(ctx, "writeArchiveStateIntoJsonGz: os.MkdirAll error: %v", err)
    58  		return err
    59  	}
    60  	f, err := os.Create(filePath)
    61  	if err != nil {
    62  		simpleFS.log.CErrorf(ctx, "writeArchiveStateIntoJsonGz: creating state file error: %v", err)
    63  		return err
    64  	}
    65  	defer f.Close()
    66  
    67  	gzWriter := gzip.NewWriter(f)
    68  	defer gzWriter.Close()
    69  
    70  	encoder := json.NewEncoder(gzWriter)
    71  	err = encoder.Encode(s)
    72  	if err != nil {
    73  		simpleFS.log.CErrorf(ctx, "writeArchiveStateIntoJsonGz: encoding state file error: %v", err)
    74  		return err
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  type errorState struct {
    81  	err       error
    82  	nextRetry time.Time
    83  }
    84  
    85  type archiveManager struct {
    86  	simpleFS *SimpleFS
    87  	username libkb.NormalizedUsername
    88  
    89  	// Just use a regular mutex rather than a rw one so all writes to
    90  	// persistent storage are synchronized.
    91  	mu               sync.Mutex
    92  	state            *keybase1.SimpleFSArchiveState
    93  	jobCtxCancellers map[string]func()
    94  	// jobID -> errorState. Populated when an error has happened. It's only
    95  	// valid for these phases:
    96  	//
    97  	//   keybase1.SimpleFSArchiveJobPhase_Indexing
    98  	//   keybase1.SimpleFSArchiveJobPhase_Copying
    99  	//   keybase1.SimpleFSArchiveJobPhase_Zipping
   100  	//
   101  	// When nextRetry is current errorRetryWorker delete the errorState from
   102  	// this map, while also putting them back to the previous phase so the
   103  	// worker can pick it up.
   104  	errors map[string]errorState
   105  
   106  	indexingWorkerSignal      chan struct{}
   107  	copyingWorkerSignal       chan struct{}
   108  	zippingWorkerSignal       chan struct{}
   109  	notifyUIStateChangeSignal chan struct{}
   110  
   111  	ctxCancel func()
   112  }
   113  
   114  func (m *archiveManager) getStateFilePath(simpleFS *SimpleFS) string {
   115  	cacheDir := simpleFS.getCacheDir()
   116  	return filepath.Join(cacheDir, fmt.Sprintf("kbfs-archive-%s.json.gz", m.username))
   117  }
   118  
   119  func (m *archiveManager) flushStateFileLocked(ctx context.Context) error {
   120  	select {
   121  	case <-ctx.Done():
   122  		return ctx.Err()
   123  	default:
   124  	}
   125  	err := writeArchiveStateIntoJsonGz(ctx, m.simpleFS, m.getStateFilePath(m.simpleFS), m.state)
   126  	if err != nil {
   127  		m.simpleFS.log.CErrorf(ctx,
   128  			"archiveManager.flushStateFileLocked: writing state file error: %v", err)
   129  		return err
   130  	}
   131  	return nil
   132  }
   133  
   134  func (m *archiveManager) flushStateFile(ctx context.Context) error {
   135  	m.mu.Lock()
   136  	defer m.mu.Unlock()
   137  	return m.flushStateFileLocked(ctx)
   138  }
   139  
   140  func (m *archiveManager) signal(ch chan struct{}) {
   141  	select {
   142  	case ch <- struct{}{}:
   143  	default:
   144  		// There's already a signal in the chan. Skipping this.
   145  	}
   146  }
   147  
   148  func (m *archiveManager) shutdown(ctx context.Context) {
   149  	// OK to cancel before flushStateFileLocked because we'll pass in the
   150  	// shutdown ctx there.
   151  	if m.ctxCancel != nil {
   152  		m.ctxCancel()
   153  	}
   154  
   155  	m.mu.Lock()
   156  	defer m.mu.Unlock()
   157  	err := m.flushStateFileLocked(ctx)
   158  	if err != nil {
   159  		m.simpleFS.log.CWarningf(ctx, "m.flushStateFileLocked error: %v", err)
   160  	}
   161  }
   162  
   163  func (m *archiveManager) notifyUIStateChange(ctx context.Context) {
   164  	m.simpleFS.log.CDebugf(ctx, "+ archiveManager.notifyUIStateChange")
   165  	defer m.simpleFS.log.CDebugf(ctx, "- archiveManager.notifyUIStateChange")
   166  	m.mu.Lock()
   167  	defer m.mu.Unlock()
   168  	state, errorStates := m.getCurrentStateLocked(ctx)
   169  	m.simpleFS.notifyUIArchiveStateChange(ctx, state, errorStates)
   170  }
   171  
   172  func (m *archiveManager) startJob(ctx context.Context, job keybase1.SimpleFSArchiveJobDesc) error {
   173  	m.simpleFS.log.CDebugf(ctx, "+ archiveManager.startJob %#+v", job)
   174  	defer m.simpleFS.log.CDebugf(ctx, "- archiveManager.startJob")
   175  
   176  	m.mu.Lock()
   177  	defer m.mu.Unlock()
   178  	if _, ok := m.state.Jobs[job.JobID]; ok {
   179  		return errors.New("job ID already exists")
   180  	}
   181  	m.state.Jobs[job.JobID] = keybase1.SimpleFSArchiveJobState{
   182  		Desc:  job,
   183  		Phase: keybase1.SimpleFSArchiveJobPhase_Queued,
   184  	}
   185  	m.state.LastUpdated = keybase1.ToTime(time.Now())
   186  	m.signal(m.notifyUIStateChangeSignal)
   187  	m.signal(m.indexingWorkerSignal)
   188  	return m.flushStateFileLocked(ctx)
   189  }
   190  
   191  func (m *archiveManager) cancelOrDismissJob(ctx context.Context,
   192  	jobID string) (err error) {
   193  	m.simpleFS.log.CDebugf(ctx, "+ archiveManager.cancelOrDismissJob")
   194  	defer m.simpleFS.log.CDebugf(ctx, "- archiveManager.cancelOrDismissJob %s", jobID)
   195  	m.mu.Lock()
   196  	defer m.mu.Unlock()
   197  
   198  	if cancel, ok := m.jobCtxCancellers[jobID]; ok {
   199  		cancel()
   200  		delete(m.jobCtxCancellers, jobID)
   201  	}
   202  
   203  	job, ok := m.state.Jobs[jobID]
   204  	if !ok {
   205  		return errors.New("job not found")
   206  	}
   207  	delete(m.state.Jobs, jobID)
   208  
   209  	err = os.RemoveAll(job.Desc.StagingPath)
   210  	if err != nil {
   211  		m.simpleFS.log.CWarningf(ctx, "removing staging path %q for job %s error: %v",
   212  			job.Desc.StagingPath, jobID, err)
   213  	}
   214  
   215  	m.signal(m.notifyUIStateChangeSignal)
   216  	return nil
   217  }
   218  
   219  func (m *archiveManager) getCurrentStateLocked(ctx context.Context) (
   220  	state keybase1.SimpleFSArchiveState, errorStates map[string]errorState) {
   221  	errorStates = make(map[string]errorState)
   222  	for jobID, errState := range m.errors {
   223  		errorStates[jobID] = errState
   224  	}
   225  	return m.state.DeepCopy(), errorStates
   226  }
   227  
   228  func (m *archiveManager) getCurrentState(ctx context.Context) (
   229  	state keybase1.SimpleFSArchiveState, errorStates map[string]errorState) {
   230  	m.simpleFS.log.CDebugf(ctx, "+ archiveManager.getCurrentState")
   231  	defer m.simpleFS.log.CDebugf(ctx, "- archiveManager.getCurrentState")
   232  	m.mu.Lock()
   233  	defer m.mu.Unlock()
   234  	return m.getCurrentStateLocked(ctx)
   235  }
   236  
   237  func (m *archiveManager) checkArchive(
   238  	ctx context.Context, archiveZipFilePath string) (
   239  	desc keybase1.SimpleFSArchiveJobDesc, pathsWithIssues map[string]string,
   240  	err error) {
   241  	m.simpleFS.log.CDebugf(ctx, "+ archiveManager.checkArchive %q", archiveZipFilePath)
   242  	defer m.simpleFS.log.CDebugf(ctx, "- archiveManager.checkArchive %q", archiveZipFilePath)
   243  
   244  	reader, err := zip.OpenReader(archiveZipFilePath)
   245  	if err != nil {
   246  		return keybase1.SimpleFSArchiveJobDesc{}, nil,
   247  			fmt.Errorf("zip.OpenReader(%s) error: %v", archiveZipFilePath, err)
   248  	}
   249  	defer reader.Close()
   250  
   251  	var receipt Receipt
   252  	{
   253  		receiptFile, err := reader.Open("receipt.json")
   254  		if err != nil {
   255  			return keybase1.SimpleFSArchiveJobDesc{}, nil,
   256  				fmt.Errorf("reader.Open(receipt.json) error: %v", err)
   257  		}
   258  		defer receiptFile.Close()
   259  		err = json.NewDecoder(receiptFile).Decode(&receipt)
   260  		if err != nil {
   261  			return keybase1.SimpleFSArchiveJobDesc{}, nil,
   262  				fmt.Errorf("json Decode on receipt.json error: %v", err)
   263  		}
   264  	}
   265  
   266  	pathsWithIssues = make(map[string]string)
   267  
   268  loopManifest:
   269  	for itemPath, item := range receipt.Manifest {
   270  		f, err := reader.Open(path.Join(receipt.Desc.TargetName, itemPath))
   271  		if err != nil {
   272  			errDesc := fmt.Sprintf("opening %q error: %v", itemPath, err)
   273  			m.simpleFS.log.CWarningf(ctx, errDesc)
   274  			pathsWithIssues[itemPath] = errDesc
   275  			continue loopManifest
   276  		}
   277  
   278  		{ // Check DirentType
   279  			fstat, err := f.Stat()
   280  			if err != nil {
   281  				errDesc := fmt.Sprintf("f.Stat %q error: %v", itemPath, err)
   282  				m.simpleFS.log.CWarningf(ctx, errDesc)
   283  				pathsWithIssues[itemPath] = errDesc
   284  				continue loopManifest
   285  			}
   286  			switch item.DirentType {
   287  			case keybase1.DirentType_DIR:
   288  				if !fstat.IsDir() {
   289  					errDesc := fmt.Sprintf(
   290  						"%q is a dir in manifest but not a dir in archive", itemPath)
   291  					m.simpleFS.log.CWarningf(ctx, errDesc)
   292  					pathsWithIssues[itemPath] = errDesc
   293  					continue loopManifest
   294  				}
   295  				continue loopManifest
   296  			case keybase1.DirentType_FILE:
   297  				if fstat.IsDir() || fstat.Mode()&os.ModeSymlink != 0 || fstat.Mode()&0111 != 0 {
   298  					errDesc := fmt.Sprintf(
   299  						"%q is a normal file with no exec bit in manifest but not in archive (mode=%v)", itemPath, fstat.Mode())
   300  					m.simpleFS.log.CWarningf(ctx, errDesc)
   301  					pathsWithIssues[itemPath] = errDesc
   302  					continue loopManifest
   303  				}
   304  			case keybase1.DirentType_SYM:
   305  				if fstat.IsDir() || fstat.Mode()&os.ModeSymlink == 0 {
   306  					errDesc := fmt.Sprintf(
   307  						"%q is a symlink in manifest but not in archive (mode=%v)", itemPath, fstat.Mode())
   308  					m.simpleFS.log.CWarningf(ctx, errDesc)
   309  					pathsWithIssues[itemPath] = errDesc
   310  					continue loopManifest
   311  				}
   312  				continue loopManifest
   313  			case keybase1.DirentType_EXEC:
   314  				if fstat.IsDir() || fstat.Mode()&os.ModeSymlink != 0 || fstat.Mode()&0111 == 0 {
   315  					errDesc := fmt.Sprintf(
   316  						"%q is a normal file with exec bit in manifest but not in archive (mode=%v)", itemPath, fstat.Mode())
   317  					m.simpleFS.log.CWarningf(ctx, errDesc)
   318  					pathsWithIssues[itemPath] = errDesc
   319  					continue loopManifest
   320  				}
   321  			}
   322  		}
   323  
   324  		{ // Check hash
   325  			h := sha256.New()
   326  			_, err = io.Copy(h, f)
   327  			if err != nil {
   328  				errDesc := fmt.Sprintf("hashing %q error: %v", itemPath, err)
   329  				m.simpleFS.log.CWarningf(ctx, errDesc)
   330  				pathsWithIssues[itemPath] = errDesc
   331  				return keybase1.SimpleFSArchiveJobDesc{}, nil,
   332  					fmt.Errorf("hashing %q error: %v", itemPath, err)
   333  			}
   334  			if hex.EncodeToString(h.Sum(nil)) != item.Sha256SumHex {
   335  				errDesc := fmt.Sprintf("hash doesn't match %q", itemPath)
   336  				m.simpleFS.log.CWarningf(ctx, errDesc)
   337  				pathsWithIssues[itemPath] = errDesc
   338  				continue loopManifest
   339  			}
   340  		}
   341  	}
   342  	return receipt.Desc, pathsWithIssues, nil
   343  }
   344  
   345  func (m *archiveManager) changeJobPhaseLocked(ctx context.Context,
   346  	jobID string, newPhase keybase1.SimpleFSArchiveJobPhase) {
   347  	copy, ok := m.state.Jobs[jobID]
   348  	if !ok {
   349  		m.simpleFS.log.CWarningf(ctx, "job %s not found. it might have been canceled", jobID)
   350  		return
   351  	}
   352  	copy.Phase = newPhase
   353  	m.state.Jobs[jobID] = copy
   354  	m.signal(m.notifyUIStateChangeSignal)
   355  }
   356  func (m *archiveManager) changeJobPhase(ctx context.Context,
   357  	jobID string, newPhase keybase1.SimpleFSArchiveJobPhase) {
   358  	m.mu.Lock()
   359  	defer m.mu.Unlock()
   360  	m.changeJobPhaseLocked(ctx, jobID, newPhase)
   361  }
   362  
   363  func (m *archiveManager) startWorkerTask(ctx context.Context,
   364  	eligiblePhase keybase1.SimpleFSArchiveJobPhase,
   365  	newPhase keybase1.SimpleFSArchiveJobPhase) (jobID string, jobCtx context.Context, ok bool) {
   366  	jobCtx, cancel := context.WithCancel(ctx)
   367  	m.mu.Lock()
   368  	defer m.mu.Unlock()
   369  	for jobID := range m.state.Jobs {
   370  		if m.state.Jobs[jobID].Phase == eligiblePhase {
   371  			m.changeJobPhaseLocked(ctx, jobID, newPhase)
   372  			m.jobCtxCancellers[jobID] = cancel
   373  			return jobID, jobCtx, true
   374  		}
   375  	}
   376  	return "", nil, false
   377  }
   378  
   379  const archiveErrorRetryDuration = time.Minute
   380  
   381  func (m *archiveManager) setJobError(
   382  	ctx context.Context, jobID string, err error) {
   383  	m.mu.Lock()
   384  	defer m.mu.Unlock()
   385  	nextRetry := time.Now().Add(archiveErrorRetryDuration)
   386  	m.simpleFS.log.CErrorf(ctx, "job %s nextRetry: %s", jobID, nextRetry)
   387  	m.errors[jobID] = errorState{
   388  		err:       err,
   389  		nextRetry: nextRetry,
   390  	}
   391  }
   392  
   393  func (m *archiveManager) doIndexing(ctx context.Context, jobID string) (err error) {
   394  	m.simpleFS.log.CDebugf(ctx, "+ doIndexing %s", jobID)
   395  	defer func() { m.simpleFS.log.CDebugf(ctx, "- doIndexing %s err: %v", jobID, err) }()
   396  
   397  	jobDesc := func() keybase1.SimpleFSArchiveJobDesc {
   398  		m.mu.Lock()
   399  		defer m.mu.Unlock()
   400  		return m.state.Jobs[jobID].Desc
   401  	}()
   402  	opid, err := m.simpleFS.SimpleFSMakeOpid(ctx)
   403  	if err != nil {
   404  		return err
   405  	}
   406  	defer m.simpleFS.SimpleFSClose(ctx, opid)
   407  	filter := keybase1.ListFilter_NO_FILTER
   408  	err = m.simpleFS.SimpleFSListRecursive(ctx, keybase1.SimpleFSListRecursiveArg{
   409  		OpID:   opid,
   410  		Path:   keybase1.NewPathWithKbfsArchived(jobDesc.KbfsPathWithRevision),
   411  		Filter: filter,
   412  	})
   413  	err = m.simpleFS.SimpleFSWait(ctx, opid)
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	listResult, err := m.simpleFS.SimpleFSReadList(ctx, opid)
   419  	if err != nil {
   420  		return err
   421  	}
   422  
   423  	var bytesTotal int64
   424  	manifest := make(map[string]keybase1.SimpleFSArchiveFile)
   425  	for _, e := range listResult.Entries {
   426  		manifest[e.Name] = keybase1.SimpleFSArchiveFile{
   427  			State:      keybase1.SimpleFSFileArchiveState_ToDo,
   428  			DirentType: e.DirentType,
   429  		}
   430  		if e.DirentType == keybase1.DirentType_FILE ||
   431  			e.DirentType == keybase1.DirentType_EXEC {
   432  			bytesTotal += int64(e.Size)
   433  		}
   434  	}
   435  
   436  	func() {
   437  		m.mu.Lock()
   438  		defer m.mu.Unlock()
   439  
   440  		jobCopy, ok := m.state.Jobs[jobID]
   441  		if !ok {
   442  			m.simpleFS.log.CWarningf(ctx, "job %s not found. it might have been canceled", jobID)
   443  			return
   444  		}
   445  		jobCopy.Manifest = manifest
   446  		jobCopy.BytesTotal = bytesTotal
   447  		m.state.Jobs[jobID] = jobCopy
   448  		m.signal(m.notifyUIStateChangeSignal)
   449  	}()
   450  	return nil
   451  }
   452  
   453  func (m *archiveManager) waitForSimpleFSInit(ctx context.Context) error {
   454  	for {
   455  		if m.simpleFS.isInitialized() {
   456  			return nil
   457  		}
   458  
   459  		t := time.NewTimer(1 * time.Second)
   460  		select {
   461  		case <-ctx.Done():
   462  			return ctx.Err()
   463  		case <-t.C:
   464  		}
   465  	}
   466  }
   467  
   468  func (m *archiveManager) indexingWorker(ctx context.Context) {
   469  	err := m.waitForSimpleFSInit(ctx)
   470  	if err != nil {
   471  		return
   472  	}
   473  
   474  	for {
   475  		select {
   476  		case <-ctx.Done():
   477  			return
   478  		case <-m.indexingWorkerSignal:
   479  		}
   480  
   481  		jobID, jobCtx, ok := m.startWorkerTask(ctx,
   482  			keybase1.SimpleFSArchiveJobPhase_Queued,
   483  			keybase1.SimpleFSArchiveJobPhase_Indexing)
   484  
   485  		if !ok {
   486  			continue
   487  		}
   488  		// We got a task. Put another token into the signal channel so we
   489  		// check again on the next iteration.
   490  		m.signal(m.indexingWorkerSignal)
   491  
   492  		m.simpleFS.log.CDebugf(ctx, "indexing: %s", jobID)
   493  
   494  		err := m.doIndexing(jobCtx, jobID)
   495  		if err == nil {
   496  			m.simpleFS.log.CDebugf(jobCtx, "indexing done on job %s", jobID)
   497  			m.changeJobPhase(jobCtx, jobID, keybase1.SimpleFSArchiveJobPhase_Indexed)
   498  			m.signal(m.copyingWorkerSignal) // Done indexing! Notify the copying worker.
   499  		} else {
   500  			m.simpleFS.log.CErrorf(jobCtx, "indexing error on job %s: %v", jobID, err)
   501  			m.setJobError(ctx, jobID, err)
   502  		}
   503  
   504  		err = m.flushStateFile(ctx)
   505  		if err != nil {
   506  			m.simpleFS.log.CWarningf(ctx, "m.flushStateFileLocked error: %v", err)
   507  		}
   508  	}
   509  }
   510  
   511  type sha256TeeReader struct {
   512  	inner          io.Reader
   513  	innerTeeReader io.Reader
   514  	h              hash.Hash
   515  }
   516  
   517  var _ io.Reader = (*sha256TeeReader)(nil)
   518  
   519  // Read implements the io.Reader interface.
   520  func (r *sha256TeeReader) Read(p []byte) (n int, err error) {
   521  	return r.innerTeeReader.Read(p)
   522  }
   523  
   524  func (r *sha256TeeReader) getSum() []byte {
   525  	return r.h.Sum(nil)
   526  }
   527  
   528  func newSHA256TeeReader(inner io.Reader) (r *sha256TeeReader) {
   529  	r = &sha256TeeReader{
   530  		inner: inner,
   531  		h:     sha256.New(),
   532  	}
   533  	r.innerTeeReader = io.TeeReader(r.inner, r.h)
   534  	return r
   535  }
   536  
   537  type bytesUpdaterFunc = func(delta int64)
   538  
   539  func ctxAwareCopy(
   540  	ctx context.Context, to io.Writer, from io.Reader,
   541  	bytesUpdater bytesUpdaterFunc) error {
   542  	for {
   543  		select {
   544  		case <-ctx.Done():
   545  			return ctx.Err()
   546  		default:
   547  		}
   548  		n, err := io.CopyN(to, from, 64*1024)
   549  		switch err {
   550  		case nil:
   551  			bytesUpdater(n)
   552  		case io.EOF:
   553  			bytesUpdater(n)
   554  			return nil
   555  		default:
   556  			return err
   557  		}
   558  	}
   559  }
   560  
   561  func (m *archiveManager) copyFileFromBeginning(ctx context.Context,
   562  	srcDirFS billy.Filesystem, entryPathWithinJob string,
   563  	localPath string, mode os.FileMode,
   564  	bytesCopiedUpdater bytesUpdaterFunc) (sha256Sum []byte, err error) {
   565  	m.simpleFS.log.CDebugf(ctx, "+ copyFileFromBeginning %s", entryPathWithinJob)
   566  	defer func() { m.simpleFS.log.CDebugf(ctx, "- copyFileFromBeginning %s err: %v", entryPathWithinJob, err) }()
   567  
   568  	src, err := srcDirFS.Open(entryPathWithinJob)
   569  	if err != nil {
   570  		return nil, fmt.Errorf("srcDirFS.Open(%s) error: %v", entryPathWithinJob, err)
   571  	}
   572  	defer src.Close()
   573  
   574  	dst, err := os.OpenFile(localPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   575  	if err != nil {
   576  		return nil, fmt.Errorf("os.OpenFile(%s) error: %v", localPath, err)
   577  	}
   578  	defer dst.Close()
   579  
   580  	teeReader := newSHA256TeeReader(src)
   581  
   582  	err = ctxAwareCopy(ctx, dst, teeReader, bytesCopiedUpdater)
   583  	if err != nil {
   584  		return nil, fmt.Errorf("[%s] io.CopyN error: %v", entryPathWithinJob, err)
   585  	}
   586  
   587  	// We didn't continue from a previously interrupted copy, so don't
   588  	// bother verifying the sha256sum and just return it.
   589  	return teeReader.getSum(), nil
   590  }
   591  
   592  func (m *archiveManager) copyFilePickupPrevious(ctx context.Context,
   593  	srcDirFS billy.Filesystem, entryPathWithinJob string,
   594  	localPath string, srcSeekOffset int64, mode os.FileMode,
   595  	bytesCopiedUpdater bytesUpdaterFunc) (sha256Sum []byte, err error) {
   596  	m.simpleFS.log.CDebugf(ctx, "+ copyFilePickupPrevious %s", entryPathWithinJob)
   597  	defer func() { m.simpleFS.log.CDebugf(ctx, "- copyFilePickupPrevious %s err: %v", entryPathWithinJob, err) }()
   598  
   599  	src, err := srcDirFS.Open(entryPathWithinJob)
   600  	if err != nil {
   601  		return nil, fmt.Errorf("srcDirFS.Open(%s) error: %v", entryPathWithinJob, err)
   602  	}
   603  	defer src.Close()
   604  
   605  	_, err = src.Seek(srcSeekOffset, io.SeekStart)
   606  	if err != nil {
   607  		return nil, fmt.Errorf("[%s] src.Seek error: %v", entryPathWithinJob, err)
   608  	}
   609  
   610  	// Copy the file.
   611  	if err = func() error {
   612  		dst, err := os.OpenFile(localPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, mode)
   613  		if err != nil {
   614  			return fmt.Errorf("os.OpenFile(%s) error: %v", localPath, err)
   615  		}
   616  		defer dst.Close()
   617  
   618  		err = ctxAwareCopy(ctx, dst, src, bytesCopiedUpdater)
   619  		if err != nil {
   620  			return fmt.Errorf("[%s] io.CopyN error: %v", entryPathWithinJob, err)
   621  		}
   622  
   623  		return nil
   624  	}(); err != nil {
   625  		return nil, err
   626  	}
   627  
   628  	var size int64
   629  	// Calculate sha256 and check the sha256 of the copied file since we
   630  	// continued from a previously interrupted copy.
   631  	srcSHA256Sum, dstSHA256Sum, err := func() (srcSHA256Sum, dstSHA256Sum []byte, err error) {
   632  		_, err = src.Seek(0, io.SeekStart)
   633  		if err != nil {
   634  			return nil, nil, fmt.Errorf("[%s] src.Seek error: %v", entryPathWithinJob, err)
   635  		}
   636  		srcSHA256SumHasher := sha256.New()
   637  		size, err = io.Copy(srcSHA256SumHasher, src)
   638  		if err != nil {
   639  			return nil, nil, fmt.Errorf("[%s] io.Copy error: %v", entryPathWithinJob, err)
   640  		}
   641  		srcSHA256Sum = srcSHA256SumHasher.Sum(nil)
   642  
   643  		dst, err := os.Open(localPath)
   644  		if err != nil {
   645  			return nil, nil, fmt.Errorf("os.Open(%s) error: %v", localPath, err)
   646  		}
   647  		defer dst.Close()
   648  		dstSHA256SumHasher := sha256.New()
   649  		_, err = io.Copy(dstSHA256SumHasher, dst)
   650  		if err != nil {
   651  			return nil, nil, fmt.Errorf("[%s] io.Copy error: %v", entryPathWithinJob, err)
   652  		}
   653  		dstSHA256Sum = dstSHA256SumHasher.Sum(nil)
   654  
   655  		return srcSHA256Sum, dstSHA256Sum, nil
   656  	}()
   657  	if err != nil {
   658  		return nil, err
   659  	}
   660  
   661  	if !bytes.Equal(srcSHA256Sum, dstSHA256Sum) {
   662  		m.simpleFS.log.CInfof(ctx,
   663  			"file corruption is detected from a previous copy. Will copy from the beginning: ",
   664  			entryPathWithinJob)
   665  		bytesCopiedUpdater(-size)
   666  		return m.copyFileFromBeginning(ctx, srcDirFS, entryPathWithinJob, localPath, mode, bytesCopiedUpdater)
   667  	}
   668  
   669  	return srcSHA256Sum, nil
   670  }
   671  
   672  func (m *archiveManager) copyFile(ctx context.Context,
   673  	srcDirFS billy.Filesystem, entryPathWithinJob string,
   674  	localPath string, srcSeekOffset int64, mode os.FileMode,
   675  	bytesCopiedUpdater bytesUpdaterFunc) (sha256Sum []byte, err error) {
   676  	if srcSeekOffset == 0 {
   677  		return m.copyFileFromBeginning(ctx, srcDirFS, entryPathWithinJob, localPath, mode, bytesCopiedUpdater)
   678  	}
   679  	return m.copyFilePickupPrevious(ctx, srcDirFS, entryPathWithinJob, localPath, srcSeekOffset, mode, bytesCopiedUpdater)
   680  }
   681  
   682  func getWorkspaceDir(jobDesc keybase1.SimpleFSArchiveJobDesc) string {
   683  	return filepath.Join(jobDesc.StagingPath, "workspace")
   684  }
   685  
   686  func (m *archiveManager) doCopying(ctx context.Context, jobID string) (err error) {
   687  	m.simpleFS.log.CDebugf(ctx, "+ doCopying %s", jobID)
   688  	defer func() { m.simpleFS.log.CDebugf(ctx, "- doCopying %s err: %v", jobID, err) }()
   689  
   690  	desc, manifest := func() (keybase1.SimpleFSArchiveJobDesc, map[string]keybase1.SimpleFSArchiveFile) {
   691  		m.mu.Lock()
   692  		defer m.mu.Unlock()
   693  		manifest := make(map[string]keybase1.SimpleFSArchiveFile)
   694  		for k, v := range m.state.Jobs[jobID].Manifest {
   695  			manifest[k] = v.DeepCopy()
   696  		}
   697  		return m.state.Jobs[jobID].Desc, manifest
   698  	}()
   699  
   700  	updateManifest := func(manifest map[string]keybase1.SimpleFSArchiveFile) {
   701  		m.mu.Lock()
   702  		defer m.mu.Unlock()
   703  		// Can override directly since only one worker can work on a give job at a time.
   704  		job := m.state.Jobs[jobID]
   705  		for k, v := range manifest {
   706  			job.Manifest[k] = v.DeepCopy()
   707  		}
   708  		m.state.Jobs[jobID] = job
   709  		m.signal(m.notifyUIStateChangeSignal)
   710  	}
   711  
   712  	updateBytesCopied := func(delta int64) {
   713  		m.mu.Lock()
   714  		defer m.mu.Unlock()
   715  		// Can override directly since only one worker can work on a give job at a time.
   716  		job := m.state.Jobs[jobID]
   717  		job.BytesCopied += delta
   718  		m.state.Jobs[jobID] = job
   719  		m.signal(m.notifyUIStateChangeSignal)
   720  	}
   721  
   722  	srcContainingDirFS, finalElem, err := m.simpleFS.getFSIfExists(ctx,
   723  		keybase1.NewPathWithKbfsArchived(desc.KbfsPathWithRevision))
   724  	if err != nil {
   725  		return fmt.Errorf("getFSIfExists error: %v", err)
   726  	}
   727  	srcDirFS, err := srcContainingDirFS.Chroot(finalElem)
   728  	if err != nil {
   729  		return fmt.Errorf("srcContainingDirFS.Chroot error: %v", err)
   730  	}
   731  	dstBase := filepath.Join(getWorkspaceDir(desc), desc.TargetName)
   732  
   733  	err = os.MkdirAll(dstBase, 0755)
   734  	if err != nil {
   735  		return fmt.Errorf("os.MkdirAll(%s) error: %v", dstBase, err)
   736  	}
   737  
   738  	entryPaths := make([]string, 0, len(manifest))
   739  	for entryPathWithinJob := range manifest {
   740  		entryPaths = append(entryPaths, entryPathWithinJob)
   741  	}
   742  	sort.Strings(entryPaths)
   743  
   744  loopEntryPaths:
   745  	for _, entryPathWithinJob := range entryPaths {
   746  		entry := manifest[entryPathWithinJob]
   747  		entry.State = keybase1.SimpleFSFileArchiveState_InProgress
   748  		manifest[entryPathWithinJob] = entry
   749  		updateManifest(manifest)
   750  
   751  		localPath := filepath.Join(dstBase, entryPathWithinJob)
   752  		srcFI, err := srcDirFS.Lstat(entryPathWithinJob)
   753  		if err != nil {
   754  			return fmt.Errorf("srcDirFS.LStat(%s) error: %v", entryPathWithinJob, err)
   755  		}
   756  		switch {
   757  		case srcFI.IsDir():
   758  			err = os.MkdirAll(localPath, 0755)
   759  			if err != nil {
   760  				return fmt.Errorf("os.MkdirAll(%s) error: %v", localPath, err)
   761  			}
   762  			err = os.Chtimes(localPath, time.Time{}, srcFI.ModTime())
   763  			if err != nil {
   764  				return fmt.Errorf("os.Chtimes(%s) error: %v", localPath, err)
   765  			}
   766  			entry.State = keybase1.SimpleFSFileArchiveState_Complete
   767  			manifest[entryPathWithinJob] = entry
   768  		case srcFI.Mode()&os.ModeSymlink != 0: // symlink
   769  			err = os.MkdirAll(filepath.Dir(localPath), 0755)
   770  			if err != nil {
   771  				return fmt.Errorf("os.MkdirAll(filepath.Dir(%s)) error: %v", localPath, err)
   772  			}
   773  			// Call Stat, which follows symlinks, to make sure the link doesn't
   774  			// escape outside the srcDirFS.
   775  			_, err = srcDirFS.Stat(entryPathWithinJob)
   776  			if err != nil {
   777  				m.simpleFS.log.CWarningf(ctx, "skipping %s due to srcDirFS.Stat error: %v", entryPathWithinJob, err)
   778  				entry.State = keybase1.SimpleFSFileArchiveState_Skipped
   779  				manifest[entryPathWithinJob] = entry
   780  				continue loopEntryPaths
   781  			}
   782  
   783  			link, err := srcDirFS.Readlink(entryPathWithinJob)
   784  			if err != nil {
   785  				return fmt.Errorf("srcDirFS(%s) error: %v", entryPathWithinJob, err)
   786  			}
   787  			m.simpleFS.log.CInfof(ctx, "calling os.Symlink(%s, %s) ", link, localPath)
   788  			err = os.Symlink(link, localPath)
   789  			if err != nil {
   790  				return fmt.Errorf("os.Symlink(%s, %s) error: %v", link, localPath, err)
   791  			}
   792  			// Skipping Chtimes becasue there doesn't seem to be a way to
   793  			// change time on symlinks.
   794  			entry.State = keybase1.SimpleFSFileArchiveState_Complete
   795  			manifest[entryPathWithinJob] = entry
   796  		default:
   797  			err = os.MkdirAll(filepath.Dir(localPath), 0755)
   798  			if err != nil {
   799  				return fmt.Errorf("os.MkdirAll(filepath.Dir(%s)) error: %v", localPath, err)
   800  			}
   801  
   802  			var mode os.FileMode = 0644
   803  			if srcFI.Mode()&0100 != 0 {
   804  				mode = 0755
   805  			}
   806  
   807  			seek := int64(0)
   808  
   809  			dstFI, err := os.Lstat(localPath)
   810  			switch {
   811  			case os.IsNotExist(err): // simple copy from the start of file
   812  			case err == nil: // continue from a previously interrupted copy
   813  				if srcFI.Mode()&os.ModeSymlink == 0 {
   814  					seek = dstFI.Size()
   815  				}
   816  				// otherwise copy from the start of file
   817  			default:
   818  				return fmt.Errorf("os.Lstat(%s) error: %v", localPath, err)
   819  			}
   820  
   821  			sha256Sum, err := m.copyFile(ctx,
   822  				srcDirFS, entryPathWithinJob, localPath, seek, mode, updateBytesCopied)
   823  			if err != nil {
   824  				return err
   825  			}
   826  
   827  			err = os.Chtimes(localPath, time.Time{}, srcFI.ModTime())
   828  			if err != nil {
   829  				return fmt.Errorf("os.Chtimes(%s) error: %v", localPath, err)
   830  			}
   831  
   832  			entry.Sha256SumHex = hex.EncodeToString(sha256Sum)
   833  			entry.State = keybase1.SimpleFSFileArchiveState_Complete
   834  			manifest[entryPathWithinJob] = entry
   835  		}
   836  		updateManifest(manifest)
   837  	}
   838  
   839  	return nil
   840  }
   841  
   842  func (m *archiveManager) copyingWorker(ctx context.Context) {
   843  	err := m.waitForSimpleFSInit(ctx)
   844  	if err != nil {
   845  		return
   846  	}
   847  
   848  	for {
   849  		select {
   850  		case <-ctx.Done():
   851  			return
   852  		case <-m.copyingWorkerSignal:
   853  		}
   854  
   855  		jobID, jobCtx, ok := m.startWorkerTask(ctx,
   856  			keybase1.SimpleFSArchiveJobPhase_Indexed,
   857  			keybase1.SimpleFSArchiveJobPhase_Copying)
   858  
   859  		if !ok {
   860  			continue
   861  		}
   862  		// We got a task. Put another token into the signal channel so we
   863  		// check again on the next iteration.
   864  		m.signal(m.copyingWorkerSignal)
   865  
   866  		m.simpleFS.log.CDebugf(ctx, "copying: %s", jobID)
   867  
   868  		err := m.doCopying(jobCtx, jobID)
   869  		if err == nil {
   870  			m.simpleFS.log.CDebugf(jobCtx, "copying done on job %s", jobID)
   871  			m.changeJobPhase(jobCtx, jobID, keybase1.SimpleFSArchiveJobPhase_Copied)
   872  			m.signal(m.zippingWorkerSignal) // Done copying! Notify the zipping worker.
   873  		} else {
   874  			m.simpleFS.log.CErrorf(jobCtx, "copying error on job %s: %v", jobID, err)
   875  			m.setJobError(ctx, jobID, err)
   876  		}
   877  
   878  		err = m.flushStateFile(ctx)
   879  		if err != nil {
   880  			m.simpleFS.log.CWarningf(ctx, "m.flushStateFileLocked error: %v", err)
   881  		}
   882  	}
   883  }
   884  
   885  // zipWriterAddDir is adapted from zip.Writer.AddFS in go1.22.0 source because 1) we're
   886  // not on a version with this function yet, and 2) Go's AddFS doesn't support
   887  // symlinks; 3) we need bytesZippedUpdater here and we need to use CopyN for it.
   888  func zipWriterAddDir(ctx context.Context,
   889  	w *zip.Writer, dirPath string, bytesZippedUpdater bytesUpdaterFunc) error {
   890  	fsys := os.DirFS(dirPath)
   891  	return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
   892  		if err != nil {
   893  			return err
   894  		}
   895  		info, err := d.Info()
   896  		if err != nil {
   897  			return err
   898  		}
   899  		if !d.IsDir() && !(info.Mode() &^ fs.ModeSymlink).IsRegular() {
   900  			return errors.New("zip: cannot add non-regular file except symlink")
   901  		}
   902  		h, err := zip.FileInfoHeader(info)
   903  		if err != nil {
   904  			return err
   905  		}
   906  		h.Name = name
   907  		h.Method = zip.Deflate
   908  		fw, err := w.CreateHeader(h)
   909  		if err != nil {
   910  			return err
   911  		}
   912  		switch {
   913  		case d.IsDir():
   914  			return nil
   915  		case info.Mode()&fs.ModeSymlink != 0:
   916  			target, err := os.Readlink(filepath.Join(dirPath, name))
   917  			if err != nil {
   918  				return err
   919  			}
   920  			_, err = fw.Write([]byte(filepath.ToSlash(target)))
   921  			if err != nil {
   922  				return err
   923  			}
   924  			return nil
   925  		default:
   926  			f, err := fsys.Open(name)
   927  			if err != nil {
   928  				return err
   929  			}
   930  			defer f.Close()
   931  			return ctxAwareCopy(ctx, fw, f, bytesZippedUpdater)
   932  		}
   933  	})
   934  }
   935  
   936  // Receipt is serialized into receipt.json in the archive.
   937  type Receipt struct {
   938  	Desc     keybase1.SimpleFSArchiveJobDesc
   939  	Manifest map[string]keybase1.SimpleFSArchiveFile
   940  }
   941  
   942  func (m *archiveManager) doZipping(ctx context.Context, jobID string) (err error) {
   943  	m.simpleFS.log.CDebugf(ctx, "+ doZipping %s", jobID)
   944  	defer func() { m.simpleFS.log.CDebugf(ctx, "- doZipping %s err: %v", jobID, err) }()
   945  
   946  	jobDesc, receiptBytes, err := func() (keybase1.SimpleFSArchiveJobDesc, []byte, error) {
   947  		m.mu.Lock()
   948  		defer m.mu.Unlock()
   949  		receiptBytes, err := json.MarshalIndent(Receipt{
   950  			Desc:     m.state.Jobs[jobID].Desc,
   951  			Manifest: m.state.Jobs[jobID].Manifest,
   952  		}, "", "  ")
   953  		return m.state.Jobs[jobID].Desc, receiptBytes, err
   954  	}()
   955  	if err != nil {
   956  		return fmt.Errorf(
   957  			"getting jobDesc and receiptBytes for %s error: %v", jobID, err)
   958  	}
   959  
   960  	// Reset BytesZipped.
   961  	func() {
   962  		m.mu.Lock()
   963  		defer m.mu.Unlock()
   964  		// Can override directly since only one worker can work on a give job at a time.
   965  		job := m.state.Jobs[jobID]
   966  		job.BytesZipped = 0
   967  		m.state.Jobs[jobID] = job
   968  		m.signal(m.notifyUIStateChangeSignal)
   969  	}()
   970  
   971  	updateBytesZipped := func(delta int64) {
   972  		m.mu.Lock()
   973  		defer m.mu.Unlock()
   974  		// Can override directly since only one worker can work on a give job at a time.
   975  		job := m.state.Jobs[jobID]
   976  		job.BytesZipped += delta
   977  		m.state.Jobs[jobID] = job
   978  		m.signal(m.notifyUIStateChangeSignal)
   979  	}
   980  
   981  	workspaceDir := getWorkspaceDir(jobDesc)
   982  
   983  	err = os.MkdirAll(filepath.Dir(jobDesc.ZipFilePath), 0755)
   984  	if err != nil {
   985  		m.simpleFS.log.CErrorf(ctx, "os.MkdirAll error: %v", err)
   986  		return err
   987  	}
   988  
   989  	err = func() (err error) {
   990  		flag := os.O_WRONLY | os.O_CREATE | os.O_EXCL
   991  		if jobDesc.OverwriteZip {
   992  			flag = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
   993  		}
   994  		zipFile, err := os.OpenFile(jobDesc.ZipFilePath, flag, 0666)
   995  		if err != nil {
   996  			return fmt.Errorf("os.Create(%s) error: %v", jobDesc.ZipFilePath, err)
   997  		}
   998  		defer func() {
   999  			closeErr := zipFile.Close()
  1000  			if err == nil {
  1001  				err = closeErr
  1002  			}
  1003  			if closeErr != nil {
  1004  				m.simpleFS.log.CWarningf(ctx, "zipFile.Close %s error %v", jobDesc.ZipFilePath, err)
  1005  			}
  1006  			// Call Quarantine even if close failed just in case.
  1007  			qerr := Quarantine(ctx, jobDesc.ZipFilePath)
  1008  			if err == nil {
  1009  				err = qerr
  1010  			}
  1011  			if qerr != nil {
  1012  				m.simpleFS.log.CWarningf(ctx, "Quarantine %s error %v", jobDesc.ZipFilePath, err)
  1013  			}
  1014  		}()
  1015  
  1016  		zipWriter := zip.NewWriter(zipFile)
  1017  		defer func() {
  1018  			closeErr := zipWriter.Close()
  1019  			if err == nil {
  1020  				err = closeErr
  1021  			}
  1022  			if closeErr != nil {
  1023  				m.simpleFS.log.CWarningf(ctx, "zipWriter.Close %s error %v", jobDesc.ZipFilePath, err)
  1024  			}
  1025  		}()
  1026  
  1027  		err = zipWriterAddDir(ctx, zipWriter, workspaceDir, updateBytesZipped)
  1028  		if err != nil {
  1029  			return fmt.Errorf("zipWriterAddDir into %s error: %v", jobDesc.ZipFilePath, err)
  1030  		}
  1031  
  1032  		{ // write the manifest and desc down
  1033  			header := &zip.FileHeader{
  1034  				Name:   "receipt.json",
  1035  				Method: zip.Deflate,
  1036  			}
  1037  			header.SetModTime(time.Now())
  1038  			w, err := zipWriter.CreateHeader(header)
  1039  			if err != nil {
  1040  				return fmt.Errorf("zipWriter.Create(receipt.json) into %s error: %v", jobDesc.ZipFilePath, err)
  1041  			}
  1042  			_, err = w.Write(receiptBytes)
  1043  			if err != nil {
  1044  				return fmt.Errorf("w.Write(receiptBytes) into %s error: %v", jobDesc.ZipFilePath, err)
  1045  			}
  1046  		}
  1047  
  1048  		return nil
  1049  	}()
  1050  	if err != nil {
  1051  		return err
  1052  	}
  1053  
  1054  	// Remove the workspace so we release the storage space early on before
  1055  	// user dismisses the job.
  1056  	err = os.RemoveAll(workspaceDir)
  1057  	if err != nil {
  1058  		m.simpleFS.log.CWarningf(ctx, "removing workspace %s error %v", workspaceDir, err)
  1059  	}
  1060  
  1061  	return nil
  1062  }
  1063  
  1064  func (m *archiveManager) zippingWorker(ctx context.Context) {
  1065  	err := m.waitForSimpleFSInit(ctx)
  1066  	if err != nil {
  1067  		return
  1068  	}
  1069  
  1070  	for {
  1071  		select {
  1072  		case <-ctx.Done():
  1073  			return
  1074  		case <-m.zippingWorkerSignal:
  1075  		}
  1076  
  1077  		jobID, jobCtx, ok := m.startWorkerTask(ctx,
  1078  			keybase1.SimpleFSArchiveJobPhase_Copied,
  1079  			keybase1.SimpleFSArchiveJobPhase_Zipping)
  1080  
  1081  		if !ok {
  1082  			continue
  1083  		}
  1084  		// We got a task. Put another token into the signal channel so we
  1085  		// check again on the next iteration.
  1086  		m.signal(m.zippingWorkerSignal)
  1087  
  1088  		m.simpleFS.log.CDebugf(ctx, "zipping: %s", jobID)
  1089  
  1090  		err := m.doZipping(jobCtx, jobID)
  1091  		if err == nil {
  1092  			m.simpleFS.log.CDebugf(jobCtx, "zipping done on job %s", jobID)
  1093  			m.changeJobPhase(jobCtx, jobID, keybase1.SimpleFSArchiveJobPhase_Done)
  1094  		} else {
  1095  			m.simpleFS.log.CErrorf(jobCtx, "zipping error on job %s: %v", jobID, err)
  1096  			m.setJobError(ctx, jobID, err)
  1097  		}
  1098  
  1099  		err = m.flushStateFile(ctx)
  1100  		if err != nil {
  1101  			m.simpleFS.log.CWarningf(ctx, "m.flushStateFileLocked error: %v", err)
  1102  		}
  1103  	}
  1104  }
  1105  
  1106  func (m *archiveManager) resetInterruptedPhaseLocked(ctx context.Context, jobID string) (changed bool) {
  1107  	switch m.state.Jobs[jobID].Phase {
  1108  	case keybase1.SimpleFSArchiveJobPhase_Indexing:
  1109  		m.simpleFS.log.CDebugf(ctx, "resetting %s phase from %s to %s", jobID,
  1110  			keybase1.SimpleFSArchiveJobPhase_Indexing,
  1111  			keybase1.SimpleFSArchiveJobPhase_Queued)
  1112  		m.changeJobPhaseLocked(ctx, jobID,
  1113  			keybase1.SimpleFSArchiveJobPhase_Queued)
  1114  		return true
  1115  	case keybase1.SimpleFSArchiveJobPhase_Copying:
  1116  		m.simpleFS.log.CDebugf(ctx, "resetting %s phase from %s to %s", jobID,
  1117  			keybase1.SimpleFSArchiveJobPhase_Copying,
  1118  			keybase1.SimpleFSArchiveJobPhase_Indexed)
  1119  		m.changeJobPhaseLocked(ctx, jobID,
  1120  			keybase1.SimpleFSArchiveJobPhase_Indexed)
  1121  		return true
  1122  	case keybase1.SimpleFSArchiveJobPhase_Zipping:
  1123  		m.simpleFS.log.CDebugf(ctx, "resetting %s phase from %s to %s", jobID,
  1124  			keybase1.SimpleFSArchiveJobPhase_Zipping,
  1125  			keybase1.SimpleFSArchiveJobPhase_Copied)
  1126  		m.changeJobPhaseLocked(ctx, jobID,
  1127  			keybase1.SimpleFSArchiveJobPhase_Copied)
  1128  		return true
  1129  	default:
  1130  		m.simpleFS.log.CDebugf(ctx, "not resetting %s phase from %s", jobID,
  1131  			m.state.Jobs[jobID].Phase)
  1132  		return false
  1133  	}
  1134  }
  1135  
  1136  func (m *archiveManager) errorRetryWorker(ctx context.Context) {
  1137  	err := m.waitForSimpleFSInit(ctx)
  1138  	if err != nil {
  1139  		return
  1140  	}
  1141  
  1142  	ticker := time.NewTicker(time.Second * 5)
  1143  	for {
  1144  		select {
  1145  		case <-ctx.Done():
  1146  			return
  1147  		case <-ticker.C:
  1148  		}
  1149  
  1150  		func() {
  1151  			m.mu.Lock()
  1152  			defer m.mu.Unlock()
  1153  			jobIDs := make([]string, len(m.state.Jobs))
  1154  			for jobID := range m.state.Jobs {
  1155  				jobIDs = append(jobIDs, jobID)
  1156  			}
  1157  		loopJobIDs:
  1158  			for _, jobID := range jobIDs {
  1159  				errState, ok := m.errors[jobID]
  1160  				if !ok {
  1161  					continue loopJobIDs
  1162  				}
  1163  				if time.Now().Before(errState.nextRetry) {
  1164  					continue loopJobIDs
  1165  				}
  1166  				m.simpleFS.log.CDebugf(ctx, "retrying job %s", jobID)
  1167  				changed := m.resetInterruptedPhaseLocked(ctx, jobID)
  1168  				if !changed {
  1169  					m.simpleFS.log.CWarningf(ctx,
  1170  						"job %s has an error state %v but an unexpected job phase",
  1171  						jobID, errState.err)
  1172  					continue loopJobIDs
  1173  				}
  1174  				delete(m.errors, jobID)
  1175  
  1176  				m.signal(m.indexingWorkerSignal)
  1177  				m.signal(m.copyingWorkerSignal)
  1178  				m.signal(m.zippingWorkerSignal)
  1179  			}
  1180  		}()
  1181  	}
  1182  }
  1183  
  1184  func (m *archiveManager) notifyUIStateChangeWorker(ctx context.Context) {
  1185  	err := m.waitForSimpleFSInit(ctx)
  1186  	if err != nil {
  1187  		return
  1188  	}
  1189  
  1190  	limiter := rate.NewLimiter(rate.Every(time.Second/2), 1)
  1191  	for {
  1192  		select {
  1193  		case <-ctx.Done():
  1194  			return
  1195  		case <-m.notifyUIStateChangeSignal:
  1196  		}
  1197  		limiter.Wait(ctx)
  1198  
  1199  		m.notifyUIStateChange(ctx)
  1200  	}
  1201  }
  1202  
  1203  func (m *archiveManager) start() {
  1204  	ctx := context.Background()
  1205  	ctx, m.ctxCancel = context.WithCancel(ctx)
  1206  	go m.indexingWorker(m.simpleFS.makeContext(ctx))
  1207  	go m.copyingWorker(m.simpleFS.makeContext(ctx))
  1208  	go m.zippingWorker(m.simpleFS.makeContext(ctx))
  1209  	go m.errorRetryWorker(m.simpleFS.makeContext(ctx))
  1210  	go m.notifyUIStateChangeWorker(m.simpleFS.makeContext(ctx))
  1211  	m.signal(m.indexingWorkerSignal)
  1212  	m.signal(m.copyingWorkerSignal)
  1213  	m.signal(m.zippingWorkerSignal)
  1214  }
  1215  
  1216  func (m *archiveManager) resetInterruptedPhasesLocked(ctx context.Context) {
  1217  	// We don't resume indexing and zipping work, so just reset them here.
  1218  	// Copying is resumable but we have per file state tracking so reset the
  1219  	// phase here as well.
  1220  	for jobID := range m.state.Jobs {
  1221  		_ = m.resetInterruptedPhaseLocked(ctx, jobID)
  1222  	}
  1223  }
  1224  
  1225  func newArchiveManager(simpleFS *SimpleFS, username libkb.NormalizedUsername) (
  1226  	m *archiveManager, err error) {
  1227  	ctx := context.Background()
  1228  	simpleFS.log.CDebugf(ctx, "+ newArchiveManager")
  1229  	defer simpleFS.log.CDebugf(ctx, "- newArchiveManager")
  1230  	m = &archiveManager{
  1231  		simpleFS:                  simpleFS,
  1232  		username:                  username,
  1233  		jobCtxCancellers:          make(map[string]func()),
  1234  		errors:                    make(map[string]errorState),
  1235  		indexingWorkerSignal:      make(chan struct{}, 1),
  1236  		copyingWorkerSignal:       make(chan struct{}, 1),
  1237  		zippingWorkerSignal:       make(chan struct{}, 1),
  1238  		notifyUIStateChangeSignal: make(chan struct{}, 1),
  1239  	}
  1240  	stateFilePath := m.getStateFilePath(simpleFS)
  1241  	simpleFS.log.CDebugf(ctx, "stateFilePath: %q", stateFilePath)
  1242  	m.state, err = loadArchiveStateFromJsonGz(ctx, simpleFS, stateFilePath)
  1243  	switch err {
  1244  	case nil:
  1245  		if m.state.Jobs == nil {
  1246  			m.state.Jobs = make(map[string]keybase1.SimpleFSArchiveJobState)
  1247  		}
  1248  		m.resetInterruptedPhasesLocked(ctx)
  1249  	default:
  1250  		simpleFS.log.CErrorf(ctx, "loadArchiveStateFromJsonGz error ( %v ). Creating a new state.", err)
  1251  		m.state = &keybase1.SimpleFSArchiveState{
  1252  			Jobs: make(map[string]keybase1.SimpleFSArchiveJobState),
  1253  		}
  1254  		err = writeArchiveStateIntoJsonGz(ctx, simpleFS, stateFilePath, m.state)
  1255  		if err != nil {
  1256  			simpleFS.log.CErrorf(ctx, "newArchiveManager: creating state file error: %v", err)
  1257  			return nil, err
  1258  		}
  1259  	}
  1260  	m.start()
  1261  	return m, nil
  1262  }
  1263  
  1264  func (m *archiveManager) getStagingPath(ctx context.Context, jobID string) (stagingPath string) {
  1265  	cacheDir := m.simpleFS.getCacheDir()
  1266  	return filepath.Join(cacheDir, fmt.Sprintf("kbfs-archive-%s-%s", m.username, jobID))
  1267  }