func appendAllToFile(src io.Reader, dest string, existingBytes int64, totalBytes int64) error { out, _ := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) defer out.Close() prevPercent := 0.0 comm.StartProgress() onWrite := func(bytesDownloaded int64) { bytesWritten := existingBytes + bytesDownloaded percent := float64(bytesWritten) / float64(totalBytes) if math.Abs(percent-prevPercent) < 0.0001 { return } prevPercent = percent comm.Progress(percent) } counter := counter.NewWriterCallback(onWrite, out) _, err := io.Copy(counter, src) if err != nil { return errors.Wrap(err, 1) } comm.EndProgress() return nil }
func doPush(buildPath string, specStr string, userVersion string, fixPerms bool) error { // start walking source container while waiting on auth flow sourceContainerChan := make(chan walkResult) walkErrs := make(chan error) go doWalk(buildPath, sourceContainerChan, walkErrs, fixPerms) spec, err := itchio.ParseSpec(specStr) if err != nil { return errors.Wrap(err, 1) } err = spec.EnsureChannel() if err != nil { return errors.Wrap(err, 1) } client, err := authenticateViaOauth() if err != nil { return errors.Wrap(err, 1) } newBuildRes, err := client.CreateBuild(spec.Target, spec.Channel, userVersion) if err != nil { return errors.Wrap(err, 1) } buildID := newBuildRes.Build.ID parentID := newBuildRes.Build.ParentBuild.ID var targetSignature *pwr.SignatureInfo if parentID == 0 { comm.Opf("For channel `%s`: pushing first build", spec.Channel) targetSignature = &pwr.SignatureInfo{ Container: &tlc.Container{}, Hashes: make([]wsync.BlockHash, 0), } } else { comm.Opf("For channel `%s`: last build is %d, downloading its signature", spec.Channel, parentID) var buildFiles itchio.ListBuildFilesResponse buildFiles, err = client.ListBuildFiles(parentID) if err != nil { return errors.Wrap(err, 1) } signatureFile := itchio.FindBuildFile(itchio.BuildFileType_SIGNATURE, buildFiles.Files) if signatureFile == nil { comm.Dief("Could not find signature for parent build %d, aborting", parentID) } var signatureReader io.Reader signatureReader, err = client.DownloadBuildFile(parentID, signatureFile.ID) if err != nil { return errors.Wrap(err, 1) } targetSignature, err = pwr.ReadSignature(signatureReader) if err != nil { return errors.Wrap(err, 1) } } newPatchRes, newSignatureRes, err := createBothFiles(client, buildID) if err != nil { return errors.Wrap(err, 1) } uploadDone := make(chan bool) uploadErrs := make(chan error) patchWriter, err := uploader.NewResumableUpload(newPatchRes.File.UploadURL, uploadDone, uploadErrs, uploader.ResumableUploadSettings{ Consumer: comm.NewStateConsumer(), }) patchWriter.MaxChunkGroup = *appArgs.maxChunkGroup if err != nil { return errors.Wrap(err, 1) } signatureWriter, err := uploader.NewResumableUpload(newSignatureRes.File.UploadURL, uploadDone, uploadErrs, uploader.ResumableUploadSettings{ Consumer: comm.NewStateConsumer(), }) signatureWriter.MaxChunkGroup = *appArgs.maxChunkGroup if err != nil { return errors.Wrap(err, 1) } comm.Debugf("Launching patch & signature channels") patchCounter := counter.NewWriter(patchWriter) signatureCounter := counter.NewWriter(signatureWriter) // we started walking the source container in the beginning, // we actually need it now. // note that we could actually start diffing before all the file // creation & upload setup is done var sourceContainer *tlc.Container var sourcePool wsync.Pool comm.Debugf("Waiting for source container") select { case walkErr := <-walkErrs: return errors.Wrap(walkErr, 1) case walkies := <-sourceContainerChan: comm.Debugf("Got sourceContainer!") sourceContainer = walkies.container sourcePool = walkies.pool break } comm.Opf("Pushing %s (%s)", humanize.IBytes(uint64(sourceContainer.Size)), sourceContainer.Stats()) comm.Debugf("Building diff context") var readBytes int64 bytesPerSec := float64(0) lastUploadedBytes := int64(0) stopTicking := make(chan struct{}) updateProgress := func() { uploadedBytes := int64(float64(patchWriter.UploadedBytes)) // input bytes that aren't in output, for example: // - bytes that have been compressed away // - bytes that were in old build and were simply reused goneBytes := readBytes - patchWriter.TotalBytes conservativeTotalBytes := sourceContainer.Size - goneBytes leftBytes := conservativeTotalBytes - uploadedBytes if leftBytes > AlmostThereThreshold { netStatus := "- network idle" if bytesPerSec > 1 { netStatus = fmt.Sprintf("@ %s/s", humanize.IBytes(uint64(bytesPerSec))) } comm.ProgressLabel(fmt.Sprintf("%s, %s left", netStatus, humanize.IBytes(uint64(leftBytes)))) } else { comm.ProgressLabel(fmt.Sprintf("- almost there")) } conservativeProgress := float64(uploadedBytes) / float64(conservativeTotalBytes) conservativeProgress = min(1.0, conservativeProgress) comm.Progress(conservativeProgress) comm.ProgressScale(float64(readBytes) / float64(sourceContainer.Size)) } go func() { ticker := time.NewTicker(time.Second * time.Duration(2)) for { select { case <-ticker.C: bytesPerSec = float64(patchWriter.UploadedBytes-lastUploadedBytes) / 2.0 lastUploadedBytes = patchWriter.UploadedBytes updateProgress() case <-stopTicking: break } } }() patchWriter.OnProgress = updateProgress stateConsumer := &state.Consumer{ OnProgress: func(progress float64) { readBytes = int64(float64(sourceContainer.Size) * progress) updateProgress() }, } dctx := &pwr.DiffContext{ Compression: &pwr.CompressionSettings{ Algorithm: pwr.CompressionAlgorithm_BROTLI, Quality: 1, }, SourceContainer: sourceContainer, Pool: sourcePool, TargetContainer: targetSignature.Container, TargetSignature: targetSignature.Hashes, Consumer: stateConsumer, } comm.StartProgress() comm.ProgressScale(0.0) err = dctx.WritePatch(patchCounter, signatureCounter) if err != nil { return errors.Wrap(err, 1) } // close in a goroutine to avoid deadlocking doClose := func(c io.Closer, done chan bool, errs chan error) { closeErr := c.Close() if closeErr != nil { errs <- errors.Wrap(closeErr, 1) return } done <- true } go doClose(patchWriter, uploadDone, uploadErrs) go doClose(signatureWriter, uploadDone, uploadErrs) for c := 0; c < 4; c++ { select { case uploadErr := <-uploadErrs: return errors.Wrap(uploadErr, 1) case <-uploadDone: comm.Debugf("upload done") } } close(stopTicking) comm.ProgressLabel("finalizing build") finalDone := make(chan bool) finalErrs := make(chan error) doFinalize := func(fileID int64, fileSize int64, done chan bool, errs chan error) { _, err = client.FinalizeBuildFile(buildID, fileID, fileSize) if err != nil { errs <- errors.Wrap(err, 1) return } done <- true } go doFinalize(newPatchRes.File.ID, patchCounter.Count(), finalDone, finalErrs) go doFinalize(newSignatureRes.File.ID, signatureCounter.Count(), finalDone, finalErrs) for i := 0; i < 2; i++ { select { case err := <-finalErrs: return errors.Wrap(err, 1) case <-finalDone: } } comm.EndProgress() { prettyPatchSize := humanize.IBytes(uint64(patchCounter.Count())) percReused := 100.0 * float64(dctx.ReusedBytes) / float64(dctx.FreshBytes+dctx.ReusedBytes) relToNew := 100.0 * float64(patchCounter.Count()) / float64(sourceContainer.Size) prettyFreshSize := humanize.IBytes(uint64(dctx.FreshBytes)) savings := 100.0 - relToNew if dctx.ReusedBytes > 0 { comm.Statf("Re-used %.2f%% of old, added %s fresh data", percReused, prettyFreshSize) } else { comm.Statf("Added %s fresh data", prettyFreshSize) } if savings > 0 && !math.IsNaN(savings) { comm.Statf("%s patch (%.2f%% savings)", prettyPatchSize, 100.0-relToNew) } else { comm.Statf("%s patch (no savings)", prettyPatchSize) } } comm.Opf("Build is now processing, should be up in a bit (see `butler status`)") comm.Logf("") return nil }
func doCp(srcPath string, destPath string, resume bool) error { src, err := eos.Open(srcPath) if err != nil { return err } defer src.Close() dir := filepath.Dir(destPath) err = os.MkdirAll(dir, 0755) if err != nil { return err } flags := os.O_CREATE | os.O_WRONLY dest, err := os.OpenFile(destPath, flags, 0644) if err != nil { return err } defer dest.Close() stats, err := src.Stat() if err != nil { return err } totalBytes := int64(stats.Size()) startOffset := int64(0) if resume { startOffset, err = dest.Seek(0, os.SEEK_END) if err != nil { return err } if startOffset == 0 { comm.Logf("Downloading %s", humanize.IBytes(uint64(totalBytes))) } else if startOffset > totalBytes { comm.Logf("Existing data too big (%s > %s), starting over", humanize.IBytes(uint64(startOffset)), humanize.IBytes(uint64(totalBytes))) } else if startOffset == totalBytes { comm.Logf("All %s already there", humanize.IBytes(uint64(totalBytes))) return nil } comm.Logf("Resuming at %s / %s", humanize.IBytes(uint64(startOffset)), humanize.IBytes(uint64(totalBytes))) _, err = src.Seek(startOffset, os.SEEK_SET) if err != nil { return err } } else { comm.Logf("Downloading %s", humanize.IBytes(uint64(totalBytes))) } start := time.Now() comm.Progress(float64(startOffset) / float64(totalBytes)) comm.StartProgressWithTotalBytes(totalBytes) cw := counter.NewWriterCallback(func(count int64) { alpha := float64(startOffset+count) / float64(totalBytes) comm.Progress(alpha) }, dest) copiedBytes, err := io.Copy(cw, src) if err != nil { return err } comm.EndProgress() totalDuration := time.Since(start) prettyStartOffset := humanize.IBytes(uint64(startOffset)) prettySize := humanize.IBytes(uint64(copiedBytes)) perSecond := humanize.IBytes(uint64(float64(totalBytes-startOffset) / totalDuration.Seconds())) comm.Statf("%s + %s copied @ %s/s\n", prettyStartOffset, prettySize, perSecond) return nil }
// Does not preserve users, nor permission, except the executable bit func ditto(src string, dst string) { comm.Debugf("rsync -a %s %s", src, dst) totalSize := int64(0) doneSize := int64(0) oldProgress := 0.0 inc := func(_ string, f os.FileInfo, err error) error { if err != nil { return nil } totalSize += f.Size() return nil } onFile := func(path string, f os.FileInfo, err error) error { if err != nil { comm.Logf("ignoring error %s", err.Error()) return nil } rel, err := filepath.Rel(src, path) must(err) dstpath := filepath.Join(dst, rel) mode := f.Mode() switch { case mode.IsDir(): dittoMkdir(dstpath) case mode.IsRegular(): dittoReg(path, dstpath, os.FileMode(f.Mode()&archiver.LuckyMode|archiver.ModeMask)) case (mode&os.ModeSymlink > 0): dittoSymlink(path, dstpath, f) } comm.Debug(rel) doneSize += f.Size() progress := float64(doneSize) / float64(totalSize) if progress-oldProgress > 0.01 { oldProgress = progress comm.Progress(progress) } return nil } rootinfo, err := os.Lstat(src) must(err) if rootinfo.IsDir() { totalSize = 0 comm.Logf("Counting files in %s...", src) filepath.Walk(src, inc) comm.Logf("Mirroring...") filepath.Walk(src, onFile) } else { totalSize = rootinfo.Size() onFile(src, rootinfo, nil) } comm.EndProgress() }