mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-22 03:20:41 +08:00
385 lines
7.7 KiB
Go
385 lines
7.7 KiB
Go
package lib
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"golang.org/x/net/webdav"
|
|
)
|
|
|
|
var _ webdav.FileSystem = multiDir{}
|
|
|
|
const windowsErrorNotSameDevice = syscall.Errno(17)
|
|
|
|
type multiDir struct {
|
|
mounts DirectoryMounts
|
|
noSniff bool
|
|
}
|
|
|
|
func (m multiDir) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
|
mount, rest, err := m.resolve(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rest == "/" {
|
|
return os.ErrExist
|
|
}
|
|
|
|
return mount.dir(m.noSniff).Mkdir(ctx, rest, perm)
|
|
}
|
|
|
|
func (m multiDir) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
|
if cleanName(name) == "/" {
|
|
if writeFlag(flag) {
|
|
return nil, os.ErrPermission
|
|
}
|
|
|
|
return &multiDirRootFile{
|
|
entries: m.rootEntries(ctx),
|
|
info: virtualDirInfo{name: "/"},
|
|
}, nil
|
|
}
|
|
|
|
mount, rest, err := m.resolve(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rest == "/" && writeFlag(flag) {
|
|
return nil, os.ErrPermission
|
|
}
|
|
|
|
file, err := mount.dir(m.noSniff).OpenFile(ctx, rest, flag, perm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if rest == "/" {
|
|
return mountRootFile{File: file, name: mount.Name}, nil
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (m multiDir) RemoveAll(ctx context.Context, name string) error {
|
|
if cleanName(name) == "/" {
|
|
return os.ErrInvalid
|
|
}
|
|
|
|
mount, rest, err := m.resolve(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rest == "/" {
|
|
return os.ErrInvalid
|
|
}
|
|
|
|
return mount.dir(m.noSniff).RemoveAll(ctx, rest)
|
|
}
|
|
|
|
func (m multiDir) Rename(ctx context.Context, oldName, newName string) error {
|
|
oldMount, oldRest, err := m.resolve(oldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newMount, newRest, err := m.resolve(newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if oldRest == "/" || newRest == "/" {
|
|
return os.ErrInvalid
|
|
}
|
|
|
|
if oldMount.Name == newMount.Name {
|
|
return oldMount.dir(m.noSniff).Rename(ctx, oldRest, newRest)
|
|
}
|
|
|
|
oldPath := oldMount.filePath(oldRest)
|
|
newPath := newMount.filePath(newRest)
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
if isCrossDeviceError(err) {
|
|
return renameAcrossMount(oldPath, newPath)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func renameAcrossMount(oldPath, newPath string) error {
|
|
info, err := os.Lstat(oldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
target, err := os.Readlink(oldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.Symlink(target, newPath); err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(oldPath)
|
|
}
|
|
|
|
if info.Mode().IsRegular() {
|
|
if err := copyRegularFile(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
source, err := fs.Sub(os.DirFS(filepath.Dir(oldPath)), filepath.Base(oldPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.CopyFS(newPath, source); err != nil {
|
|
_ = os.RemoveAll(newPath)
|
|
return err
|
|
}
|
|
}
|
|
if err := copyMetadata(oldPath, newPath); err != nil {
|
|
_ = os.RemoveAll(newPath)
|
|
return err
|
|
}
|
|
return os.RemoveAll(oldPath)
|
|
}
|
|
|
|
func copyRegularFile(oldPath, newPath string) error {
|
|
source, err := os.Open(oldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
target, err := os.OpenFile(newPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
_ = source.Close()
|
|
return err
|
|
}
|
|
|
|
_, copyErr := io.Copy(target, source)
|
|
copyErr = errors.Join(copyErr, target.Close(), source.Close())
|
|
if copyErr != nil {
|
|
_ = os.Remove(newPath)
|
|
return copyErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyMetadata(oldPath, newPath string) error {
|
|
return filepath.Walk(oldPath, func(name string, info os.FileInfo, err error) error {
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(oldPath, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newName := filepath.Join(newPath, rel)
|
|
if err := os.Chmod(newName, info.Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
return os.Chtimes(newName, info.ModTime(), info.ModTime())
|
|
})
|
|
}
|
|
|
|
func isCrossDeviceError(err error) bool {
|
|
return errors.Is(err, syscall.EXDEV) || runtime.GOOS == "windows" && errors.Is(err, windowsErrorNotSameDevice)
|
|
}
|
|
|
|
func (m multiDir) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
|
if cleanName(name) == "/" {
|
|
return virtualDirInfo{name: "/"}, nil
|
|
}
|
|
|
|
mount, rest, err := m.resolve(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info, err := mount.dir(m.noSniff).Stat(ctx, rest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rest == "/" {
|
|
return namedFileInfo{FileInfo: info, name: mount.Name}, nil
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func (m multiDir) resolve(name string) (DirectoryMount, string, error) {
|
|
name = cleanName(name)
|
|
if name == "/" {
|
|
return DirectoryMount{}, "", os.ErrInvalid
|
|
}
|
|
|
|
trimmed := strings.TrimPrefix(name, "/")
|
|
mountName, rest, _ := strings.Cut(trimmed, "/")
|
|
for _, mount := range m.mounts {
|
|
if mount.Name == mountName {
|
|
if rest == "" {
|
|
return mount, "/", nil
|
|
}
|
|
return mount, "/" + rest, nil
|
|
}
|
|
}
|
|
|
|
return DirectoryMount{}, "", os.ErrNotExist
|
|
}
|
|
|
|
func (m multiDir) rootEntries(ctx context.Context) []os.FileInfo {
|
|
entries := make([]os.FileInfo, 0, len(m.mounts))
|
|
for _, mount := range m.mounts {
|
|
info, err := mount.dir(m.noSniff).Stat(ctx, "/")
|
|
if err != nil {
|
|
entries = append(entries, virtualDirInfo{name: mount.Name})
|
|
continue
|
|
}
|
|
entries = append(entries, namedFileInfo{FileInfo: info, name: mount.Name})
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Name() < entries[j].Name()
|
|
})
|
|
|
|
return entries
|
|
}
|
|
|
|
func (d DirectoryMount) dir(noSniff bool) Dir {
|
|
return Dir{
|
|
Dir: webdav.Dir(d.Path),
|
|
noSniff: noSniff,
|
|
}
|
|
}
|
|
|
|
func (d DirectoryMount) filePath(name string) string {
|
|
return filepath.Join(d.Path, filepath.FromSlash(strings.TrimPrefix(name, "/")))
|
|
}
|
|
|
|
func cleanName(name string) string {
|
|
if name == "" || !strings.HasPrefix(name, "/") {
|
|
name = "/" + name
|
|
}
|
|
return path.Clean(name)
|
|
}
|
|
|
|
func writeFlag(flag int) bool {
|
|
return flag&(os.O_WRONLY|os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_APPEND) != 0
|
|
}
|
|
|
|
type multiDirRootFile struct {
|
|
entries []os.FileInfo
|
|
info os.FileInfo
|
|
offset int
|
|
}
|
|
|
|
func (f *multiDirRootFile) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (f *multiDirRootFile) Read([]byte) (int, error) {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func (f *multiDirRootFile) Seek(offset int64, whence int) (int64, error) {
|
|
var next int64
|
|
switch whence {
|
|
case io.SeekStart:
|
|
next = offset
|
|
case io.SeekCurrent:
|
|
next = int64(f.offset) + offset
|
|
case io.SeekEnd:
|
|
next = int64(len(f.entries)) + offset
|
|
default:
|
|
return 0, os.ErrInvalid
|
|
}
|
|
if next < 0 {
|
|
return 0, os.ErrInvalid
|
|
}
|
|
f.offset = int(next)
|
|
return next, nil
|
|
}
|
|
|
|
func (f *multiDirRootFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
if count <= 0 {
|
|
entries := f.entries[f.offset:]
|
|
f.offset = len(f.entries)
|
|
return entries, nil
|
|
}
|
|
|
|
if f.offset >= len(f.entries) {
|
|
return nil, io.EOF
|
|
}
|
|
|
|
end := f.offset + count
|
|
if end > len(f.entries) {
|
|
end = len(f.entries)
|
|
}
|
|
entries := f.entries[f.offset:end]
|
|
f.offset = end
|
|
return entries, nil
|
|
}
|
|
|
|
func (f *multiDirRootFile) Stat() (os.FileInfo, error) {
|
|
return f.info, nil
|
|
}
|
|
|
|
func (f *multiDirRootFile) Write([]byte) (int, error) {
|
|
return 0, os.ErrPermission
|
|
}
|
|
|
|
type mountRootFile struct {
|
|
webdav.File
|
|
name string
|
|
}
|
|
|
|
func (f mountRootFile) Stat() (os.FileInfo, error) {
|
|
info, err := f.File.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return namedFileInfo{FileInfo: info, name: f.name}, nil
|
|
}
|
|
|
|
type namedFileInfo struct {
|
|
os.FileInfo
|
|
name string
|
|
}
|
|
|
|
func (i namedFileInfo) Name() string {
|
|
return i.name
|
|
}
|
|
|
|
type virtualDirInfo struct {
|
|
name string
|
|
}
|
|
|
|
func (i virtualDirInfo) Name() string {
|
|
return i.name
|
|
}
|
|
|
|
func (i virtualDirInfo) Size() int64 {
|
|
return 0
|
|
}
|
|
|
|
func (i virtualDirInfo) Mode() os.FileMode {
|
|
return os.ModeDir | 0555
|
|
}
|
|
|
|
func (i virtualDirInfo) ModTime() time.Time {
|
|
return time.Time{}
|
|
}
|
|
|
|
func (i virtualDirInfo) IsDir() bool {
|
|
return true
|
|
}
|
|
|
|
func (i virtualDirInfo) Sys() any {
|
|
return nil
|
|
}
|