Merged in feature/semver (pull request #93)

Add semantic versioning to all services

* working

* Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/semver

* 1.24 mod

* go upgrade

* tool

* Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/semver
This commit is contained in:
Jay Brown
2025-03-06 22:33:39 +00:00
parent 1211697f61
commit 7d78e65d0c
1117 changed files with 130807 additions and 6557 deletions
+1
View File
@@ -0,0 +1 @@
coverage.txt
+28
View File
@@ -0,0 +1,28 @@
linters:
# Disable all linters.
# Default: false
disable-all: true
# Enable specific linter
# https://golangci-lint.run/usage/linters/#enabled-by-default
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- contextcheck
- durationcheck
- exportloopref
- gocheckcompilerdirectives
- gosec
- loggercheck
- nilerr
- prealloc
- predeclared
- reassign
linters-settings:
staticcheck:
checks:
- all
- '-SA1024'
+4
View File
@@ -0,0 +1,4 @@
quiet: False
all: True
inpackage: True
testonly: True
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+23
View File
@@ -0,0 +1,23 @@
SHELL=bash
.PHONY: all
all: fmt mocks test install docker
.PHONY: fmt
fmt:
go fmt ./...
.PHONY: test
test:
go test -v -coverprofile=coverage.txt ./...
.PHONY: test.ci
test.ci: test fmt
.PHONY: lint
lint:
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 run
.PHONY: clean
clean:
rm -rf mocks
+171
View File
@@ -0,0 +1,171 @@
pathlib
========
[![Build Status](https://travis-ci.org/chigopher/pathlib.svg?branch=master)](https://travis-ci.org/chigopher/pathlib) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/chigopher/pathlib) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/chigopher/pathlib?style=flat-square) [![codecov](https://codecov.io/gh/chigopher/pathlib/branch/master/graph/badge.svg)](https://codecov.io/gh/chigopher/pathlib) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/chigopher/pathlib?style=flat-square) ![License](https://img.shields.io/github/license/chigopher/pathlib?style=flat-square)
Inspired by Python's pathlib, made better by Go.
`pathlib` is an "object-oriented" package for manipulating filesystem path objects. It takes many cues from [Python's pathlib](https://docs.python.org/3/library/pathlib.html), although it does not strictly adhere to its design philosophy. It provides a simple, intuitive, easy, and abstracted interface for dealing with many different types of filesystems.
`pathlib` is currently in the beta stage of development. The API is not guaranteed to be solidified, however changes will be as minimal as possible.
Table of Contents
-----------------
* [Examples](#examples)
* [OsFs](#osfs)
* [In\-memory FS](#in-memory-fs)
* [Design Philosophy](#design-philosophy)
* [filepath\.Path](#filepathpath)
* [filepath\.File](#filepathfile)
* [Frequently Asked Questions](#frequently-asked-questions)
* [Why pathlib and not filepath?](#why-pathlib-and-not-filepath)
* [Why not use afero directly?](#why-not-use-afero-directly)
* [Does this provide any benefit to my unit tests?](#does-this-provide-any-benefit-to-my-unit-tests)
* [What filesystems does this support?](#what-filesystems-does-this-support)
Examples
---------
### OsFs
Beacuse `pathlib` treats `afero` filesystems as first-class citizens, you can instantiate a `Path` object with the filesystem of your choosing.
#### Code
```go
package main
import (
"fmt"
"os"
"github.com/chigopher/pathlib"
"github.com/spf13/afero"
)
func main() {
// Create a path on your regular OS filesystem
path := pathlib.NewPathAfero("/home/ltclipp", afero.NewOsFs())
subdirs, err := path.ReadDir()
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
for _, dir := range subdirs {
fmt.Println(dir.Name())
}
}
```
#### Output
```bash
[ltclipp@landon-virtualbox examples]$ go build .
[ltclipp@landon-virtualbox examples]$ ./examples | tail
Music
Pictures
Public
Templates
Videos
git
go
mockery_test
snap
software
```
### In-memory FS
#### Code
```go
package main
import (
"fmt"
"os"
"github.com/chigopher/pathlib"
"github.com/spf13/afero"
)
func main() {
// Create a path using an in-memory filesystem
path := pathlib.NewPathAfero("/", afero.NewMemMapFs())
hello := path.Join("hello_world.txt")
hello.WriteFile([]byte("hello world!"), 0o644)
subpaths, err := path.ReadDir()
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
for _, subpath := range subpaths {
fmt.Printf("Name: %s Mode: %o Size: %d\n", subpath.Name(), subpath.Mode(), subpath.Size())
}
bytes, _ := hello.ReadFile()
fmt.Println(string(bytes))
}
```
#### Output
```bash
[ltclipp@landon-virtualbox examples]$ go build
[ltclipp@landon-virtualbox examples]$ ./examples
Name: hello_world.txt Mode: 644 Size: 12
hello world!
```
Design Philosophy
------------------
The design philosophy of this package is to be as thin of a layer as possible to existing community-standard packages, like `io`, `afero`, and `os`. Additional functionality is provided in consise and logical ways to extend the existing community APIs.
### `filepath.Path`
The API of `filepath.Path` can be grouped into a few main categories:
1. `github.com/spf13/afero.Fs` wrappers: these are methods that have nearly identical signatures to `afero.Fs`, with the exception of the path string (which is stored in the `pathlib.Path` object itself. `afero.Fs` is an object that is meant to interact directly with the filesystem.
2. `github.com/spf13/afero.Afero` wrappers: these are methods that again have nearly identical signatures to `afero.Afero`. `afero.Afero` is a convenience object that provides higher-level behavior to the underlying `afero.Fs` object.
3. Filesystem-specific methods: these are methods that are implemented by some, but not all, of the afero filesystems. These methods may fail at runtime if the filesystem you provide does not implement the required interface.
4. [Python's Pathlib](https://docs.python.org/3/library/pathlib.html)-inspired methods: these are methods that are not implemented in the previous two steps, and that provide the power behind the object-oriented design.
5. `github.com/chigopher/pathlib`-specific methods: these are miscellaneous methods that are not covered by any of the previous categories. These methods are typically conveniences around methods in one of the previous categories.
### `filepath.File`
`filepath.File` is intended to be a thin wrapper around [`afero.File`](https://pkg.go.dev/github.com/spf13/afero?tab=doc#File). We avoid simply returning this interface on calls to `Open()` and `OpenFile()` (etc) because we want the ability to extend our API beyond what `afero` provides. So, we create our own `File` object which embeds `afero.File`, but might possibly contain further functionality.
### Whoa whoa whoa, what is this afero nonsense?
[`github.com/spf13/afero`](https://github.com/spf13/afero) is a package that provides an abstracted interface to the underlying filesystem API calls. `pathlib` uses this package for operating on the abstracted filesystem. This is powerful because it allows you to to use essentially any kind of filesystem that you want. Additionally, afero is a first-class-citizen in `pathlib` meaning that you can implement and explicitly provide your own afero object.
The basic diagram looks like this:
![Pathlib Diagram](https://github.com/chigopher/pathlib/blob/master/docs/pathlib-diagram.png)
Frequently Asked Questions
--------------------------
### Why `pathlib` and not [`filepath`](https://golang.org/pkg/path/filepath/)?
[`filepath`](https://golang.org/pkg/path/filepath/) is a package that is tightly coupled to the OS filesystem APIs and also is not written in an object-oriented way. `pathlib` uses [`afero`](https://github.com/spf13/afero) under the hood for its abstracted filesystem interface, which allows you to represent a vast array of different filesystems (e.g. SFTP, HTTP, in-memory, and of course OS filesystems) using the same `Path` object.
### Why not use `afero` directly?
You certainly could, however `afero` does not represent a _filesystem object_ in an object-oriented way. It is only object-oriented with respect to the filesystem itself. `pathlib` is simply a thin layer on top of `afero` that provides the filesystem-object-orientation.
### Does this provide any benefit to my unit tests?
Most certainly! `pathlib` allows you to create [in-memory filesystems](#in-memory-fs), which have the nice property of being automatically garbage collected by Golang's GC when they go out of scope. You don't have to worry about defering any `Remove()` functions or setting up temporary dirs in `/tmp`. Just instantiate a `MemMapFs` and you're good to go!
### What filesystems does this support?
Currently only POSIX-style paths are supported.
+29
View File
@@ -0,0 +1,29 @@
package pathlib
import "fmt"
var (
// ErrDoesNotImplement indicates that the afero filesystem doesn't
// implement the required interface.
ErrDoesNotImplement = fmt.Errorf("doesn't implement required interface")
// ErrInfoIsNil indicates that a nil os.FileInfo object was provided
ErrInfoIsNil = fmt.Errorf("provided os.Info object was nil")
// ErrInvalidAlgorithm specifies that an unknown algorithm was given for Walk
ErrInvalidAlgorithm = fmt.Errorf("invalid algorithm specified")
// ErrLstatNotPossible specifies that the filesystem does not support lstat-ing
ErrLstatNotPossible = fmt.Errorf("lstat is not possible")
// ErrRelativeTo indicates that we could not make one path relative to another
ErrRelativeTo = fmt.Errorf("failed to make path relative to other")
errWalkControl = fmt.Errorf("walk control")
// ErrSkipSubtree indicates to the walk function that the current subtree of
// directories should be skipped. It's recommended to only use this error
// with the AlgorithmPreOrderDepthFirst algorithm, as many other walk algorithms
// will not respect this error due to the nature of the ordering in which the
// algorithms visit each node of the filesystem tree.
ErrWalkSkipSubtree = fmt.Errorf("skip subtree: %w", errWalkControl)
// ErrStopWalk indicates to the Walk function that the walk should be aborted.
// DEPRECATED: Use ErrWalkStop
ErrStopWalk = ErrWalkStop
// ErrWalkStop indicates to the Walk function that the walk should be aborted.
ErrWalkStop = fmt.Errorf("stop filesystem walk: %w", errWalkControl)
)
+9
View File
@@ -0,0 +1,9 @@
package pathlib
import "github.com/spf13/afero"
// File represents a file in the filesystem. It inherits the afero.File interface
// but might also include additional functionality.
type File struct {
afero.File
}
+657
View File
@@ -0,0 +1,657 @@
package pathlib
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/afero"
)
// Path is an object that represents a path
type Path struct {
path string
fs afero.Fs
// DefaultFileMode is the mode that is used when creating new files in functions
// that do not accept os.FileMode as a parameter.
DefaultFileMode os.FileMode
// DefaultDirMode is the mode that will be used when creating new directories
DefaultDirMode os.FileMode
// Sep is the seperator used in path calculations. By default this is set to
// os.PathSeparator.
Sep string
}
type PathOpts func(p *Path)
func PathWithAfero(fs afero.Fs) PathOpts {
return func(p *Path) {
p.fs = fs
}
}
func PathWithSeperator(sep string) PathOpts {
return func(p *Path) {
p.Sep = sep
}
}
// NewPath returns a new OS path
func NewPath(path string, opts ...PathOpts) *Path {
p := &Path{
path: path,
fs: afero.NewOsFs(),
DefaultFileMode: DefaultFileMode,
DefaultDirMode: DefaultDirMode,
Sep: string(os.PathSeparator),
}
for _, opt := range opts {
opt(p)
}
return p
}
// NewPathAfero returns a Path object with the given Afero object
//
// Deprecated: Use the PathWithAfero option in Newpath instead.
func NewPathAfero(path string, fs afero.Fs) *Path {
return NewPath(path, PathWithAfero(fs))
}
// Glob returns all of the path objects matched by the given pattern
// inside of the afero filesystem.
func Glob(fs afero.Fs, pattern string) ([]*Path, error) {
matches, err := afero.Glob(fs, pattern)
if err != nil {
return nil, fmt.Errorf("failed to glob: %w", err)
}
pathMatches := []*Path{}
for _, match := range matches {
pathMatches = append(pathMatches, NewPathAfero(match, fs))
}
return pathMatches, nil
}
type namer interface {
Name() string
}
func getFsName(fs afero.Fs) string {
if name, ok := fs.(namer); ok {
return name.Name()
}
return ""
}
// Fs returns the internal afero.Fs object.
func (p *Path) Fs() afero.Fs {
return p.fs
}
func (p *Path) doesNotImplementErr(interfaceName string) error {
return doesNotImplementErr(interfaceName, p.Fs())
}
func doesNotImplementErr(interfaceName string, fs afero.Fs) error {
return fmt.Errorf("%w: Path's afero filesystem %s does not implement %s", ErrDoesNotImplement, getFsName(fs), interfaceName)
}
func (p *Path) lstatNotPossible() error {
return lstatNotPossible(p.Fs())
}
func lstatNotPossible(fs afero.Fs) error {
return fmt.Errorf("%w: Path's afero filesystem %s does not support lstat", ErrLstatNotPossible, getFsName(fs))
}
// *******************************
// * afero.Fs wrappers *
// *******************************
// Create creates a file if possible, returning the file and an error, if any happens.
func (p *Path) Create() (File, error) {
file, err := p.Fs().Create(p.String())
return File{file}, err
}
// Mkdir makes the current dir. If the parents don't exist, an error
// is returned.
func (p *Path) Mkdir() error {
return p.Fs().Mkdir(p.String(), p.DefaultDirMode)
}
// MkdirMode makes the current dir. If the parents don't exist, an error
// is returned.
func (p *Path) MkdirMode(perm os.FileMode) error {
return p.Fs().Mkdir(p.String(), perm)
}
// MkdirAll makes all of the directories up to, and including, the given path.
func (p *Path) MkdirAll() error {
return p.Fs().MkdirAll(p.String(), p.DefaultDirMode)
}
// MkdirAllMode makes all of the directories up to, and including, the given path.
func (p *Path) MkdirAllMode(perm os.FileMode) error {
return p.Fs().MkdirAll(p.String(), perm)
}
// Open opens a file for read-only, returning it or an error, if any happens.
func (p *Path) Open() (*File, error) {
handle, err := p.Fs().Open(p.String())
return &File{
File: handle,
}, err
}
// OpenFile opens a file using the given flags.
// See the list of flags at: https://golang.org/pkg/os/#pkg-constants
func (p *Path) OpenFile(flag int) (*File, error) {
handle, err := p.Fs().OpenFile(p.String(), flag, p.DefaultFileMode)
return &File{
File: handle,
}, err
}
// OpenFileMode opens a file using the given flags and the given mode.
// See the list of flags at: https://golang.org/pkg/os/#pkg-constants
func (p *Path) OpenFileMode(flag int, perm os.FileMode) (*File, error) {
handle, err := p.Fs().OpenFile(p.String(), flag, perm)
return &File{
File: handle,
}, err
}
// Remove removes a file, returning an error, if any
// happens.
func (p *Path) Remove() error {
return p.Fs().Remove(p.String())
}
// RemoveAll removes the given path and all of its children.
func (p *Path) RemoveAll() error {
return p.Fs().RemoveAll(p.String())
}
// RenameStr renames a file
func (p *Path) RenameStr(newname string) error {
if err := p.Fs().Rename(p.String(), newname); err != nil {
return err
}
// Rename succeeded. Set our path to the newname.
p.path = newname
return nil
}
// Rename renames a file
func (p *Path) Rename(target *Path) error {
return p.RenameStr(target.String())
}
// Stat returns the os.FileInfo of the given path
func (p *Path) Stat() (os.FileInfo, error) {
return p.Fs().Stat(p.String())
}
// Chmod changes the file mode of the given path
func (p *Path) Chmod(mode os.FileMode) error {
return p.Fs().Chmod(p.String(), mode)
}
// Chtimes changes the modification and access time of the given path.
func (p *Path) Chtimes(atime time.Time, mtime time.Time) error {
return p.Fs().Chtimes(p.String(), atime, mtime)
}
// ************************
// * afero.Afero wrappers *
// ************************
// DirExists returns whether or not the path represents a directory that exists
func (p *Path) DirExists() (bool, error) {
return afero.DirExists(p.Fs(), p.String())
}
// Exists returns whether the path exists
func (p *Path) Exists() (bool, error) {
return afero.Exists(p.Fs(), p.String())
}
// FileContainsAnyBytes returns whether or not the path contains
// any of the listed bytes.
func (p *Path) FileContainsAnyBytes(subslices [][]byte) (bool, error) {
return afero.FileContainsAnyBytes(p.Fs(), p.String(), subslices)
}
// FileContainsBytes returns whether or not the given file contains the bytes
func (p *Path) FileContainsBytes(subslice []byte) (bool, error) {
return afero.FileContainsBytes(p.Fs(), p.String(), subslice)
}
// IsDir checks if a given path is a directory.
func (p *Path) IsDir() (bool, error) {
return afero.IsDir(p.Fs(), p.String())
}
// IsDir returns whether or not the os.FileMode object represents a
// directory.
func IsDir(mode os.FileMode) bool {
return mode.IsDir()
}
// IsEmpty checks if a given file or directory is empty.
func (p *Path) IsEmpty() (bool, error) {
return afero.IsEmpty(p.Fs(), p.String())
}
// ReadDir reads the current path and returns a list of the corresponding
// Path objects. This function differs from os.Readdir in that it does
// not call Stat() on the files. Instead, it calls Readdirnames which
// is less expensive and does not force the caller to make expensive
// Stat calls.
func (p *Path) ReadDir() ([]*Path, error) {
paths := []*Path{}
handle, err := p.Open()
if err != nil {
return paths, err
}
children, err := handle.Readdirnames(-1)
if err != nil {
return paths, err
}
for _, child := range children {
paths = append(paths, p.Join(child))
}
return paths, err
}
// ReadFile reads the given path and returns the data. If the file doesn't exist
// or is a directory, an error is returned.
func (p *Path) ReadFile() ([]byte, error) {
return afero.ReadFile(p.Fs(), p.String())
}
// SafeWriteReader is the same as WriteReader but checks to see if file/directory already exists.
func (p *Path) SafeWriteReader(r io.Reader) error {
return afero.SafeWriteReader(p.Fs(), p.String(), r)
}
// WriteFileMode writes the given data to the path (if possible). If the file exists,
// the file is truncated. If the file is a directory, or the path doesn't exist,
// an error is returned.
func (p *Path) WriteFileMode(data []byte, perm os.FileMode) error {
return afero.WriteFile(p.Fs(), p.String(), data, perm)
}
// WriteFile writes the given data to the path (if possible). If the file exists,
// the file is truncated. If the file is a directory, or the path doesn't exist,
// an error is returned.
func (p *Path) WriteFile(data []byte) error {
return afero.WriteFile(p.Fs(), p.String(), data, p.DefaultFileMode)
}
// WriteReader takes a reader and writes the content
func (p *Path) WriteReader(r io.Reader) error {
return afero.WriteReader(p.Fs(), p.String(), r)
}
// *************************************
// * pathlib.Path-like implementations *
// *************************************
// Name returns the string representing the final path component
func (p *Path) Name() string {
return filepath.Base(p.path)
}
// Parent returns the Path object of the parent directory
func (p *Path) Parent() *Path {
return NewPathAfero(filepath.Dir(p.String()), p.Fs())
}
// Readlink returns the target path of a symlink.
//
// This will fail if the underlying afero filesystem does not implement
// afero.LinkReader.
func (p *Path) Readlink() (*Path, error) {
linkReader, ok := p.Fs().(afero.LinkReader)
if !ok {
return nil, p.doesNotImplementErr("afero.LinkReader")
}
resolvedPathStr, err := linkReader.ReadlinkIfPossible(p.path)
if err != nil {
return nil, err
}
return NewPathAfero(resolvedPathStr, p.fs), nil
}
func resolveIfSymlink(path *Path) (*Path, bool, error) {
isSymlink, err := path.IsSymlink()
if err != nil {
return path, isSymlink, err
}
if isSymlink {
resolvedPath, err := path.Readlink()
if err != nil {
// Return the path unchanged on errors
return path, isSymlink, err
}
return resolvedPath, isSymlink, nil
}
return path, isSymlink, nil
}
func resolveAllHelper(path *Path) (*Path, error) {
parts := path.Parts()
for i := 0; i < len(parts); i++ {
rightOfComponent := parts[i+1:]
upToComponent := parts[:i+1]
componentPath := NewPathAfero(strings.Join(upToComponent, path.Sep), path.Fs())
resolved, isSymlink, err := resolveIfSymlink(componentPath)
if err != nil {
return path, err
}
if isSymlink {
if resolved.IsAbsolute() {
return resolveAllHelper(resolved.Join(strings.Join(rightOfComponent, path.Sep)))
}
return resolveAllHelper(componentPath.Parent().JoinPath(resolved).Join(rightOfComponent...))
}
}
// If we get through the entire iteration above, that means no component was a symlink.
// Return the argument.
return path, nil
}
// ResolveAll canonicalizes the path by following every symlink in
// every component of the given path recursively. The behavior
// should be identical to the `readlink -f` command from POSIX OSs.
// This will fail if the underlying afero filesystem does not implement
// afero.LinkReader. The path will be returned unchanged on errors.
func (p *Path) ResolveAll() (*Path, error) {
return resolveAllHelper(p)
}
// Parts returns the individual components of a path
func (p *Path) Parts() []string {
parts := []string{}
if p.IsAbsolute() {
parts = append(parts, p.Sep)
}
normalizedPathStr := normalizePathString(p.String())
normalizedParts := normalizePathParts(strings.Split(normalizedPathStr, p.Sep))
return append(parts, normalizedParts...)
}
// IsAbsolute returns whether or not the path is an absolute path. This is
// determined by checking if the path starts with a slash.
func (p *Path) IsAbsolute() bool {
return strings.HasPrefix(p.path, "/")
}
// Join joins the current object's path with the given elements and returns
// the resulting Path object.
func (p *Path) Join(elems ...string) *Path {
paths := []string{p.path}
paths = append(paths, elems...)
return NewPathAfero(strings.Join(paths, p.Sep), p.Fs())
}
// JoinPath is the same as Join() except it accepts a path object
func (p *Path) JoinPath(path *Path) *Path {
return p.Join(path.Parts()...)
}
func normalizePathString(path string) string {
path = strings.TrimSpace(path)
path = strings.TrimPrefix(path, "./")
path = strings.TrimRight(path, " ")
if len(path) > 1 {
path = strings.TrimSuffix(path, "/")
}
return path
}
func normalizePathParts(path []string) []string {
// We might encounter cases where path represents a split of the path
// "///" etc. We will get a bunch of erroneous empty strings in such a split,
// so remove all of the trailing empty strings except for the first one (if any)
normalized := []string{}
for i := 0; i < len(path); i++ {
if path[i] != "" {
normalized = append(normalized, path[i])
}
}
return normalized
}
// RelativeTo computes a relative version of path to the other path. For instance,
// if the object is /path/to/foo.txt and you provide /path/ as the argment, the
// returned Path object will represent to/foo.txt.
func (p *Path) RelativeTo(other *Path) (*Path, error) {
thisPathNormalized := normalizePathString(p.String())
otherPathNormalized := normalizePathString(other.String())
thisParts := p.Parts()
otherParts := other.Parts()
var relativeBase int
for idx, part := range otherParts {
if idx >= len(thisParts) || thisParts[idx] != part {
return p, fmt.Errorf("%s does not start with %s: %w", thisPathNormalized, otherPathNormalized, ErrRelativeTo)
}
relativeBase = idx
}
relativePath := thisParts[relativeBase+1:]
if len(relativePath) == 0 || (len(relativePath) == 1 && relativePath[0] == "") {
relativePath = []string{"."}
}
return NewPathAfero(strings.Join(relativePath, "/"), p.Fs()), nil
}
// RelativeToStr computes a relative version of path to the other path. For instance,
// if the object is /path/to/foo.txt and you provide /path/ as the argment, the
// returned Path object will represent to/foo.txt.
func (p *Path) RelativeToStr(other string) (*Path, error) {
return p.RelativeTo(NewPathAfero(other, p.Fs()))
}
// Lstat lstat's the path if the underlying afero filesystem supports it. If
// the filesystem does not support afero.Lstater, or if the filesystem implements
// afero.Lstater but returns false for the "lstat called" return value.
//
// A nil os.FileInfo is returned on errors.
func (p *Path) Lstat() (os.FileInfo, error) {
lStater, ok := p.Fs().(afero.Lstater)
if !ok {
return nil, p.doesNotImplementErr("afero.Lstater")
}
stat, lstatCalled, err := lStater.LstatIfPossible(p.String())
if !lstatCalled && err == nil {
return nil, p.lstatNotPossible()
}
return stat, err
}
// SymlinkStr symlinks to the target location. This will fail if the underlying
// afero filesystem does not implement afero.Linker.
func (p *Path) SymlinkStr(target string) error {
return p.Symlink(NewPathAfero(target, p.Fs()))
}
// Symlink symlinks to the target location. This will fail if the underlying
// afero filesystem does not implement afero.Linker.
func (p *Path) Symlink(target *Path) error {
symlinker, ok := p.fs.(afero.Linker)
if !ok {
return p.doesNotImplementErr("afero.Linker")
}
return symlinker.SymlinkIfPossible(target.path, p.path)
}
// String returns the string representation of the path
func (p *Path) String() string {
return p.path
}
// IsFile returns true if the given path is a file.
func (p *Path) IsFile() (bool, error) {
fileInfo, err := p.Stat()
if err != nil {
return false, err
}
return IsFile(fileInfo.Mode()), nil
}
// IsFile returns whether or not the file described by the given
// os.FileMode is a regular file.
func IsFile(mode os.FileMode) bool {
return mode.IsRegular()
}
// IsSymlink returns true if the given path is a symlink.
// Fails if the filesystem doesn't implement afero.Lstater.
func (p *Path) IsSymlink() (bool, error) {
fileInfo, err := p.Lstat()
if err != nil {
return false, err
}
return IsSymlink(fileInfo.Mode()), nil
}
// IsSymlink returns true if the file described by the given
// os.FileMode describes a symlink.
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}
// DeepEquals returns whether or not the path pointed to by other
// has the same resolved filepath as self.
func (p *Path) DeepEquals(other *Path) (bool, error) {
selfResolved, err := p.ResolveAll()
if err != nil {
return false, err
}
otherResolved, err := other.ResolveAll()
if err != nil {
return false, err
}
return selfResolved.Clean().Equals(otherResolved.Clean()), nil
}
// Equals returns whether or not the object's path is identical
// to other's, in a shallow sense. It simply checks for equivalence
// in the unresolved Paths() of each object.
func (p *Path) Equals(other *Path) bool {
return p.String() == other.String()
}
// GetLatest returns the file or directory that has the most recent mtime. Only
// works if this path is a directory and it exists. If the directory is empty,
// the returned Path object will be nil.
func (p *Path) GetLatest() (*Path, error) {
files, err := p.ReadDir()
if err != nil {
return nil, err
}
var greatestFileSeen *Path
for _, file := range files {
if greatestFileSeen == nil {
greatestFileSeen = p.Join(file.Name())
}
greatestMtime, err := greatestFileSeen.Mtime()
if err != nil {
return nil, err
}
thisMtime, err := file.Mtime()
// There is a possible race condition where the file is deleted after
// our call to ReadDir. We throw away the error if it isn't
// os.ErrNotExist
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if thisMtime.After(greatestMtime) {
greatestFileSeen = p.Join(file.Name())
}
}
return greatestFileSeen, nil
}
// Glob returns all matches of pattern relative to this object's path.
func (p *Path) Glob(pattern string) ([]*Path, error) {
return Glob(p.Fs(), p.Join(pattern).String())
}
// Clean returns a new object that is a lexically-cleaned
// version of Path.
func (p *Path) Clean() *Path {
return NewPathAfero(filepath.Clean(p.String()), p.Fs())
}
// Mtime returns the modification time of the given path.
func (p *Path) Mtime() (time.Time, error) {
stat, err := p.Stat()
if err != nil {
return time.Time{}, err
}
return Mtime(stat)
}
// Copy copies the path to another path using io.Copy.
// Returned is the number of bytes copied and any error values.
// The destination file is truncated if it exists, and is created
// if it does not exist.
func (p *Path) Copy(other *Path) (int64, error) {
srcFile, err := p.Open()
if err != nil {
return 0, fmt.Errorf("opening source file: %w", err)
}
defer srcFile.Close()
dstFile, err := other.OpenFile(os.O_TRUNC | os.O_CREATE | os.O_WRONLY)
if err != nil {
return 0, fmt.Errorf("opening destination file: %w", err)
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
// Mtime returns the mtime described in the given os.FileInfo object
func Mtime(fileInfo os.FileInfo) (time.Time, error) {
return fileInfo.ModTime(), nil
}
// Size returns the size of the object. Fails if the object doesn't exist.
func (p *Path) Size() (int64, error) {
stat, err := p.Stat()
if err != nil {
return 0, err
}
return Size(stat), nil
}
// Size returns the size described by the os.FileInfo. Before you say anything,
// yes... you could just do fileInfo.Size(). This is purely a convenience function
// to create API consistency.
func Size(fileInfo os.FileInfo) int64 {
return fileInfo.Size()
}
+9
View File
@@ -0,0 +1,9 @@
package pathlib
import "os"
// DefaultFileMode is the file mode that will be applied to new pathlib files
var DefaultFileMode = os.FileMode(0o644)
// DefaultDirMode is the default mode that will be applied to new directories
var DefaultDirMode = os.FileMode(0o755)
+405
View File
@@ -0,0 +1,405 @@
package pathlib
import (
"errors"
"fmt"
"os"
"slices"
)
// WalkOpts is the struct that defines how a walk should be performed
type WalkOpts struct {
// Depth defines how far down a directory we should recurse. A value of -1 means
// infinite depth. 0 means only the direct children of root will be returned, etc.
Depth int
// Algorithm specifies the algoritm that the Walk() function should use to
// traverse the directory.
Algorithm Algorithm
// FollowSymlinks defines whether symlinks should be dereferenced or not. If True,
// the symlink itself will never be returned to WalkFunc, but rather whatever it
// points to.
FollowSymlinks bool
// MinimumFileSize specifies the minimum size of a file for visitation.
// If negative, there is no minimum size.
MinimumFileSize int64
// MaximumFileSize specifies the maximum size of a file for visitation.
// If negative, there is no maximum size.
MaximumFileSize int64
// VisitFiles specifies that we should visit regular files during
// the walk.
VisitFiles bool
// VisitDirs specifies that we should visit directories during the walk.
VisitDirs bool
// VisitSymlinks specifies that we should visit symlinks during the walk.
VisitSymlinks bool
// SortChildren causes all children of a path to be lexigraphically sorted before
// being sent to the WalkFunc.
SortChildren bool
}
// DefaultWalkOpts returns the default WalkOpts struct used when
// walking a directory.
func DefaultWalkOpts() *WalkOpts {
return &WalkOpts{
Depth: -1,
Algorithm: AlgorithmBasic,
FollowSymlinks: false,
MinimumFileSize: -1,
MaximumFileSize: -1,
VisitFiles: true,
VisitDirs: true,
VisitSymlinks: true,
SortChildren: false,
}
}
// MeetsMinimumSize returns whether size is at least the minimum specified.
func (w *WalkOpts) MeetsMinimumSize(size int64) bool {
if w.MinimumFileSize < 0 {
return true
}
return size >= w.MinimumFileSize
}
// MeetsMaximumSize returns whether size is less than or equal to the maximum specified.
func (w *WalkOpts) MeetsMaximumSize(size int64) bool {
if w.MaximumFileSize < 0 {
return true
}
return size <= w.MaximumFileSize
}
// Algorithm represents the walk algorithm that will be performed.
type Algorithm int
const (
// AlgorithmBasic is a walk algorithm. It iterates over filesystem objects in the
// order in which they are returned by the operating system. It guarantees no
// ordering of any kind. It will recurse into subdirectories as soon as it encounters them,
// and will continue iterating the remaining children after the recursion is complete.
// It behaves as a quasi-DFS algorithm.
AlgorithmBasic Algorithm = iota
// AlgorithmDepthFirst is a walk algorithm. More specifically, it is a post-order
// depth first search whereby subdirectories are recursed into before
// visiting the children of the current directory.
// DEPRECATED: Use AlgorithmPostOrderDepthFirst
AlgorithmDepthFirst
// AlgorithmPostOrderDepthFirst is a walk algorithm that recurses into all of its children
// before visiting any of a node's elements.
AlgorithmPostOrderDepthFirst
// AlgorithmPreOrderDepthFirst is a walk algorithm. It visits all of a node's elements
// before recursing into its children.
AlgorithmPreOrderDepthFirst
)
// Walk is an object that handles walking through a directory tree
type Walk struct {
Opts *WalkOpts
root *Path
}
type WalkOptsFunc func(config *WalkOpts)
func WalkDepth(depth int) WalkOptsFunc {
return func(config *WalkOpts) {
config.Depth = depth
}
}
func WalkAlgorithm(algo Algorithm) WalkOptsFunc {
return func(config *WalkOpts) {
config.Algorithm = algo
}
}
func WalkFollowSymlinks(follow bool) WalkOptsFunc {
return func(config *WalkOpts) {
config.FollowSymlinks = follow
}
}
func WalkMinimumFileSize(size int64) WalkOptsFunc {
return func(config *WalkOpts) {
config.MinimumFileSize = size
}
}
func WalkMaximumFileSize(size int64) WalkOptsFunc {
return func(config *WalkOpts) {
config.MaximumFileSize = size
}
}
func WalkVisitFiles(value bool) WalkOptsFunc {
return func(config *WalkOpts) {
config.VisitFiles = value
}
}
func WalkVisitDirs(value bool) WalkOptsFunc {
return func(config *WalkOpts) {
config.VisitDirs = value
}
}
func WalkVisitSymlinks(value bool) WalkOptsFunc {
return func(config *WalkOpts) {
config.VisitSymlinks = value
}
}
func WalkSortChildren(value bool) WalkOptsFunc {
return func(config *WalkOpts) {
config.SortChildren = value
}
}
// NewWalk returns a new Walk struct with default values applied
func NewWalk(root *Path, opts ...WalkOptsFunc) (*Walk, error) {
config := DefaultWalkOpts()
for _, opt := range opts {
opt(config)
}
return NewWalkWithOpts(root, config)
}
// NewWalkWithOpts returns a Walk object with the given WalkOpts applied
func NewWalkWithOpts(root *Path, opts *WalkOpts) (*Walk, error) {
if root == nil {
return nil, fmt.Errorf("root path can't be nil")
}
if opts == nil {
return nil, fmt.Errorf("opts can't be nil")
}
return &Walk{
Opts: opts,
root: root,
}, nil
}
func (w *Walk) maxDepthReached(currentDepth int) bool {
if w.Opts.Depth >= 0 && currentDepth > w.Opts.Depth {
return true
}
return false
}
type dfsObjectInfo struct {
path *Path
info os.FileInfo
err error
}
func (w *Walk) walkDFS(walkFn WalkFunc, root *Path, currentDepth int) error {
if w.maxDepthReached(currentDepth) {
return nil
}
var children []*dfsObjectInfo
if err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
// Since we are doing depth-first, we have to first recurse through all the directories,
// and save all non-directory objects so we can defer handling at a later time.
if IsDir(info.Mode()) {
if err := w.walkDFS(walkFn, child, currentDepth+1); err != nil && !errors.Is(err, ErrWalkSkipSubtree) {
return err
}
}
children = append(children, &dfsObjectInfo{
path: child,
info: info,
err: encounteredErr,
})
return nil
}); err != nil {
return err
}
// Iterate over all children after all subdirs have been recursed
for _, child := range children {
passesQuery, err := w.passesQuerySpecification(child.info)
if err != nil {
return err
}
if passesQuery {
if err := walkFn(child.path, child.info, child.err); err != nil {
return err
}
}
}
return nil
}
// iterateImmediateChildren is a function that handles discovering root's immediate children,
// and will run the algorithm function for every child. The algorithm function is essentially
// what differentiates how each walk behaves, and determines what actions to take given a
// certain child.
func (w *Walk) iterateImmediateChildren(root *Path, algorithmFunction WalkFunc) error {
children, err := root.ReadDir()
if err != nil {
return err
}
if w.Opts.SortChildren {
slices.SortFunc[[]*Path, *Path](children, func(a *Path, b *Path) int {
if a.String() < b.String() {
return -1
}
if a.String() == b.String() {
return 0
}
return 1
})
}
var info os.FileInfo
for _, child := range children {
if child.String() == root.String() {
continue
}
if w.Opts.FollowSymlinks {
info, err = child.Stat()
if err != nil {
return err
}
} else {
info, err = child.Lstat()
}
if info == nil {
if err != nil {
return err
}
return ErrInfoIsNil
}
if algoErr := algorithmFunction(child, info, err); algoErr != nil {
return algoErr
}
}
return nil
}
// passesQuerySpecification returns whether or not the object described by
// the os.FileInfo passes all of the query specifications listed in
// the walk options.
func (w *Walk) passesQuerySpecification(info os.FileInfo) (bool, error) {
if IsFile(info.Mode()) {
if !w.Opts.VisitFiles {
return false, nil
}
if !w.Opts.MeetsMinimumSize(info.Size()) ||
!w.Opts.MeetsMaximumSize(info.Size()) {
return false, nil
}
} else if IsDir(info.Mode()) && !w.Opts.VisitDirs {
return false, nil
} else if IsSymlink(info.Mode()) && !w.Opts.VisitSymlinks {
return false, nil
}
return true, nil
}
func (w *Walk) walkBasic(walkFn WalkFunc, root *Path, currentDepth int) error {
if w.maxDepthReached(currentDepth) {
return nil
}
err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
if IsDir(info.Mode()) {
// In the case the error is ErrWalkSkipSubtree, we ignore it as we've already
// exited from the recursive call. Any other error should be bubbled up.
if err := w.walkBasic(walkFn, child, currentDepth+1); err != nil && !errors.Is(err, ErrWalkSkipSubtree) {
return err
}
}
passesQuery, err := w.passesQuerySpecification(info)
if err != nil {
return err
}
if passesQuery {
if err := walkFn(child, info, encounteredErr); err != nil {
return err
}
}
return nil
})
return err
}
func (w *Walk) walkPreOrderDFS(walkFn WalkFunc, root *Path, currentDepth int) error {
if w.maxDepthReached(currentDepth) {
return nil
}
dirs := []*Path{}
err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
if IsDir(info.Mode()) {
dirs = append(dirs, child)
}
passesQuery, err := w.passesQuerySpecification(info)
if err != nil {
return err
}
if passesQuery {
if err := walkFn(child, info, encounteredErr); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
for _, dir := range dirs {
if err := w.walkPreOrderDFS(walkFn, dir, currentDepth+1); err != nil && !errors.Is(err, ErrWalkSkipSubtree) {
return err
}
}
return nil
}
// WalkFunc is the function provided to the Walk function for each directory.
type WalkFunc func(path *Path, info os.FileInfo, err error) error
// Walk walks the directory using the algorithm specified in the configuration. Your WalkFunc
// may return any of the ErrWalk* errors to control various behavior of the walker. See the documentation
// of each error for more details.
func (w *Walk) Walk(walkFn WalkFunc) error {
funcs := map[Algorithm]func(walkFn WalkFunc, root *Path, currentDepth int) error{
AlgorithmBasic: w.walkBasic,
AlgorithmDepthFirst: w.walkDFS,
AlgorithmPostOrderDepthFirst: w.walkDFS,
AlgorithmPreOrderDepthFirst: w.walkPreOrderDFS,
}
algoFunc, ok := funcs[w.Opts.Algorithm]
if !ok {
return ErrInvalidAlgorithm
}
if err := algoFunc(walkFn, w.root, 0); err != nil {
if errors.Is(err, errWalkControl) {
return nil
}
return err
}
return nil
}