2024-10-01 01:38:18 +08:00
|
|
|
package localfs
|
|
|
|
|
|
|
|
|
|
import (
|
2024-11-12 23:37:31 +08:00
|
|
|
"cmp"
|
2024-10-01 01:38:18 +08:00
|
|
|
"io/fs"
|
2024-11-16 02:53:49 +08:00
|
|
|
"iter"
|
2024-10-01 01:38:18 +08:00
|
|
|
"os"
|
2024-11-12 23:37:31 +08:00
|
|
|
"slices"
|
|
|
|
|
"sync"
|
2024-10-01 01:38:18 +08:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/pkg/sftp/v2"
|
|
|
|
|
sshfx "github.com/pkg/sftp/v2/encoding/ssh/filexfer"
|
|
|
|
|
)
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
// File wraps an [os.File] to provide the additional operations necessary to implement [sftp.FileHandler].
|
2024-10-01 01:38:18 +08:00
|
|
|
type File struct {
|
|
|
|
|
*os.File
|
|
|
|
|
|
|
|
|
|
filename string
|
|
|
|
|
handle string
|
|
|
|
|
idLookup sftp.NameLookup
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
mu sync.Mutex
|
2024-11-16 02:53:49 +08:00
|
|
|
lastErr error
|
|
|
|
|
lastEnt *sshfx.NameEntry
|
2024-11-12 23:37:31 +08:00
|
|
|
entries []fs.FileInfo
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
// Handle returns the SFTP handle associated with the file.
|
2024-10-01 01:38:18 +08:00
|
|
|
func (f *File) Handle() string {
|
|
|
|
|
return f.handle
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
// Stat overrides the [os.File.Stat] receiver method
|
|
|
|
|
// by converting the [fs.FileInfo] into a [sshfx.Attributes].
|
2024-10-01 01:38:18 +08:00
|
|
|
func (f *File) Stat() (*sshfx.Attributes, error) {
|
|
|
|
|
fi, err := f.File.Stat()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fileInfoToAttrs(fi), nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
// rangedir returns an iterator over the directory entries of the directory.
|
|
|
|
|
// It will only ever yield either a [fs.FileInfo] or an error, never both.
|
2024-11-16 02:53:49 +08:00
|
|
|
// No error will be yielded until all available FileInfos have been yielded.
|
|
|
|
|
// Only one error will be yielded per invocation.
|
2024-11-12 23:37:31 +08:00
|
|
|
//
|
|
|
|
|
// We do not expose an iterator, because none has been standardized yet,
|
|
|
|
|
// and we do not want to accidentally implement an API inconsistent with future standards.
|
|
|
|
|
// However, for internal usage, we can separate the paginated Readdir code from the conversion to SFTP entries.
|
|
|
|
|
//
|
|
|
|
|
// Callers must guarantee synchronization by either holding the file lock, or holding an exclusive reference.
|
2024-11-16 02:53:49 +08:00
|
|
|
func (f *File) rangedir(grow func(int)) iter.Seq2[fs.FileInfo, error] {
|
|
|
|
|
return func(yield func(fs.FileInfo, error) bool) {
|
|
|
|
|
for {
|
|
|
|
|
grow(len(f.entries))
|
|
|
|
|
|
|
|
|
|
for i, entry := range f.entries {
|
|
|
|
|
if !yield(entry, nil) {
|
|
|
|
|
// This is a break condition.
|
|
|
|
|
// We need to remove all entries that have been consumed,
|
|
|
|
|
// and that includes the one we are currently on.
|
|
|
|
|
f.entries = slices.Delete(f.entries, 0, i+1)
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-16 02:53:49 +08:00
|
|
|
// We have consumed all of the saved entries, so we remove everything.
|
|
|
|
|
f.entries = slices.Delete(f.entries, 0, len(f.entries))
|
2024-10-01 01:38:18 +08:00
|
|
|
|
2024-11-16 02:53:49 +08:00
|
|
|
if f.lastErr != nil {
|
|
|
|
|
yield(nil, f.lastErr)
|
|
|
|
|
f.lastErr = nil
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-10-01 01:38:18 +08:00
|
|
|
|
2024-11-16 02:53:49 +08:00
|
|
|
// We cannot guarantee we only get entries, or an error, never both.
|
|
|
|
|
// So we need to just save these, and loop.
|
|
|
|
|
f.entries, f.lastErr = f.Readdir(128)
|
2024-11-12 23:37:31 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReadDir overrides the [os.File.ReadDir] receiver method
|
|
|
|
|
// by converting the slice of [fs.DirEntry] into into a slice of [sshfx.NameEntry].
|
|
|
|
|
func (f *File) ReadDir(maxDataLen uint32) (entries []*sshfx.NameEntry, err error) {
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
2024-11-16 02:53:49 +08:00
|
|
|
if f.lastEnt != nil {
|
|
|
|
|
// Last ReadDir left an entry for us to include in this call.
|
|
|
|
|
entries = append(entries, f.lastEnt)
|
|
|
|
|
f.lastEnt = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
grow := func(more int) {
|
|
|
|
|
entries = slices.Grow(entries, more)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
var size int
|
2024-11-16 02:53:49 +08:00
|
|
|
for fi, err := range f.rangedir(grow) {
|
2024-11-12 23:37:31 +08:00
|
|
|
if err != nil {
|
|
|
|
|
if len(entries) != 0 {
|
|
|
|
|
return entries, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, err
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
attrs := fileInfoToAttrs(fi)
|
2024-10-01 01:38:18 +08:00
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
entry := &sshfx.NameEntry{
|
|
|
|
|
Filename: fi.Name(),
|
|
|
|
|
Longname: sftp.FormatLongname(fi, f.idLookup),
|
|
|
|
|
Attrs: *attrs,
|
|
|
|
|
}
|
2024-10-01 01:38:18 +08:00
|
|
|
|
2025-03-04 21:56:56 +08:00
|
|
|
size += entry.MarshalSize()
|
2024-11-12 23:37:31 +08:00
|
|
|
|
|
|
|
|
if size > int(maxDataLen) {
|
2024-11-16 02:53:49 +08:00
|
|
|
// This would exceed the packet data length,
|
|
|
|
|
// so save this one for the next call,
|
|
|
|
|
// and return.
|
|
|
|
|
f.lastEnt = entry
|
2024-11-12 23:37:31 +08:00
|
|
|
break
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
2024-11-12 23:37:31 +08:00
|
|
|
|
|
|
|
|
entries = append(entries, entry)
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
2024-11-12 23:37:31 +08:00
|
|
|
|
|
|
|
|
return entries, nil
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-12 23:37:31 +08:00
|
|
|
// SetStat implements [sftp.SetStatFileHandler].
|
2024-10-01 01:38:18 +08:00
|
|
|
func (f *File) SetStat(attrs *sshfx.Attributes) (err error) {
|
2025-03-05 23:56:03 +08:00
|
|
|
if len(attrs.Extended) > 0 {
|
|
|
|
|
err = &sshfx.StatusPacket{
|
|
|
|
|
StatusCode: sshfx.StatusOpUnsupported,
|
|
|
|
|
ErrorMessage: "unsupported fsetstat: extended atributes",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-04 21:56:56 +08:00
|
|
|
if attrs.HasSize() {
|
|
|
|
|
sz := attrs.GetSize()
|
2025-03-05 23:56:03 +08:00
|
|
|
err = cmp.Or(f.Truncate(int64(sz)), err)
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-04 21:56:56 +08:00
|
|
|
if attrs.HasPermissions() {
|
|
|
|
|
perm := attrs.GetPermissions()
|
2025-03-05 23:56:03 +08:00
|
|
|
err = cmp.Or(f.Chmod(fs.FileMode(perm.Perm())), err)
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-04 21:56:56 +08:00
|
|
|
if attrs.HasACModTime() {
|
|
|
|
|
atime, mtime := attrs.GetACModTime()
|
2025-03-05 23:56:03 +08:00
|
|
|
err = cmp.Or(os.Chtimes(f.filename, time.Unix(int64(atime), 0), time.Unix(int64(mtime), 0)), err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-27 23:36:43 +08:00
|
|
|
if attrs.HasUserGroup() {
|
|
|
|
|
uid, gid := attrs.GetUserGroup()
|
2025-03-05 23:56:03 +08:00
|
|
|
err = cmp.Or(f.Chown(int(uid), int(gid)), err)
|
2024-10-01 01:38:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|