Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows support #207

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
go-version: [1.22, 1.23]
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Windows support in the os backend.
### Fixed
- Ability to run all unit tests on Windows.

## [6.22.0] - 2024-11-06
### Fixed
Expand Down
8 changes: 2 additions & 6 deletions backend/azure/fileSystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net/url"
"os"
"path"
"regexp"
"strings"
Expand Down Expand Up @@ -133,11 +132,8 @@ func ParsePath(p string) (host, pth string, err error) {
if p == "/" {
return "", "", errors.New("no container specified for Azure path")
}
var isLocation bool
if p[len(p)-1:] == string(os.PathSeparator) {
isLocation = true
}
l := strings.Split(p, string(os.PathSeparator))
isLocation := strings.HasSuffix(p, "/")
l := strings.Split(p, "/")
p = utils.EnsureLeadingSlash(path.Join(l[2:]...))
if isLocation {
p = utils.EnsureTrailingSlash(p)
Expand Down
7 changes: 4 additions & 3 deletions backend/mem/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func (s *memFileTest) SetupTest() {
s.NoError(s.testFile.Touch(), "unexpected error touching file")
}

func (s *memFileTest) TeardownTest() {
func (s *memFileTest) TearDownTest() {
err := s.testFile.Close()
s.NoError(err, "close error not expected")
s.NoError(s.testFile.Delete(), "delete failed unexpectedly")
_ = s.testFile.Delete()
}

// TestZBR ensures that we can always read zero bytes
Expand Down Expand Up @@ -382,14 +382,15 @@ func (s *memFileTest) TestCopyToLocationOS() {

s.NotNil(copiedFile)
s.Equal("/test_files/test.txt", s.testFile.Path()) // testFile's path should be unchanged
s.Equal(filepath.Join(dir, "test.txt"), copiedFile.Path()) // new path should be that
s.Equal(path.Join(osFile.Location().Path(), "test.txt"), copiedFile.Path()) // new path should be that

_, err = copiedFile.Read(readSlice)
s.NoError(err, "unexpected read error")

_, err = s.testFile.Read(readSlice2)
s.NoError(err, "unexpected read error")
s.Equal(readSlice2, readSlice) // both reads should be the same
s.Require().NoError(copiedFile.Close())
cleanErr := os.RemoveAll(dir) // clean up
s.NoError(cleanErr, "unexpected error cleaning up osFiles")
}
Expand Down
59 changes: 38 additions & 21 deletions backend/os/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"time"

"github.com/c2fo/vfs/v6"
Expand All @@ -22,6 +23,7 @@ type opener func(filePath string) (*os.File, error)
// File implements vfs.File interface for os fs.
type File struct {
file *os.File
volume string
name string
filesystem *FileSystem
cursorPos int64
Expand All @@ -34,7 +36,7 @@ type File struct {

// Delete unlinks the file returning any error or nil.
func (f *File) Delete(_ ...options.DeleteOption) error {
err := os.Remove(f.Path())
err := os.Remove(osFilePath(f))
if err == nil {
f.file = nil
}
Expand All @@ -43,7 +45,7 @@ func (f *File) Delete(_ ...options.DeleteOption) error {

// LastModified returns the timestamp of the file's mtime or error, if any.
func (f *File) LastModified() (*time.Time, error) {
stats, err := os.Stat(f.Path())
stats, err := os.Stat(osFilePath(f))
if err != nil {
return nil, err
}
Expand All @@ -66,12 +68,12 @@ func (f *File) Name() string {
//
// someFile.Location().Path()
func (f *File) Path() string {
return filepath.Join(f.Location().Path(), f.Name())
return path.Join(f.Location().Path(), f.Name())
}

// Size returns the size (in bytes) of the File or any error.
func (f *File) Size() (uint64, error) {
stats, err := os.Stat(f.Path())
stats, err := os.Stat(osFilePath(f))
if err != nil {
return 0, err
}
Expand Down Expand Up @@ -181,7 +183,7 @@ func (f *File) Seek(offset int64, whence int) (int64, error) {

// Exists true if the file exists on the file system, otherwise false, and an error, if any.
func (f *File) Exists() (bool, error) {
_, err := os.Stat(f.Path())
_, err := os.Stat(osFilePath(f))
if err != nil {
// file does not exist
if os.IsNotExist(err) {
Expand Down Expand Up @@ -217,19 +219,22 @@ func (f *File) Write(p []byte) (n int, err error) {
func (f *File) Location() vfs.Location {
return &Location{
fileSystem: f.filesystem,
volume: f.volume,
name: utils.EnsureTrailingSlash(path.Dir(f.name)),
}
}

// MoveToFile move a file. It accepts a target vfs.File and returns an error, if any.
func (f *File) MoveToFile(file vfs.File) error {
// validate seek is at 0,0 before doing copy
if err := backend.ValidateCopySeekPosition(f); err != nil {
return err
if f.file != nil {
// validate seek is at 0,0 before doing copy
if err := backend.ValidateCopySeekPosition(f); err != nil {
return err
}
}
// handle native os move/rename
if file.Location().FileSystem().Scheme() == Scheme {
return safeOsRename(f.Path(), file.Path())
return safeOsRename(osFilePath(f), osFilePath(file))
}

// do copy/delete move for non-native os moves
Expand All @@ -245,7 +250,7 @@ func safeOsRename(srcName, dstName string) error {
err := os.Rename(srcName, dstName)
if err != nil {
e, ok := err.(*os.LinkError)
if ok && e.Err.Error() == osCrossDeviceLinkError {
if ok && (e.Err.Error() == osCrossDeviceLinkError || (runtime.GOOS == "windows" && e.Err.Error() == "Access is denied.")) {
// do cross-device renaming
if err := osCopy(srcName, dstName); err != nil {
return err
Expand Down Expand Up @@ -306,9 +311,11 @@ func (f *File) MoveToLocation(location vfs.Location) (vfs.File, error) {

// CopyToFile copies the file to a new File. It accepts a vfs.File and returns an error, if any.
func (f *File) CopyToFile(file vfs.File) error {
// validate seek is at 0,0 before doing copy
if err := backend.ValidateCopySeekPosition(f); err != nil {
return err
if f.file != nil {
// validate seek is at 0,0 before doing copy
if err := backend.ValidateCopySeekPosition(f); err != nil {
return err
}
}
_, err := f.copyWithName(file.Name(), file.Location())
return err
Expand All @@ -317,9 +324,11 @@ func (f *File) CopyToFile(file vfs.File) error {
// CopyToLocation copies existing File to new Location with the same name.
// It accepts a vfs.Location and returns a vfs.File and error, if any.
func (f *File) CopyToLocation(location vfs.Location) (vfs.File, error) {
// validate seek is at 0,0 before doing copy
if err := backend.ValidateCopySeekPosition(f); err != nil {
return nil, err
if f.file != nil {
// validate seek is at 0,0 before doing copy
if err := backend.ValidateCopySeekPosition(f); err != nil {
return nil, err
}
}
return f.copyWithName(f.Name(), location)
}
Expand Down Expand Up @@ -351,7 +360,7 @@ func (f *File) Touch() error {
return f.Close()
}
now := time.Now()
return os.Chtimes(f.Path(), now, now)
return os.Chtimes(osFilePath(f), now, now)
}

func (f *File) copyWithName(name string, location vfs.Location) (vfs.File, error) {
Expand Down Expand Up @@ -386,7 +395,7 @@ func (f *File) openFile() (*os.File, error) {
openFunc = f.fileOpener
}

file, err := openFunc(f.Path())
file, err := openFunc(osFilePath(f))
if err != nil {
return nil, err
}
Expand All @@ -398,7 +407,7 @@ func (f *File) openFile() (*os.File, error) {
func openOSFile(filePath string) (*os.File, error) {
// Ensure the path exists before opening the file, NoOp if dir already exists.
var fileMode os.FileMode = 0666
if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|0750); err != nil {
if err := os.MkdirAll(filepath.Dir(filePath), os.ModeDir|0750); err != nil {
return nil, err
}

Expand Down Expand Up @@ -431,7 +440,7 @@ func (f *File) getInternalFile() (*os.File, error) {
openFunc = f.fileOpener
}

finalFile, err := openFunc(f.Path())
finalFile, err := openFunc(osFilePath(f))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -475,10 +484,11 @@ func (f *File) copyToLocalTempReader() (*os.File, error) {
openFunc = f.fileOpener
}

actualFile, err := openFunc(f.Path())
actualFile, err := openFunc(osFilePath(f))
if err != nil {
return nil, err
}
defer func() { _ = actualFile.Close() }()
if _, err := io.Copy(tmpFile, actualFile); err != nil {
return nil, err
}
Expand All @@ -493,3 +503,10 @@ func (f *File) copyToLocalTempReader() (*os.File, error) {

return tmpFile, nil
}

func osFilePath(f vfs.File) string {
if runtime.GOOS == "windows" {
return f.Location().Volume() + filepath.FromSlash(f.Path())
}
return f.Path()
}
21 changes: 20 additions & 1 deletion backend/os/fileSystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package os

import (
"path"
"path/filepath"
"runtime"

"github.com/c2fo/vfs/v6"
"github.com/c2fo/vfs/v6/backend"
Expand All @@ -22,22 +24,39 @@ func (fs *FileSystem) Retry() vfs.Retry {

// NewFile function returns the os implementation of vfs.File.
func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) {
if runtime.GOOS == "windows" && filepath.IsAbs(name) {
if v := filepath.VolumeName(name); v != "" {
volume = v
name = name[len(v):]
}
}

name = filepath.ToSlash(name)
err := utils.ValidateAbsoluteFilePath(name)
if err != nil {
return nil, err
}
return &File{name: name, filesystem: fs}, nil
return &File{volume: volume, name: name, filesystem: fs}, nil
}

// NewLocation function returns the os implementation of vfs.Location.
func (fs *FileSystem) NewLocation(volume, name string) (vfs.Location, error) {
if runtime.GOOS == "windows" && filepath.IsAbs(name) {
if v := filepath.VolumeName(name); v != "" {
volume = v
name = name[len(v):]
}
}

name = filepath.ToSlash(name)
err := utils.ValidateAbsoluteLocationPath(name)
if err != nil {
return nil, err
}

return &Location{
fileSystem: fs,
volume: volume,
name: utils.EnsureTrailingSlash(path.Clean(name)),
}, nil
}
Expand Down
Loading
Loading