2022-06-22 22:24:35 +00:00
|
|
|
package disk
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-12-28 00:13:09 +00:00
|
|
|
"context"
|
2023-12-16 14:19:53 +00:00
|
|
|
"fmt"
|
2022-06-22 22:24:35 +00:00
|
|
|
"io"
|
2023-12-28 00:13:09 +00:00
|
|
|
"log"
|
2023-12-16 14:19:53 +00:00
|
|
|
"log/slog"
|
2022-06-22 22:24:35 +00:00
|
|
|
"net/url"
|
2023-12-28 00:13:09 +00:00
|
|
|
"path/filepath"
|
2022-06-22 22:24:35 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
"github.com/jackc/puddle/v2"
|
2022-06-22 22:24:35 +00:00
|
|
|
"github.com/jlaffaye/ftp"
|
|
|
|
)
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
// TODO Make configurable
|
|
|
|
const connectionCount = 5
|
|
|
|
|
2022-06-22 22:24:35 +00:00
|
|
|
var _ Disk = (*ftpDisk)(nil)
|
|
|
|
|
|
|
|
type ftpDisk struct {
|
2023-12-28 00:13:09 +00:00
|
|
|
pool *puddle.Pool[*ftp.ServerConn]
|
|
|
|
path string
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type ftpEntry struct {
|
|
|
|
*ftp.Entry
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f ftpEntry) IsDir() bool {
|
|
|
|
return f.Entry.Type == ftp.EntryTypeFolder
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f ftpEntry) Name() string {
|
|
|
|
return f.Entry.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
func newFTP(path string) (Disk, error) {
|
|
|
|
u, err := url.Parse(path)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to parse ftp url: %w", err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
pool, err := puddle.NewPool(&puddle.Config[*ftp.ServerConn]{
|
|
|
|
Constructor: func(ctx context.Context) (*ftp.ServerConn, error) {
|
|
|
|
c, failedHidden, err := testFTP(u, ftp.DialWithTimeout(time.Second*5), ftp.DialWithForceListHidden(true))
|
|
|
|
if failedHidden {
|
|
|
|
c, _, err = testFTP(u, ftp.DialWithTimeout(time.Second*5))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
slog.Info("logged into ftp", slog.Bool("hidden-files", !failedHidden))
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
},
|
|
|
|
MaxSize: connectionCount,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ftpDisk{
|
|
|
|
path: u.Path,
|
|
|
|
pool: pool,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFTP(u *url.URL, options ...ftp.DialOption) (*ftp.ServerConn, bool, error) {
|
|
|
|
c, err := ftp.Dial(u.Host, options...)
|
2022-06-22 22:24:35 +00:00
|
|
|
if err != nil {
|
2023-12-28 00:13:09 +00:00
|
|
|
return nil, false, fmt.Errorf("failed to dial host %s: %w", u.Host, err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
password, _ := u.User.Password()
|
|
|
|
if err := c.Login(u.User.Username(), password); err != nil {
|
2023-12-28 00:13:09 +00:00
|
|
|
return nil, false, fmt.Errorf("failed to login: %w", err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
_, err = c.List("/")
|
|
|
|
if err != nil {
|
|
|
|
return nil, true, fmt.Errorf("failed listing dir: %w", err)
|
|
|
|
}
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
return c, false, nil
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
func (l *ftpDisk) Exists(path string) (bool, error) {
|
|
|
|
res, err := l.acquire()
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Release()
|
|
|
|
|
|
|
|
slog.Debug("checking if file exists", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
|
|
|
|
|
|
|
split := strings.Split(clean(path)[1:], "/")
|
|
|
|
for _, s := range split[:len(split)-1] {
|
|
|
|
dir, err := l.readDirLock(res, "")
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
currentDir, _ := res.Value().CurrentDir()
|
|
|
|
|
|
|
|
foundDir := false
|
|
|
|
for _, entry := range dir {
|
|
|
|
if entry.IsDir() && entry.Name() == s {
|
|
|
|
foundDir = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !foundDir {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
slog.Debug("entering directory", slog.String("dir", s), slog.String("cwd", currentDir), slog.String("schema", "ftp"))
|
|
|
|
if err := res.Value().ChangeDir(s); err != nil {
|
|
|
|
return false, fmt.Errorf("failed to enter directory: %w", err)
|
|
|
|
}
|
|
|
|
}
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
dir, err := l.readDirLock(res, "")
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed listing directory: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
found := false
|
|
|
|
for _, entry := range dir {
|
|
|
|
if entry.Name() == clean(filepath.Base(path)) {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return found, nil
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) Read(path string) ([]byte, error) {
|
2023-12-28 00:13:09 +00:00
|
|
|
res, err := l.acquire()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Release()
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("reading file", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
f, err := res.Value().Retr(clean(path))
|
2022-06-22 22:24:35 +00:00
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to retrieve path: %w", err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
data, err := io.ReadAll(f)
|
2023-12-16 14:19:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return data, nil
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) Write(path string, data []byte) error {
|
2023-12-28 00:13:09 +00:00
|
|
|
res, err := l.acquire()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Release()
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("writing to file", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
|
|
|
if err := res.Value().Stor(clean(path), bytes.NewReader(data)); err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return fmt.Errorf("failed to write file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) Remove(path string) error {
|
2023-12-28 00:13:09 +00:00
|
|
|
res, err := l.acquire()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Release()
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("deleting path", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
|
|
|
if err := res.Value().Delete(clean(path)); err != nil {
|
|
|
|
if err := res.Value().RemoveDirRecur(clean(path)); err != nil {
|
|
|
|
return fmt.Errorf("failed to delete path: %w", err)
|
|
|
|
}
|
2023-12-16 14:19:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) MkDir(path string) error {
|
2023-12-28 00:13:09 +00:00
|
|
|
res, err := l.acquire()
|
2022-06-22 22:24:35 +00:00
|
|
|
if err != nil {
|
2023-12-28 00:13:09 +00:00
|
|
|
return err
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
defer res.Release()
|
|
|
|
|
|
|
|
split := strings.Split(clean(path)[1:], "/")
|
2022-06-22 22:24:35 +00:00
|
|
|
for _, s := range split {
|
2023-12-28 00:13:09 +00:00
|
|
|
dir, err := l.readDirLock(res, "")
|
2022-06-22 22:24:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
currentDir, _ := res.Value().CurrentDir()
|
|
|
|
|
2022-06-22 22:24:35 +00:00
|
|
|
foundDir := false
|
|
|
|
for _, entry := range dir {
|
|
|
|
if entry.IsDir() && entry.Name() == s {
|
|
|
|
foundDir = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !foundDir {
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("making directory", slog.String("dir", s), slog.String("cwd", currentDir), slog.String("schema", "ftp"))
|
|
|
|
if err := res.Value().MakeDir(s); err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return fmt.Errorf("failed to make directory: %w", err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("entering directory", slog.String("dir", s), slog.String("cwd", currentDir), slog.String("schema", "ftp"))
|
|
|
|
if err := res.Value().ChangeDir(s); err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return fmt.Errorf("failed to enter directory: %w", err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) ReadDir(path string) ([]Entry, error) {
|
2023-12-28 00:13:09 +00:00
|
|
|
res, err := l.acquire()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
defer res.Release()
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
return l.readDirLock(res, path)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) readDirLock(res *puddle.Resource[*ftp.ServerConn], path string) ([]Entry, error) {
|
|
|
|
slog.Debug("reading directory", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
|
|
|
|
|
|
|
dir, err := res.Value().List(clean(path))
|
2022-06-22 22:24:35 +00:00
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to list files in directory: %w", err)
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
entries := make([]Entry, len(dir))
|
|
|
|
for i, entry := range dir {
|
|
|
|
entries[i] = ftpEntry{
|
|
|
|
Entry: entry,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return entries, nil
|
|
|
|
}
|
|
|
|
|
2023-12-06 03:01:49 +00:00
|
|
|
func (l *ftpDisk) Open(path string, _ int) (io.WriteCloser, error) {
|
2023-12-28 00:13:09 +00:00
|
|
|
res, err := l.acquire()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-22 22:24:35 +00:00
|
|
|
reader, writer := io.Pipe()
|
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("opening for writing", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
2022-06-22 22:24:35 +00:00
|
|
|
|
|
|
|
go func() {
|
2023-12-28 00:13:09 +00:00
|
|
|
defer res.Release()
|
2022-06-22 22:24:35 +00:00
|
|
|
|
2023-12-28 00:13:09 +00:00
|
|
|
err := res.Value().Stor(clean(path), reader)
|
2022-06-22 22:24:35 +00:00
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
slog.Error("failed to store file", slog.Any("err", err))
|
2022-06-22 22:24:35 +00:00
|
|
|
}
|
2023-12-28 00:13:09 +00:00
|
|
|
slog.Debug("write success", slog.String("path", clean(path)), slog.String("schema", "ftp"))
|
2022-06-22 22:24:35 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
return writer, nil
|
|
|
|
}
|
2023-12-28 00:13:09 +00:00
|
|
|
|
|
|
|
func (l *ftpDisk) goHome(res *puddle.Resource[*ftp.ServerConn]) error {
|
|
|
|
slog.Debug("going to root directory", slog.String("schema", "ftp"))
|
|
|
|
|
|
|
|
err := res.Value().ChangeDir("/")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to change directory: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *ftpDisk) acquire() (*puddle.Resource[*ftp.ServerConn], error) {
|
|
|
|
res, err := l.pool.Acquire(context.TODO())
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed acquiring connection: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := l.goHome(res); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|