[cas] Use pathInfo struct (#336)

Refactor the code to pass pathInfo struct around which is a tuple of
absPath and os.FileInfo. This is a preparation for a CL which actually
adds new functionality, and uses the new struct.
diff --git a/go/pkg/cas/upload.go b/go/pkg/cas/upload.go
index 1fb7ad0..dc88141 100644
--- a/go/pkg/cas/upload.go
+++ b/go/pkg/cas/upload.go
@@ -241,24 +241,29 @@
 			return errors.WithStack(err)
 		}
 
-		_, err = u.visitPath(ctx, absPath, info, in.PathExclude)
+		_, err = u.visitPath(ctx, pathInfo{abs: absPath, FileInfo: info}, in.PathExclude)
 		return errors.Wrapf(err, "%q", absPath)
 	})
 	return nil
 }
 
+type pathInfo struct {
+	abs string
+	os.FileInfo
+}
+
 // visitPath visits the file/dir depending on its type (regular, dir, symlink).
 // Visits each file only once.
 //
 // If the file should be skipped, then returns (nil, nil).
-func (u *uploader) visitPath(ctx context.Context, absPath string, info os.FileInfo, pathExclude *regexp.Regexp) (dirEntry proto.Message, err error) {
+func (u *uploader) visitPath(ctx context.Context, info pathInfo, pathExclude *regexp.Regexp) (dirEntry proto.Message, err error) {
 	// First, check if the file passes all filters.
-	if pathExclude != nil && pathExclude.MatchString(absPath) {
+	if pathExclude != nil && pathExclude.MatchString(info.abs) {
 		return nil, nil
 	}
 	// Call the Prelude only after checking the pathExclude.
 	if u.Prelude != nil {
-		switch err := u.Prelude(absPath, info.Mode()); {
+		switch err := u.Prelude(info.abs, info.Mode()); {
 		case errors.Is(err, ErrSkip):
 			return nil, nil
 		case err != nil:
@@ -272,7 +277,7 @@
 		ExcludeRegexp string
 	}
 	cacheKey := cacheKeyType{
-		AbsPath: absPath,
+		AbsPath: info.abs,
 	}
 	// Incorporate the pathExclude, unless it is a regular file.
 	// If it is a regular file, then there's no need to include pathExclude in the
@@ -285,12 +290,12 @@
 	node, err := u.fsCache.LoadOrStore(cacheKey, func() (interface{}, error) {
 		switch {
 		case info.Mode()&os.ModeSymlink == os.ModeSymlink:
-			return u.visitSymlink(ctx, absPath, pathExclude)
+			return u.visitSymlink(ctx, info, pathExclude)
 		case info.Mode().IsDir():
-			return u.visitDir(ctx, absPath, pathExclude)
+			return u.visitDir(ctx, info, pathExclude)
 		case info.Mode().IsRegular():
 			// Code above assumes that pathExclude is not used here.
-			return u.visitRegularFile(ctx, absPath, info)
+			return u.visitRegularFile(ctx, info)
 		default:
 			return nil, fmt.Errorf("unexpected file mode %s", info.Mode())
 		}
@@ -320,7 +325,7 @@
 //    network disks. Reading many large files concurrently appears to saturate
 //    the network and slows down the progress.
 //    See also ClientConfig.LargeFileThreshold.
-func (u *uploader) visitRegularFile(ctx context.Context, absPath string, info os.FileInfo) (*repb.FileNode, error) {
+func (u *uploader) visitRegularFile(ctx context.Context, info pathInfo) (*repb.FileNode, error) {
 	isLarge := info.Size() >= u.Config.LargeFileThreshold
 
 	// Lock the mutex before acquiring a semaphore to avoid hogging the latter.
@@ -335,7 +340,7 @@
 	}
 	defer u.semFileIO.Release(1)
 
-	f, err := u.openFileSource(absPath)
+	f, err := u.openFileSource(info.abs)
 	if err != nil {
 		return nil, err
 	}
@@ -352,7 +357,7 @@
 		if err != nil {
 			return nil, err
 		}
-		item := uploadItemFromBlob(absPath, contents)
+		item := uploadItemFromBlob(info.abs, contents)
 		ret.Digest = item.Digest
 		return ret, u.scheduleCheck(ctx, item)
 	}
@@ -367,7 +372,7 @@
 	ret.Digest = dig.ToProto()
 
 	item := &uploadItem{
-		Title:  absPath,
+		Title:  info.abs,
 		Digest: ret.Digest,
 	}
 
@@ -388,7 +393,7 @@
 	// item.Open will reopen the file.
 
 	item.Open = func() (uploadSource, error) {
-		return u.openFileSource(absPath)
+		return u.openFileSource(info.abs)
 	}
 	return ret, u.scheduleCheck(ctx, item)
 }
@@ -404,7 +409,7 @@
 // visitDir reads a directory and its descendants. The function blocks until
 // each descendant is visited, but the visitation happens concurrently, using
 // u.eg.
-func (u *uploader) visitDir(ctx context.Context, absPath string, pathExclude *regexp.Regexp) (*repb.DirectoryNode, error) {
+func (u *uploader) visitDir(ctx context.Context, info pathInfo, pathExclude *regexp.Regexp) (*repb.DirectoryNode, error) {
 	var mu sync.Mutex
 	dir := &repb.Directory{}
 	var subErr error
@@ -418,7 +423,7 @@
 		}
 		defer u.semFileIO.Release(1)
 
-		f, err := os.Open(absPath)
+		f, err := os.Open(info.abs)
 		if err != nil {
 			return err
 		}
@@ -426,7 +431,7 @@
 
 		// Check the context, since file IO functions don't.
 		for ctx.Err() == nil {
-			infos, err := f.Readdir(128)
+			children, err := f.Readdir(128)
 			if err == io.EOF {
 				break
 			}
@@ -434,15 +439,17 @@
 				return err
 			}
 
-			for _, info := range infos {
-				info := info
-				absChild := joinFilePathsFast(absPath, info.Name())
+			for _, ci := range children {
+				child := pathInfo{
+					abs:      joinFilePathsFast(info.abs, ci.Name()),
+					FileInfo: ci,
+				}
 				wgChildren.Add(1)
 				u.wgFS.Add(1)
 				u.eg.Go(func() error {
 					defer wgChildren.Done()
 					defer u.wgFS.Done()
-					node, err := u.visitPath(ctx, absChild, info, pathExclude)
+					node, err := u.visitPath(ctx, child, pathExclude)
 					mu.Lock()
 					defer mu.Unlock()
 					if err != nil {
@@ -476,15 +483,15 @@
 
 	wgChildren.Wait()
 	if subErr != nil {
-		return nil, errors.Wrapf(subErr, "failed to read the directory %q entirely", absPath)
+		return nil, errors.Wrapf(subErr, "failed to read the directory %q entirely", info.abs)
 	}
 
-	item := uploadItemFromDirMsg(absPath, dir)
+	item := uploadItemFromDirMsg(info.abs, dir)
 	if err := u.scheduleCheck(ctx, item); err != nil {
 		return nil, err
 	}
 	return &repb.DirectoryNode{
-		Name:   filepath.Base(absPath),
+		Name:   filepath.Base(info.abs),
 		Digest: item.Digest,
 	}, nil
 }
@@ -493,15 +500,15 @@
 // of the target file.
 // If u.PreserveSymlinks is true, then returns a SymlinkNode, otherwise
 // returns the directory node of the target file.
-func (u *uploader) visitSymlink(ctx context.Context, absPath string, pathExclude *regexp.Regexp) (proto.Message, error) {
-	target, err := os.Readlink(absPath)
+func (u *uploader) visitSymlink(ctx context.Context, info pathInfo, pathExclude *regexp.Regexp) (proto.Message, error) {
+	target, err := os.Readlink(info.abs)
 	if err != nil {
 		return nil, errors.Wrapf(err, "os.ReadLink")
 	}
 
 	// Determine absolute and relative paths of the target.
 	var absTarget, relTarget string
-	symlinkDir := filepath.Dir(absPath)
+	symlinkDir := filepath.Dir(info.abs)
 	target = filepath.Clean(target) // target may end with slash
 	if filepath.IsAbs(target) {
 		absTarget = target
@@ -516,7 +523,7 @@
 	}
 
 	symlinkNode := &repb.SymlinkNode{
-		Name:   filepath.Base(absPath),
+		Name:   filepath.Base(info.abs),
 		Target: filepath.ToSlash(relTarget),
 	}
 
@@ -529,7 +536,7 @@
 		return nil, errors.WithStack(err)
 	}
 
-	switch targetNode, err := u.visitPath(ctx, absTarget, targetInfo, pathExclude); {
+	switch targetNode, err := u.visitPath(ctx, pathInfo{abs: absTarget, FileInfo: targetInfo}, pathExclude); {
 	case err != nil:
 		return nil, err
 	case !u.PreserveSymlinks:
@@ -537,7 +544,7 @@
 	case targetNode == nil && !u.AllowDanglingSymlinks:
 		// The target got skipped via Prelude or PathExclude,
 		// resulting in a dangling symlink, which is not allowed.
-		return nil, errors.Wrapf(ErrFilteredSymlinkTarget, "path: %q, target: %q", absPath, target)
+		return nil, errors.Wrapf(ErrFilteredSymlinkTarget, "path: %q, target: %q", info.abs, target)
 	default:
 		// Note: even though we throw away targetNode, it was still important to visit the target.
 		return symlinkNode, nil