Skip to content

Commit

Permalink
newSink: Support Windows paths
Browse files Browse the repository at this point in the history
We use `newSink` to decide where the zap.Config.OutputPaths field
intends to send logs. It can be in the form,

    file:///foo/bar
    # or equivalently,
    /foo/bar

Or a user can register a custom sink constructor with `RegisterSink`,
and then use the specified scheme in the output paths.

    zap.RegisterSink("myscheme", ...)

    zap.Config{
        OutputPaths: []string{
            "myscheme://whatever/I/want",
        },
    }

This method of configuration hasn't worked for Windows (ref #994)
because we use `url.Parse` to parse these output paths.

`url.Parse` parses a Windows file path like `C:\foo\bar` to the URL:

    URL{
        Scheme: "c",
        Opaque: `\foo\bar`,
    }

This commit adds support for Windows file paths.
It does so by converting "\" symbols in the output path with "/"
before attempting to parse it with url.Parse. This gives us,

    URL{
        Scheme: "c",
        Path: "/foo/bar",
    }

Following that, we check if the scheme matches the path's "volume name".
On Windows, the volume name of `C:\foo\bar` is `"C:"`.
On Unix, the volume name of all paths is `""`.

This lets us convert the partial file path in the URL
back to a valid Windows file path
and set the scheme to "file" to use the file sink.
  • Loading branch information
abhinav committed Nov 20, 2021
1 parent c508650 commit 82e841b
Showing 1 changed file with 30 additions and 2 deletions.
32 changes: 30 additions & 2 deletions sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"io"
"net/url"
"os"
"path/filepath"
"strings"
"sync"

Expand Down Expand Up @@ -96,11 +97,38 @@ func RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error {
}

func newSink(rawURL string) (Sink, error) {
u, err := url.Parse(rawURL)
// Before we attempt to parse this as a URL,
// ensure that file separators have been replaced with "/".
// Unix systems will be unaffected, and on Windows, this will turn,
// C:\foo\bar
// To,
// C:/foo/bar
// Which will parse as,
// URL{Scheme: "c", Path: "/foo/bar"}
// Note that Scheme is case-insensitive; it's always lower case.
u, err := url.Parse(filepath.ToSlash(rawURL))
if err != nil {
return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err)
}
if u.Scheme == "" {

// Volume name is the "C:" of "C:\foo\bar" on Windows,
// and an empty string on other systems.
volumeName := filepath.VolumeName(rawURL)
switch u.Scheme {
case "":
// Unix:
// Naked paths like "/foo/bar" will
// parse to an empty scheme.
u.Scheme = schemeFile

case strings.ToLower(strings.TrimSuffix(volumeName, ":")): // scheme is lower case
// Windows:
// Naked paths like "C:\foo\bar" will
// parse to "c" as the scheme (lower case)
// after ToSlash normalization above.
//
// Convert it back to C:\foo\bar.
u.Path = filepath.Join(volumeName, filepath.FromSlash(u.Path))
u.Scheme = schemeFile
}

Expand Down

0 comments on commit 82e841b

Please sign in to comment.