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 }