-
-
Notifications
You must be signed in to change notification settings - Fork 317
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
util: Add afero relpath implementation
This new filesystem implements a relative filesystem modifier which can be useful for converting between absolute filesystems rooted at / and relative ones. This is particularly useful when interfacing with the golang embed package. The upstream requires a CLA, so we'll just store this here instead. spf13/afero#417
- Loading branch information
1 parent
d6cf595
commit 7ce5a81
Showing
1 changed file
with
299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
// Mgmt | ||
// Copyright (C) 2013-2024+ James Shubin and the project contributors | ||
// Written by James Shubin <[email protected]> and the project contributors | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
// This filesystem implementation is based on the afero.BasePathFs code. | ||
|
||
package util | ||
|
||
import ( | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/spf13/afero" | ||
) | ||
|
||
const ( | ||
// RelPathFsScheme returns a unique name for this type of filesystem. | ||
RelPathFsScheme = "RelPathFs" | ||
) | ||
|
||
var ( | ||
_ afero.Lstater = (*RelPathFs)(nil) | ||
_ fs.ReadDirFile = (*RelPathFile)(nil) | ||
) | ||
|
||
// RelPathFs removes a prefix from all operations to a given path within an Fs. | ||
// The given file name to the operations on this Fs will have a prefix removed | ||
// before calling the base Fs. | ||
// | ||
// When initializing it with "/", a call to `/foo` turns into `foo`. | ||
// | ||
// Note that it does not clean the error messages on return, so you may reveal | ||
// the real path on errors. | ||
type RelPathFs struct { | ||
source afero.Fs | ||
|
||
prefix string | ||
} | ||
|
||
// RelPathFile represents a file node. | ||
type RelPathFile struct { | ||
afero.File | ||
|
||
prefix string | ||
} | ||
|
||
// Name returns the path of the file. | ||
func (obj *RelPathFile) Name() string { | ||
sourcename := obj.File.Name() | ||
//return strings.TrimPrefix(sourcename, filepath.Clean(obj.prefix)) | ||
return filepath.Clean(obj.prefix) + sourcename // add prefix back on | ||
} | ||
|
||
// Readdir lists the contents of the directory and returns a list of file info | ||
// objects for each entry. | ||
func (obj *RelPathFile) ReadDir(n int) ([]fs.DirEntry, error) { | ||
if rdf, ok := obj.File.(fs.ReadDirFile); ok { | ||
return rdf.ReadDir(n) | ||
} | ||
return readDirFile{obj.File}.ReadDir(n) | ||
} | ||
|
||
// NewRelPathFs creates a new RelPathFs. | ||
func NewRelPathFs(source afero.Fs, prefix string) afero.Fs { | ||
return &RelPathFs{source: source, prefix: prefix} | ||
} | ||
|
||
// RealPath returns the correct path with the prefix removed. | ||
func (obj *RelPathFs) RealPath(name string) (string, error) { | ||
if name == "/" { | ||
return ".", nil // special trim | ||
} | ||
if name == "" { | ||
return filepath.Clean(name), nil // returns a single period | ||
} | ||
path := filepath.Clean(name) // actual path | ||
prefix := filepath.Clean(obj.prefix) // is often a / and we trim it off | ||
|
||
//if strings.HasPrefix(path, prefix) { // redundant | ||
path = strings.TrimPrefix(path, prefix) | ||
//} | ||
|
||
return path, nil | ||
} | ||
|
||
// Chtimes changes the access and modification times of the named file, similar | ||
// to the Unix utime() or utimes() functions. The underlying filesystem may | ||
// truncate or round the values to a less precise time unit. If there is an | ||
// error, it will be of type *PathError. | ||
func (obj *RelPathFs) Chtimes(name string, atime, mtime time.Time) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "chtimes", Path: name, Err: err} | ||
} | ||
return obj.source.Chtimes(name, atime, mtime) | ||
} | ||
|
||
// Chmod changes the mode of a file. | ||
func (obj *RelPathFs) Chmod(name string, mode os.FileMode) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "chmod", Path: name, Err: err} | ||
} | ||
return obj.source.Chmod(name, mode) | ||
} | ||
|
||
// Chown changes the ownership of a file. It is the equivalent of os.Chown. | ||
func (obj *RelPathFs) Chown(name string, uid, gid int) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "chown", Path: name, Err: err} | ||
} | ||
return obj.source.Chown(name, uid, gid) | ||
} | ||
|
||
// Name returns the name of this filesystem. | ||
func (obj *RelPathFs) Name() string { | ||
return RelPathFsScheme | ||
} | ||
|
||
// Stat returns some information about the particular path. | ||
func (obj *RelPathFs) Stat(name string) (fi os.FileInfo, err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return nil, &os.PathError{Op: "stat", Path: name, Err: err} | ||
} | ||
return obj.source.Stat(name) | ||
} | ||
|
||
// Rename moves or renames a file or directory. | ||
func (obj *RelPathFs) Rename(oldname, newname string) (err error) { | ||
if oldname, err = obj.RealPath(oldname); err != nil { | ||
return &os.PathError{Op: "rename", Path: oldname, Err: err} | ||
} | ||
if newname, err = obj.RealPath(newname); err != nil { | ||
return &os.PathError{Op: "rename", Path: newname, Err: err} | ||
} | ||
return obj.source.Rename(oldname, newname) | ||
} | ||
|
||
// RemoveAll removes path and any children it contains. It removes everything it | ||
// can but returns the first error it encounters. If the path does not exist, | ||
// RemoveAll returns nil (no error). | ||
func (obj *RelPathFs) RemoveAll(name string) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "remove_all", Path: name, Err: err} | ||
} | ||
return obj.source.RemoveAll(name) | ||
} | ||
|
||
// Remove removes a path. | ||
func (obj *RelPathFs) Remove(name string) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "remove", Path: name, Err: err} | ||
} | ||
return obj.source.Remove(name) | ||
} | ||
|
||
// OpenFile opens a path with a particular flag and permission. | ||
func (obj *RelPathFs) OpenFile(name string, flag int, mode os.FileMode) (f afero.File, err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return nil, &os.PathError{Op: "openfile", Path: name, Err: err} | ||
} | ||
sourcef, err := obj.source.OpenFile(name, flag, mode) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &RelPathFile{File: sourcef, prefix: obj.prefix}, nil | ||
} | ||
|
||
// Open opens a path. It will be opened read-only. | ||
func (obj *RelPathFs) Open(name string) (f afero.File, err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return nil, &os.PathError{Op: "open", Path: name, Err: err} | ||
} | ||
sourcef, err := obj.source.Open(name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &RelPathFile{File: sourcef, prefix: obj.prefix}, nil | ||
} | ||
|
||
// Mkdir makes a new directory. | ||
func (obj *RelPathFs) Mkdir(name string, mode os.FileMode) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "mkdir", Path: name, Err: err} | ||
} | ||
return obj.source.Mkdir(name, mode) | ||
} | ||
|
||
// MkdirAll creates a directory named path, along with any necessary parents, | ||
// and returns nil, or else returns an error. The permission bits perm are used | ||
// for all directories that MkdirAll creates. If path is already a directory, | ||
// MkdirAll does nothing and returns nil. | ||
func (obj *RelPathFs) MkdirAll(name string, mode os.FileMode) (err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return &os.PathError{Op: "mkdir", Path: name, Err: err} | ||
} | ||
return obj.source.MkdirAll(name, mode) | ||
} | ||
|
||
// Create creates a new file. | ||
func (obj *RelPathFs) Create(name string) (f afero.File, err error) { | ||
if name, err = obj.RealPath(name); err != nil { | ||
return nil, &os.PathError{Op: "create", Path: name, Err: err} | ||
} | ||
sourcef, err := obj.source.Create(name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &RelPathFile{File: sourcef, prefix: obj.prefix}, nil | ||
} | ||
|
||
// LstatIfPossible is for the lstater interface. | ||
func (obj *RelPathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { | ||
name, err := obj.RealPath(name) | ||
if err != nil { | ||
return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err} | ||
} | ||
if lstater, ok := obj.source.(afero.Lstater); ok { | ||
return lstater.LstatIfPossible(name) | ||
} | ||
fi, err := obj.source.Stat(name) | ||
return fi, false, err | ||
} | ||
|
||
// SymlinkIfPossible is for the weird Afero symlink API. | ||
func (obj *RelPathFs) SymlinkIfPossible(oldname, newname string) error { | ||
oldname, err := obj.RealPath(oldname) | ||
if err != nil { | ||
return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: err} | ||
} | ||
newname, err = obj.RealPath(newname) | ||
if err != nil { | ||
return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: err} | ||
} | ||
if linker, ok := obj.source.(afero.Linker); ok { | ||
return linker.SymlinkIfPossible(oldname, newname) | ||
} | ||
return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: afero.ErrNoSymlink} | ||
} | ||
|
||
// ReadlinkIfPossible is for the weird Afero readlink API. | ||
func (obj *RelPathFs) ReadlinkIfPossible(name string) (string, error) { | ||
name, err := obj.RealPath(name) | ||
if err != nil { | ||
return "", &os.PathError{Op: "readlink", Path: name, Err: err} | ||
} | ||
if reader, ok := obj.source.(afero.LinkReader); ok { | ||
return reader.ReadlinkIfPossible(name) | ||
} | ||
return "", &os.PathError{Op: "readlink", Path: name, Err: afero.ErrNoReadlink} | ||
} | ||
|
||
// readDirFile provides adapter from afero.File to fs.ReadDirFile needed for | ||
// correct Open | ||
type readDirFile struct { | ||
afero.File | ||
} | ||
|
||
var _ fs.ReadDirFile = readDirFile{} | ||
|
||
func (r readDirFile) ReadDir(n int) ([]fs.DirEntry, error) { | ||
items, err := r.File.Readdir(n) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ret := make([]fs.DirEntry, len(items)) | ||
for i := range items { | ||
//ret[i] = common.FileInfoDirEntry{FileInfo: items[i]} | ||
ret[i] = FileInfoDirEntry{FileInfo: items[i]} | ||
} | ||
|
||
return ret, nil | ||
} | ||
|
||
// FileInfoDirEntry provides an adapter from os.FileInfo to fs.DirEntry | ||
type FileInfoDirEntry struct { | ||
fs.FileInfo | ||
} | ||
|
||
var _ fs.DirEntry = FileInfoDirEntry{} | ||
|
||
func (d FileInfoDirEntry) Type() fs.FileMode { return d.FileInfo.Mode().Type() } | ||
|
||
func (d FileInfoDirEntry) Info() (fs.FileInfo, error) { return d.FileInfo, nil } |