sftp realpath: resolve symlinks
Fixes #890 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
e0ce2e2e8a
commit
55b47cf741
14 changed files with 244 additions and 100 deletions
|
@ -14,8 +14,11 @@ Several storage backends are supported: local filesystem, encrypted local filesy
|
|||
## Sponsors
|
||||
|
||||
If you find SFTPGo useful please consider supporting this Open Source project.
|
||||
Maintaing and evolving SFTPGo is a lot of work - easily the equivalent of a full time job - for me.
|
||||
|
||||
Maintaining and evolving SFTPGo is a lot of work - easily the equivalent of a full time job - for me.
|
||||
|
||||
I'd like to make SFTPGo into a sustainable long term project and would not like to introduce a dual licensing option and limit some features to the proprietary version only.
|
||||
|
||||
If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
|
||||
This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
|
||||
|
||||
|
|
|
@ -482,7 +482,7 @@ func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) err
|
|||
initialSize := int64(-1)
|
||||
if dstInfo, err := fsDst.Lstat(fsTargetPath); err == nil {
|
||||
if dstInfo.IsDir() {
|
||||
c.Log(logger.LevelWarn, "attempted to rename %#v overwriting an existing directory %#v",
|
||||
c.Log(logger.LevelWarn, "attempted to rename %q overwriting an existing directory %q",
|
||||
fsSourcePath, fsTargetPath)
|
||||
return c.GetOpUnsupportedError()
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ func (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {
|
|||
func (u *User) checkRootPath(connectionID string) error {
|
||||
fs, err := u.GetFilesystemForPath("/", connectionID)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "could not create main filesystem for user %#v err: %v", u.Username, err)
|
||||
logger.Warn(logSender, connectionID, "could not create main filesystem for user %q err: %v", u.Username, err)
|
||||
return fmt.Errorf("could not create root filesystem: %w", err)
|
||||
}
|
||||
fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
|
||||
|
|
|
@ -6,9 +6,9 @@ Run the following instructions from the directory that contains the sftpgo binar
|
|||
|
||||
## Linux
|
||||
|
||||
The easiest way to run SFTPGo as a service is to download and install the pre-compiled deb/rpm package or use one of the Arch Linux PKGBUILDs we maintain.
|
||||
The easiest way to run SFTPGo as a service is to use the [deb/rpm repos](./repo.md) or download and install a pre-compiled deb/rpm package or use one of the Arch Linux PKGBUILDs we maintain.
|
||||
|
||||
This section describes the procedure to use if you prefer to build SFTPGo yourself or if you want to download and configure a pre-built release as tar.
|
||||
This section describes the manual procedure.
|
||||
|
||||
A `systemd` sample [service](../init/sftpgo.service "systemd service") can be found inside the source tree.
|
||||
|
||||
|
@ -67,6 +67,10 @@ sudo /usr/bin/sftpgo gen man -d /usr/share/man/man1
|
|||
|
||||
## macOS
|
||||
|
||||
The easiest way to run SFTPGo as a service is to install it from the Homebrew [Formula](https://formulae.brew.sh/formula/sftpgo).
|
||||
|
||||
This section describes the procedure to use if you prefer to build SFTPGo yourself or if you want to download and configure a pre-built release as tar.
|
||||
|
||||
For macOS, a `launchd` sample [service](../init/com.github.drakkan.sftpgo.plist "launchd plist") can be found inside the source tree. The `launchd` plist assumes that SFTPGo has `/usr/local/opt/sftpgo` as base directory.
|
||||
|
||||
Here are some basic instructions to run SFTPGo as service, please run the following commands from the directory where you downloaded SFTPGo:
|
||||
|
|
15
go.mod
15
go.mod
|
@ -35,7 +35,7 @@ require (
|
|||
github.com/hashicorp/go-plugin v1.4.4
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
github.com/klauspost/compress v1.15.7
|
||||
github.com/klauspost/compress v1.15.8
|
||||
github.com/lestrrat-go/jwx v1.2.25
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
|
@ -53,12 +53,12 @@ require (
|
|||
github.com/rs/zerolog v1.27.0
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
|
||||
github.com/shirou/gopsutil/v3 v3.22.6
|
||||
github.com/spf13/afero v1.8.2
|
||||
github.com/spf13/afero v1.9.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
|
||||
github.com/unrolled/secure v1.11.0
|
||||
github.com/unrolled/secure v1.12.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||
|
@ -68,7 +68,7 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
google.golang.org/api v0.87.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
|
@ -103,7 +103,7 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-test/deep v1.0.8 // indirect
|
||||
github.com/goccy/go-json v0.9.8 // indirect
|
||||
github.com/goccy/go-json v0.9.10 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
|
@ -138,7 +138,7 @@ require (
|
|||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.36.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
|
@ -155,7 +155,7 @@ require (
|
|||
golang.org/x/tools v0.1.11 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220712132514-bdd2acd4974d // indirect
|
||||
google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9 // indirect
|
||||
google.golang.org/grpc v1.48.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
|
@ -166,6 +166,7 @@ require (
|
|||
|
||||
replace (
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220624105932-71c5dfcef1e8
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0
|
||||
)
|
||||
|
|
34
go.sum
34
go.sum
|
@ -264,6 +264,8 @@ github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHP
|
|||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0 h1:VL3ucjcGkcuAEflNvFbNYHOTdAqgYOMRJBBRy7nQ/E4=
|
||||
github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d h1:kNk/KRhszPJASp7WvjagNW254aKK643Lu8/fr4/ukiM=
|
||||
github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
|
@ -332,8 +334,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
|
|||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
|
||||
github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
|
||||
github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
|
@ -540,8 +542,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok=
|
||||
github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.15.8 h1:JahtItbkWjf2jzm/T+qgMxkP9EMHsqEUA6vCMGmXvhA=
|
||||
github.com/klauspost/compress v1.15.8/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
|
||||
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
|
@ -654,9 +656,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
|
@ -681,8 +680,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
|||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.36.0 h1:78hJTing+BLYLjhXE+Z2BubeEymH5Lr0/Mt8FKkxxYo=
|
||||
github.com/prometheus/common v0.36.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
|
@ -720,8 +719,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
|
|||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
|
||||
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
|
||||
github.com/spf13/afero v1.9.0 h1:sFSLUHgxdnN32Qy38hK3QkYBFXZj9DKjVjCUCtD7juY=
|
||||
github.com/spf13/afero v1.9.0/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
|
@ -760,8 +759,8 @@ github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjM
|
|||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/unrolled/secure v1.11.0 h1:fjkKhD/MsQnlmz/au+MmFptCFNhvf5iv04ALkdCXRCI=
|
||||
github.com/unrolled/secure v1.11.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/unrolled/secure v1.12.0 h1:7k3jcgLwfjiKkhQde6VbQ3D4KDLtDBqDd/hs3PPANDY=
|
||||
github.com/unrolled/secure v1.12.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
|
||||
|
@ -935,7 +934,6 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -972,8 +970,8 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -1224,8 +1222,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
|
|||
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220712132514-bdd2acd4974d h1:YbuF5+kdiC516xIP60RvlHeFbY9sRDR73QsAGHpkeVw=
|
||||
google.golang.org/genproto v0.0.0-20220712132514-bdd2acd4974d/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9 h1:1aEQRgZ4Gks2SRAkLzIPpIszRazwVfjSFe1cKc+e0Jg=
|
||||
google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
|
|
@ -268,6 +268,31 @@ func (c *Connection) Lstat(request *sftp.Request) (sftp.ListerAt, error) {
|
|||
return listerAt([]os.FileInfo{s}), nil
|
||||
}
|
||||
|
||||
// RealPath implements the RealPathFileLister interface
|
||||
func (c *Connection) RealPath(p string) (string, error) {
|
||||
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(p)) {
|
||||
return "", sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
|
||||
if c.User.Filters.StartDirectory == "" {
|
||||
p = util.CleanPath(p)
|
||||
} else {
|
||||
p = util.CleanPathWithBase(c.User.Filters.StartDirectory, p)
|
||||
}
|
||||
fs, fsPath, err := c.GetFsAndResolvedPath(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if realPather, ok := fs.(vfs.FsRealPather); ok {
|
||||
realPath, err := realPather.RealPath(fsPath)
|
||||
if err != nil {
|
||||
return "", c.GetFsError(fs, err)
|
||||
}
|
||||
return realPath, nil
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// StatVFS implements StatVFSFileCmder interface
|
||||
func (c *Connection) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) {
|
||||
c.UpdateLastActivity()
|
||||
|
|
|
@ -613,7 +613,7 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
|
|||
sftp.WithStartDirectory(connection.User.Filters.StartDirectory))
|
||||
|
||||
defer server.Close()
|
||||
if err := server.Serve(); err == io.EOF {
|
||||
if err := server.Serve(); errors.Is(err, io.EOF) {
|
||||
exitStatus := sshSubsystemExitStatus{Status: uint32(0)}
|
||||
_, err = channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||
connection.Log(logger.LevelInfo, "connection closed, sent exit status %+v error: %v", exitStatus, err)
|
||||
|
|
|
@ -763,60 +763,70 @@ func TestStartDirectory(t *testing.T) {
|
|||
startDir := "/st@ rt/dir"
|
||||
u := getTestUser(usePubKey)
|
||||
u.Filters.StartDirectory = startDir
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
u = getTestSFTPUser(usePubKey)
|
||||
u.Filters.StartDirectory = startDir
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
currentDir, err := client.Getwd()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, startDir, currentDir)
|
||||
currentDir, err := client.Getwd()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, startDir, currentDir)
|
||||
|
||||
entries, err := client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, 0)
|
||||
entries, err := client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, 0)
|
||||
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, testFileName+"_rename")
|
||||
assert.NoError(t, err)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, testFileName+"_rename")
|
||||
assert.NoError(t, err)
|
||||
|
||||
entries, err = client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, 1)
|
||||
entries, err = client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, 1)
|
||||
|
||||
currentDir, err = client.RealPath("..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, path.Dir(startDir), currentDir)
|
||||
currentDir, err = client.RealPath("..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, path.Dir(startDir), currentDir)
|
||||
|
||||
currentDir, err = client.RealPath("../..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", currentDir)
|
||||
currentDir, err = client.RealPath("../..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", currentDir)
|
||||
|
||||
currentDir, err = client.RealPath("../../..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", currentDir)
|
||||
currentDir, err = client.RealPath("../../..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", currentDir)
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
err = client.Remove(testFileName + "_rename")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -1181,22 +1191,59 @@ func TestProxyProtocol(t *testing.T) {
|
|||
func TestRealPath(t *testing.T) {
|
||||
usePubKey := true
|
||||
u := getTestUser(usePubKey)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
p, err := client.RealPath("../..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", p)
|
||||
p, err = client.RealPath("../test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/test", p)
|
||||
u = getTestSFTPUser(usePubKey)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
p, err := client.RealPath("../..")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", p)
|
||||
p, err = client.RealPath("../test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/test", p)
|
||||
subdir := "testsubdir"
|
||||
err = client.Mkdir(subdir)
|
||||
assert.NoError(t, err)
|
||||
linkName := testFileName + "_link"
|
||||
err = client.Symlink(testFileName, path.Join(subdir, linkName))
|
||||
assert.NoError(t, err)
|
||||
p, err = client.RealPath(path.Join(subdir, linkName))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, path.Join("/", testFileName), p)
|
||||
// an existing path
|
||||
sftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
|
||||
if assert.NoError(t, err) {
|
||||
testData := []byte("hello world")
|
||||
n, err := sftpFile.WriteAt(testData, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(testData), n)
|
||||
}
|
||||
p, err = client.RealPath(path.Join(subdir, linkName))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, path.Join("/", testFileName), p)
|
||||
// now a link outside the home dir
|
||||
err = os.Symlink(filepath.Clean(os.TempDir()), filepath.Join(localUser.GetHomeDir(), subdir, "temp"))
|
||||
assert.NoError(t, err)
|
||||
_, err = client.RealPath(path.Join(subdir, "temp"))
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
err = os.Remove(filepath.Join(localUser.GetHomeDir(), subdir, "temp"))
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(filepath.Join(localUser.GetHomeDir(), subdir))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -7138,13 +7185,15 @@ func TestPermList(t *testing.T) {
|
|||
defer conn.Close()
|
||||
defer client.Close()
|
||||
_, err = client.ReadDir(".")
|
||||
assert.Error(t, err, "read remote dir without permission should not succeed")
|
||||
assert.ErrorIs(t, err, os.ErrPermission, "read remote dir without permission should not succeed")
|
||||
_, err = client.Stat("test_file")
|
||||
assert.Error(t, err, "stat remote file without permission should not succeed")
|
||||
assert.ErrorIs(t, err, os.ErrPermission, "stat remote file without permission should not succeed")
|
||||
_, err = client.Lstat("test_file")
|
||||
assert.Error(t, err, "lstat remote file without permission should not succeed")
|
||||
assert.ErrorIs(t, err, os.ErrPermission, "lstat remote file without permission should not succeed")
|
||||
_, err = client.ReadLink("test_link")
|
||||
assert.Error(t, err, "read remote link without permission on source dir should not succeed")
|
||||
assert.ErrorIs(t, err, os.ErrPermission, "read remote link without permission on source dir should not succeed")
|
||||
_, err = client.RealPath(".")
|
||||
assert.ErrorIs(t, err, os.ErrPermission, "real path without permission should not succeed")
|
||||
f, err := client.Create(testFileName)
|
||||
if assert.NoError(t, err) {
|
||||
_, err = f.Write([]byte("content"))
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
{{if not .HideSupportLink}}
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<a class="small" href="https://github.com/drakkan/sftpgo#sponsors">SFTPGo needs your help</a>
|
||||
<a class="small" href="https://github.com/drakkan/sftpgo#sponsors" target="_blank">SFTPGo needs your help</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
|
|
@ -308,9 +308,15 @@ func GetDirsForVirtualPath(virtualPath string) []string {
|
|||
|
||||
// CleanPath returns a clean POSIX (/) absolute path to work with
|
||||
func CleanPath(p string) string {
|
||||
return CleanPathWithBase("/", p)
|
||||
}
|
||||
|
||||
// CleanPathWithBase returns a clean POSIX (/) absolute path to work with.
|
||||
// The specified base will be used if the provided path is not absolute
|
||||
func CleanPathWithBase(base, p string) string {
|
||||
p = filepath.ToSlash(p)
|
||||
if !path.IsAbs(p) {
|
||||
p = "/" + p
|
||||
p = path.Join(base, p)
|
||||
}
|
||||
return path.Clean(p)
|
||||
}
|
||||
|
|
40
vfs/osfs.go
40
vfs/osfs.go
|
@ -313,12 +313,44 @@ func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
|
|||
|
||||
err = fs.isSubDir(p)
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "Invalid path resolution, dir %#v original path %#v resolved %#v err: %v",
|
||||
fsLog(fs, logger.LevelError, "Invalid path resolution, path %q original path %q resolved %q err: %v",
|
||||
p, virtualPath, r, err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
// RealPath implements the FsRealPather interface
|
||||
func (fs *OsFs) RealPath(p string) (string, error) {
|
||||
linksWalked := 0
|
||||
for {
|
||||
info, err := os.Lstat(p)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fs.GetRelativePath(p), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
return fs.GetRelativePath(p), nil
|
||||
}
|
||||
resolvedLink, err := os.Readlink(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resolvedLink = filepath.Clean(resolvedLink)
|
||||
if filepath.IsAbs(resolvedLink) {
|
||||
p = resolvedLink
|
||||
} else {
|
||||
p = filepath.Join(filepath.Dir(p), resolvedLink)
|
||||
}
|
||||
linksWalked++
|
||||
if linksWalked > 10 {
|
||||
fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
|
||||
return "", &pathResolutionError{err: "too many links"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDirSize returns the number of files and the size for a folder
|
||||
// including any subfolders
|
||||
func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
|
||||
|
@ -402,14 +434,14 @@ func (fs *OsFs) isSubDir(sub string) error {
|
|||
// fs.rootDir must exist and it is already a validated absolute path
|
||||
parent, err := filepath.EvalSymlinks(fs.rootDir)
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "invalid root path %#v: %v", fs.rootDir, err)
|
||||
fsLog(fs, logger.LevelError, "invalid root path %q: %v", fs.rootDir, err)
|
||||
return err
|
||||
}
|
||||
if parent == sub {
|
||||
return nil
|
||||
}
|
||||
if len(sub) < len(parent) {
|
||||
err = fmt.Errorf("path %#v is not inside %#v", sub, parent)
|
||||
err = fmt.Errorf("path %q is not inside %q", sub, parent)
|
||||
return &pathResolutionError{err: err.Error()}
|
||||
}
|
||||
separator := string(os.PathSeparator)
|
||||
|
@ -419,7 +451,7 @@ func (fs *OsFs) isSubDir(sub string) error {
|
|||
separator = ""
|
||||
}
|
||||
if !strings.HasPrefix(sub, parent+separator) {
|
||||
err = fmt.Errorf("path %#v is not inside %#v", sub, parent)
|
||||
err = fmt.Errorf("path %q is not inside %q", sub, parent)
|
||||
return &pathResolutionError{err: err.Error()}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -628,6 +628,25 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
|
|||
return fsPath, nil
|
||||
}
|
||||
|
||||
// RealPath implements the FsRealPather interface
|
||||
func (fs *SFTPFs) RealPath(p string) (string, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
resolved, err := fs.sftpClient.RealPath(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if fs.config.Prefix != "/" {
|
||||
if err := fs.isSubDir(resolved); err != nil {
|
||||
fsLog(fs, logger.LevelError, "Invalid real path resolution, original path %q resolved %q err: %v",
|
||||
p, resolved, err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return fs.GetRelativePath(resolved), nil
|
||||
}
|
||||
|
||||
// getRealPath returns the real remote path trying to resolve symbolic links if any
|
||||
func (fs *SFTPFs) getRealPath(name string) (string, error) {
|
||||
linksWalked := 0
|
||||
|
@ -641,7 +660,7 @@ func (fs *SFTPFs) getRealPath(name string) (string, error) {
|
|||
}
|
||||
resolvedLink, err := fs.sftpClient.ReadLink(name)
|
||||
if err != nil {
|
||||
return name, err
|
||||
return name, fmt.Errorf("unable to resolve link to %q: %w", name, err)
|
||||
}
|
||||
resolvedLink = path.Clean(resolvedLink)
|
||||
if path.IsAbs(resolvedLink) {
|
||||
|
@ -651,6 +670,7 @@ func (fs *SFTPFs) getRealPath(name string) (string, error) {
|
|||
}
|
||||
linksWalked++
|
||||
if linksWalked > 10 {
|
||||
fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
|
||||
return "", &pathResolutionError{err: "too many links"}
|
||||
}
|
||||
}
|
||||
|
@ -661,11 +681,11 @@ func (fs *SFTPFs) isSubDir(name string) error {
|
|||
return nil
|
||||
}
|
||||
if len(name) < len(fs.config.Prefix) {
|
||||
err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
|
||||
err := fmt.Errorf("path %q is not inside: %#v", name, fs.config.Prefix)
|
||||
return &pathResolutionError{err: err.Error()}
|
||||
}
|
||||
if !strings.HasPrefix(name, fs.config.Prefix+"/") {
|
||||
err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
|
||||
err := fmt.Errorf("path %q is not inside: %#v", name, fs.config.Prefix)
|
||||
return &pathResolutionError{err: err.Error()}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -89,6 +89,12 @@ type Fs interface {
|
|||
Close() error
|
||||
}
|
||||
|
||||
// FsRealPather is a Fs that implements the RealPath method.
|
||||
type FsRealPather interface {
|
||||
Fs
|
||||
RealPath(p string) (string, error)
|
||||
}
|
||||
|
||||
// fsMetadataChecker is a Fs that implements the getFileNamesInPrefix method.
|
||||
// This interface is used to abstract metadata consistency checks
|
||||
type fsMetadataChecker interface {
|
||||
|
|
Loading…
Reference in a new issue