Golang中FTP进入扩展被动模式的问题探讨

Golang中FTP进入扩展被动模式的问题探讨 我正在编写一个程序,用于从FTP服务器检索目录中的所有文件。第一个文件可以成功下载,但在第二个文件时 c.Retr(entry) 返回错误:

229 Entering Extended Passive Mode (|||44022|).

显然我对FTP的理解有所欠缺。不知道该如何处理。我尝试在 ftp.Dial(...) 中提供 ftp.DialWithDisabledEPSV(true),但在第二个文件时 c.Retr(entry) 返回错误:

226 Transfer complete

但没有文件被成功检索。

以下是代码。请原谅其脚本性质。非常感谢任何帮助。

package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"time"

	"github.com/jlaffaye/ftp"
)

const (
	monthOffset = -1
)

func main() {
	c, err := ftp.Dial(
		os.Getenv("FTP_HOST"),
		ftp.DialWithTimeout(5*time.Second),
	)
	if err != nil {
		log.Fatalln("unable to connect: ", err)
	}

	err = c.Login(
		os.Getenv("ACCOUNT"),
		os.Getenv("PASSWORD"),
	)
	if err != nil {
		log.Fatal("login failed: ", err)
	}

	t := time.Now().AddDate(0, monthOffset, 0)
	year, month, _ := t.Date()
	path := fmt.Sprintf("%d/%02d/", year, month)

	entries, err := c.NameList(path)
	if err != nil {
		log.Fatalf("unable to NLIST %s: %v\n", path, err)
	}

	for _, entry := range entries {
		fmt.Printf("retrieving %s...\n", entry)
		resp, err := c.Retr(entry)
		if err != nil {
			log.Fatalf("unable to retrieve %s: %v\n", entry, err)
		}
		defer resp.Close()

		if _, err := os.Stat(entry); os.IsNotExist(err) {
			os.MkdirAll("data/"+path, 0700)
		}
		destination, err := os.Create("data/" + entry)
		if err != nil {
			log.Fatalln("failed to create file:", err)
		}

		b, err := io.Copy(destination, resp)
		if err != nil {
			log.Fatalln("unable to copy response to dest file: ", err)
		}
		fmt.Printf("successfully copied: %s, bytes copied: %d\n", destination.Name(), b)
	}
}

更多关于Golang中FTP进入扩展被动模式的问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

问题在于我在调用 c.Retr(entry) 的 for 循环中使用了 defer resp.Close()

解决方案是在复制后直接调用 resp.Close() 而不使用 defer。

更多关于Golang中FTP进入扩展被动模式的问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我最初没有看到 func DialWithDebugOutput(w io.Writer) DialOption

使用 ftp.DialWithDebugOutput(os.Stdout) 的结果如下。在调试输出中我没有看到第二次 RETR 尝试。说实话还是有点困惑…

220 (vsFTPd 2.2.2)
FEAT
211-Features:
 AUTH SSL
 AUTH TLS
 EPRT
 EPSV
 MDTM
 PASV
 PBSZ
 PROT
 REST STREAM
 SIZE
 TVFS
 UTF8
211 End
USER <user>
331 Please specify the password.
PASS <password>
230 Login successful.
TYPE I
200 Switching to Binary mode.
OPTS UTF8 ON
200 Always in UTF8 mode.
EPSV
229 Entering Extended Passive Mode (|||44010|).
NLST 2019/07/
150 Here comes the directory listing.
226 Directory send OK.
retrieving 2019/07/<file>...
EPSV
229 Entering Extended Passive Mode (|||44081|).
RETR 2019/07/<file>
150 Opening BINARY mode data connection for 2019/07/<file> (290200 bytes).
successfully copied: data/2019/07/<file>, bytes copied: 290200
retrieving 2019/07/<file>...
EPSV
226 Transfer complete.
PASV
229 Entering Extended Passive Mode (|||44023|).
2019/08/04 07:07:36 unable to retrieve 2019/07/<file>: 229 Entering Extended Passive Mode (|||44023|).
exit status 1

问题出现在处理FTP扩展被动模式(EPSV)和文件传输完成后的连接管理上。当服务器发送"229 Entering Extended Passive Mode"时,它正在建立数据传输连接,但代码没有正确处理多个文件的连续传输。

主要问题:

  1. 在循环中使用了defer resp.Close(),这会导致所有响应在函数结束时才关闭
  2. 没有正确处理EPSV模式下的数据传输
  3. 文件创建逻辑有问题,应该在传输前创建目录

以下是修复后的代码:

package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/jlaffaye/ftp"
)

const (
	monthOffset = -1
)

func main() {
	c, err := ftp.Dial(
		os.Getenv("FTP_HOST"),
		ftp.DialWithTimeout(5*time.Second),
		ftp.DialWithDisabledEPSV(true), // 禁用EPSV,使用传统PASV模式
	)
	if err != nil {
		log.Fatalln("unable to connect: ", err)
	}
	defer c.Quit()

	err = c.Login(
		os.Getenv("ACCOUNT"),
		os.Getenv("PASSWORD"),
	)
	if err != nil {
		log.Fatal("login failed: ", err)
	}

	t := time.Now().AddDate(0, monthOffset, 0)
	year, month, _ := t.Date()
	path := fmt.Sprintf("%d/%02d/", year, month)

	entries, err := c.NameList(path)
	if err != nil {
		log.Fatalf("unable to NLIST %s: %v\n", path, err)
	}

	// 提前创建目标目录
	dataDir := "data/" + path
	if err := os.MkdirAll(dataDir, 0700); err != nil {
		log.Fatalf("unable to create directory %s: %v\n", dataDir, err)
	}

	for _, entry := range entries {
		fmt.Printf("retrieving %s...\n", entry)
		
		resp, err := c.Retr(entry)
		if err != nil {
			log.Printf("unable to retrieve %s: %v\n", entry, err)
			continue // 继续处理下一个文件而不是退出
		}

		// 提取文件名并创建完整路径
		filename := filepath.Base(entry)
		destinationPath := filepath.Join(dataDir, filename)
		
		destination, err := os.Create(destinationPath)
		if err != nil {
			resp.Close()
			log.Printf("failed to create file %s: %v\n", destinationPath, err)
			continue
		}

		b, err := io.Copy(destination, resp)
		if err != nil {
			resp.Close()
			destination.Close()
			log.Printf("unable to copy response to dest file %s: %v\n", destinationPath, err)
			continue
		}

		// 立即关闭响应和文件
		resp.Close()
		destination.Close()
		
		fmt.Printf("successfully copied: %s, bytes copied: %d\n", destinationPath, b)
	}
}

关键修改:

  1. 使用ftp.DialWithDisabledEPSV(true)禁用EPSV模式
  2. 移除了循环内的defer,改为立即关闭响应和文件
  3. 提前创建目标目录
  4. 使用filepath包正确处理文件路径
  5. log.Fatalf改为log.Printf并继续处理,避免单个文件失败导致整个程序退出

这样修改后应该能够正确处理多个文件的连续下载,避免EPSV模式相关的问题。

回到顶部