Repository: C2FO/vfs
Pull request #113 (2022-02-17 09:12:54)

Vfs util path2file

codecov[bot]: # [Codecov](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) Report
> Merging [#113](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) (11a6790) into [master](https://codecov.io/gh/C2FO/vfs/commit/39abb911747b92fa09283572daa64b69a95fa3df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) (39abb91) will **decrease** coverage by `0.04%`.
> The diff coverage is `61.11%`.

[![Impacted file tree graph](https://codecov.io/gh/C2FO/vfs/pull/113/graphs/tree.svg?width=650&height=150&src=pr&token=vCl9BUzOBW&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO)](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO)

```diff
@@            Coverage Diff             @@
##           master     #113      +/-   ##
==========================================
- Coverage   67.59%   67.55%   -0.05%     
==========================================
  Files          34       34              
  Lines        2466     2484      +18     
==========================================
+ Hits         1667     1678      +11     
- Misses        553      556       +3     
- Partials      246      250       +4     
```


| [Impacted Files](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) | Coverage Δ | |
|---|---|---|
| [utils/utils.go](https://codecov.io/gh/C2FO/vfs/pull/113/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO#diff-dXRpbHMvdXRpbHMuZ28=) | `70.66% <61.11%> (-3.02%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO). Last update [39abb91...11a6790](https://codecov.io/gh/C2FO/vfs/pull/113?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5bcde1..4c6e2fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 ## [Unreleased]
 
+## [6.1.0]
+### Added
+- Added utils.PathToURI() func to change path to OS URI.  resolves #112
+
 ## [6.0.2]
 ### Fixed
 - fixed linting issues with missing godoc on exported functions and new build tag formatting.
diff --git a/docs/utils.md b/docs/utils.md
index 4902358..6c73cf3 100644
--- a/docs/utils.md
+++ b/docs/utils.md
@@ -18,8 +18,8 @@ const (
 	// ErrBadAbsLocationPath constant is returned when a file path is not absolute
 	ErrBadAbsLocationPath = "absolute location path is invalid - must include leading and trailing slashes"
 	// ErrBadRelLocationPath constant is returned when a file path is not relative
-	ErrBadRelLocationPath = "relative location path is invalid - may not include leading slash but must include trailing slash" 
-	// TouchCopyMinBufferSize min buffer size used in TouchCopyBuffered in bytes 
+	ErrBadRelLocationPath = "relative location path is invalid - may not include leading slash but must include trailing slash"
+	// TouchCopyMinBufferSize min buffer size used in TouchCopyBuffered in bytes
 	TouchCopyMinBufferSize = 262144
 )
 ```
@@ -54,6 +54,27 @@ func GetLocationURI(l vfs.Location) string
 ```
 GetLocationURI returns a Location URI
 
+#### func  PathToURI
+
+```go
+func PathToURI(p string) (string, error)
+```
+PathToURI takes a relative or absolute path and returns an OS URI. 
+* We assume
+non-scheme path is an OS File or Location. 
+* We assume volume(URI authority) is
+empty. 
+* We assume relative paths are relative to the pwd (program's working
+directory)
+* We assum an empty path is equal to the root path: "/"
+
+| original path | becomes URI |
+| --------------- | --------------- |
+| /absolute/path/to/file.txt | file:///absolute/path/to/file.txt |
+| /some/absolute/path/ | file:///absolute/path/ |
+| relative/path/to/file.txt | file:///absolute/path/with/relative/path/to/file.txt |
+| relative/path/ | file:///absolute/path/with/relative/path/ |
+
 #### func  RemoveLeadingSlash
 
 ```go
@@ -69,22 +90,25 @@ func RemoveTrailingSlash(path string) string
 RemoveTrailingSlash removes trailing slash, if any
 
 #### func  TouchCopy
- 
+
 ```go
 func TouchCopy(writer io.Writer, reader io.Reader) error
 ```
-Deprecated: Use TouchCopyBuffer Instead. TouchCopy is a wrapper around [io.Copy](https://godoc.org/io#Copy) which ensures that even empty source files
-(reader) will get written as an empty file. It guarantees a Write() call on the target file.
+TouchCopy is a wrapper around [io.Copy](https://godoc.org/io#Copy) which ensures that even empty source files
+(reader) will get written as an empty file. It guarantees a Write() call on the
+target file. Deprecated: Use TouchCopyBuffer Instead
 
 #### func  TouchCopyBuffered
 
 ```go
-func TouchCopyBuffered(writer io.Writer, reader io.Reader, bufferSize int)
+func TouchCopyBuffered(writer io.Writer, reader io.Reader, bufferSize int) error
 ```
-TouchCopy is a wrapper around [io.CopyBuffer](https://godoc.org/io#CopyBuffer) which ensures that even empty source files
-(reader) will get written as an empty file. It guarantees a Write() call on the target file. Furthermore, it will use a
-buffer that is equal to bufferSize measured in bytes. If bufferSize is 0, then a default buffer of TouchCopyMinBufferSize 
-bytes is used.
+TouchCopyBuffered is a wrapper around [io.CopyBuffer](https://godoc.org/io#CopyBuffer) which ensures that even
+empty source files (reader) will get written as an empty file. It guarantees a
+Write() call on the target file. bufferSize is in bytes and if is less than
+TouchCopyMinBufferSize will result in a buffer of size TouchCopyMinBufferSize
+bytes. If bufferSize is > TouchCopyMinBufferSize it will result in a buffer of
+size bufferSize bytes
 
 #### func  UpdateLastModifiedByMoving
 
diff --git a/utils/utils.go b/utils/utils.go
index daf089b..6e40f16 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -4,6 +4,8 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net/url"
+	"path/filepath"
 	"regexp"
 	"strings"
 	"time"
@@ -98,6 +100,48 @@ func EnsureLeadingSlash(dir string) string {
 	return "/" + dir
 }
 
+// PathToURI takes a relative or absolute path and returns an OS URI.
+// We assume non-scheme path is an OS File or Location.
+// We assume volume(URI authority) is empty.
+// We assume relative paths are relative to the pwd (program's working directory)
+//
+// /absolute/path/to/file.txt : file:///absolute/path/to/file.txt
+// /some/absolute/path/       : file:///absolute/path/
+// relative/path/to/file.txt  : file:///absolute/path/with/relative/path/to/file.txt
+// relative/path/             : file:///absolute/path/with/relative/path/
+func PathToURI(p string) (string, error) {
+	if p == "" {
+		p = "/"
+	}
+	var URI string
+
+	// parse path
+	u, err := url.Parse(p)
+	if err != nil {
+		return "", err
+	}
+
+	// if scheme is found, its already a URI
+	if u.Scheme != "" {
+		return p, nil
+	}
+
+	// make absolute path (if not already)
+	absPath, err := filepath.Abs(p)
+	if err != nil {
+		return "", err
+	}
+
+	// Abs() strips trailing slashes so add back if original path had slash
+	if p[len(p)-1:] == "/" {
+		absPath = EnsureTrailingSlash(absPath)
+	}
+
+	URI = "file://" + absPath
+
+	return URI, err
+}
+
 // TouchCopy is a wrapper around io.Copy which ensures that even empty source files (reader) will get written as an
 // empty file. It guarantees a Write() call on the target file.
 // Deprecated: Use TouchCopyBuffer Instead
diff --git a/vfscp/vfscp.go b/vfscp/vfscp.go
index 2704d36..0075e20 100644
--- a/vfscp/vfscp.go
+++ b/vfscp/vfscp.go
@@ -3,12 +3,11 @@ package main
 import (
 	"flag"
 	"fmt"
-	"net/url"
 	"os"
-	"path/filepath"
 
 	"github.com/fatih/color"
 
+	"github.com/c2fo/vfs/v6/utils"
 	"github.com/c2fo/vfs/v6/vfssimple"
 )
 
@@ -48,11 +47,11 @@ func main() {
 
 	fmt.Println("")
 
-	srcFileURI, err := normalizeArgs(flag.Arg(0))
+	srcFileURI, err := utils.PathToURI(flag.Arg(0))
 	if err != nil {
 		panic(err)
 	}
-	targetFileURI, err := normalizeArgs(flag.Arg(1))
+	targetFileURI, err := utils.PathToURI(flag.Arg(1))
 	if err != nil {
 		panic(err)
 	}
@@ -82,24 +81,6 @@ func copyFiles(srcFileURI, targetFileURI string) {
 
 }
 
-func normalizeArgs(str string) (string, error) {
-	var normalizedArg string
-	u, err := url.Parse(str)
-	if err != nil {
-		return "", err
-	}
-	if u.IsAbs() {
-		normalizedArg = str
-	} else {
-		absPath, err := filepath.Abs(str)
-		if err != nil {
-			return "", err
-		}
-		normalizedArg = "file://" + absPath
-	}
-	return normalizedArg, err
-}
-
 func failMessage(err error) {
 	red := color.New(color.FgHiRed).Add(color.Bold)
 	fmt.Printf(red.Sprint("failed\n\n")+"\n%s\n\n", err.Error())


Test patch:
diff --git a/utils/utils_test.go b/utils/utils_test.go
index 44b6886..e13f948 100644
--- a/utils/utils_test.go
+++ b/utils/utils_test.go
@@ -1,6 +1,7 @@
 package utils_test
 
 import (
+	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
@@ -422,6 +423,88 @@ func (s *utilsTest) TestValidateRelLocationPath() {
 	}
 }
 
+type URITest struct {
+	path     string
+	expected string
+	message  string
+	isRegex  bool
+}
+
+func (s *utilsTest) TestPathToURI() {
+	tests := []URITest{
+		{
+			path:     "/absolute/path/",
+			expected: "file:///absolute/path/",
+			message:  "absolute location path",
+		},
+		{
+			path:     "/absolute/path",
+			expected: "file:///absolute/path",
+			message:  "absolute file path - looks like location",
+		},
+		{
+			path:     "/absolute/path/to/file.txt",
+			expected: "file:///absolute/path/to/file.txt",
+			message:  "absolute file path",
+		},
+		{
+			path:     "relative/path/",
+			expected: "file:///.+?/relative/path/",
+			message:  "relative location path",
+			isRegex:  true,
+		},
+		{
+			path:     "relative/path",
+			expected: "file:///.+?/relative/path",
+			message:  "relative file path - looks like location",
+			isRegex:  true,
+		},
+		{
+			path:     "relative/path/to/file.txt",
+			expected: "file:///.+?/relative/path/to/file.txt",
+			message:  "relative file path",
+			isRegex:  true,
+		},
+		{
+			path:     "/",
+			expected: "file:///",
+			message:  "just a slash - root location",
+		},
+		{
+			path:     "",
+			expected: "file:///",
+			message:  "empty string - root location",
+		},
+		{
+			path:     "file:///some/path/file.txt",
+			expected: "file:///some/path/file.txt",
+			message:  "already a URI - OS Scheme",
+		},
+		{
+			path:     "s3://bucket/some/path/file.txt",
+			expected: "s3://bucket/some/path/file.txt",
+			message:  "already a URI - other Scheme",
+		},
+	}
+
+	for _, slashtest := range tests {
+		uri, err := utils.PathToURI(slashtest.path)
+		s.Require().NoError(err, "no error expected")
+		if slashtest.isRegex {
+			s.Regexp(slashtest.expected, uri, slashtest.message)
+		} else {
+			s.Equal(slashtest.expected, uri, slashtest.message)
+		}
+	}
+
+	// test error return from bad uri.parse
+	const nullChar = '\u0000'
+	// parse path with null character
+	_, err := utils.PathToURI(fmt.Sprintf("/some%s/path/", string(nullChar)))
+	s.Error(err, "expected error on ctrl char in path")
+	s.EqualError(err, "parse \"/some\\x00/path/\": net/url: invalid control character in URL")
+}
+
 func (s *utilsTest) TestGetURI() {
 
 	// set up mocks