package storage import ( "bytes" "fmt" "io" "mime" "path/filepath" "strings" "time" "github.com/jlaffaye/ftp" ) type FTPConfig struct { Host string // FTP 服务器地址 Port int // FTP 端口,默认 21 Username string // 用户名 Password string // 密码 BasePath string // 基础路径 } type FTP struct { config FTPConfig } func NewFTP(config FTPConfig) (Storage, error) { if config.Port == 0 { config.Port = 21 } config.BasePath = strings.Trim(config.BasePath, "/") f := &FTP{ config: config, } if err := f.ensureBasePath(); err != nil { return nil, fmt.Errorf("failed to ensure base path: %w", err) } return f, nil } // connect 建立 FTP 连接 func (f *FTP) connect() (*ftp.ServerConn, error) { addr := fmt.Sprintf("%s:%d", f.config.Host, f.config.Port) conn, err := ftp.Dial(addr) if err != nil { return nil, err } err = conn.Login(f.config.Username, f.config.Password) if err != nil { _ = conn.Quit() return nil, err } return conn, nil } // ensureBasePath 确保基础路径存在 func (f *FTP) ensureBasePath() error { conn, err := f.connect() if err != nil { return err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) // 递归创建路径 parts := strings.Split(f.config.BasePath, "/") currentPath := "" for _, part := range parts { if part == "" { continue } if currentPath == "" { currentPath = part } else { currentPath = currentPath + "/" + part } _ = conn.MakeDir(currentPath) } return nil } // getRemotePath 获取远程路径 func (f *FTP) getRemotePath(path string) string { path = strings.TrimPrefix(path, "/") if f.config.BasePath == "" { return path } if path == "" { return f.config.BasePath } return fmt.Sprintf("%s/%s", f.config.BasePath, path) } // MakeDirectory 创建目录 func (f *FTP) MakeDirectory(directory string) error { conn, err := f.connect() if err != nil { return err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(directory) // 递归创建目录 parts := strings.Split(remotePath, "/") currentPath := "" for _, part := range parts { if part == "" { continue } if currentPath == "" { currentPath = part } else { currentPath = currentPath + "/" + part } // 尝试创建目录 _ = conn.MakeDir(currentPath) } return nil } // DeleteDirectory 删除目录 func (f *FTP) DeleteDirectory(directory string) error { conn, err := f.connect() if err != nil { return err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(directory) return conn.RemoveDir(remotePath) } // Copy 复制文件到新位置 func (f *FTP) Copy(oldFile, newFile string) error { // FTP 不支持直接复制,需要下载再上传 data, err := f.Get(oldFile) if err != nil { return err } return f.Put(newFile, string(data)) } // Delete 删除文件 func (f *FTP) Delete(files ...string) error { conn, err := f.connect() if err != nil { return err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) for _, file := range files { remotePath := f.getRemotePath(file) if err := conn.Delete(remotePath); err != nil { return err } } return nil } // Exists 检查文件是否存在 func (f *FTP) Exists(file string) bool { conn, err := f.connect() if err != nil { return false } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(file) _, err = conn.FileSize(remotePath) return err == nil } // Files 获取目录下的所有文件 func (f *FTP) Files(path string) ([]string, error) { conn, err := f.connect() if err != nil { return nil, err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(path) entries, err := conn.List(remotePath) if err != nil { return nil, err } var files []string for _, entry := range entries { if entry.Type == ftp.EntryTypeFile { files = append(files, entry.Name) } } return files, nil } // Get 读取文件内容 func (f *FTP) Get(file string) ([]byte, error) { conn, err := f.connect() if err != nil { return nil, err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(file) resp, err := conn.Retr(remotePath) if err != nil { return nil, err } defer func(resp *ftp.Response) { _ = resp.Close() }(resp) return io.ReadAll(resp) } // LastModified 获取文件最后修改时间 func (f *FTP) LastModified(file string) (time.Time, error) { conn, err := f.connect() if err != nil { return time.Time{}, err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(file) entries, err := conn.List(filepath.Dir(remotePath)) if err != nil { return time.Time{}, err } fileName := filepath.Base(remotePath) for _, entry := range entries { if entry.Name == fileName { return entry.Time, nil } } return time.Time{}, fmt.Errorf("file not found: %s", file) } // MimeType 获取文件的 MIME 类型 func (f *FTP) MimeType(file string) (string, error) { ext := filepath.Ext(file) mimeType := mime.TypeByExtension(ext) if mimeType == "" { return "application/octet-stream", nil } return mimeType, nil } // Missing 检查文件是否不存在 func (f *FTP) Missing(file string) bool { return !f.Exists(file) } // Move 移动文件到新位置 func (f *FTP) Move(oldFile, newFile string) error { conn, err := f.connect() if err != nil { return err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) oldPath := f.getRemotePath(oldFile) newPath := f.getRemotePath(newFile) // 确保目标目录存在 newDir := filepath.Dir(newPath) if newDir != "." { f.createDirectoryPath(conn, newDir) } return conn.Rename(oldPath, newPath) } // createDirectoryPath 递归创建目录路径 func (f *FTP) createDirectoryPath(conn *ftp.ServerConn, path string) { parts := strings.Split(path, "/") currentPath := "" for _, part := range parts { if part == "" { continue } if currentPath == "" { currentPath = part } else { currentPath = currentPath + "/" + part } _ = conn.MakeDir(currentPath) } } // Path 获取文件的完整路径 func (f *FTP) Path(file string) string { return fmt.Sprintf("ftp://%s:%d/%s", f.config.Host, f.config.Port, f.getRemotePath(file)) } // Put 写入文件内容 func (f *FTP) Put(file, content string) error { conn, err := f.connect() if err != nil { return err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(file) // 确保目录存在 remoteDir := filepath.Dir(remotePath) if remoteDir != "." { f.createDirectoryPath(conn, remoteDir) } return conn.Stor(remotePath, bytes.NewReader([]byte(content))) } // Size 获取文件大小 func (f *FTP) Size(file string) (int64, error) { conn, err := f.connect() if err != nil { return 0, err } defer func(conn *ftp.ServerConn) { _ = conn.Quit() }(conn) remotePath := f.getRemotePath(file) return conn.FileSize(remotePath) }