198 lines
5.3 KiB
C
198 lines
5.3 KiB
C
#include "ftp_manager.h"
|
|
#include "esp_log.h"
|
|
|
|
#include "lwip/sockets.h"
|
|
#include "lwip/netdb.h"
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
static const char *TAG = "FTP_MANAGER";
|
|
|
|
/* Receive one line terminated by \r\n into buffer (null-terminated) */
|
|
static int ftp_recv_line(int sock, char *buf, size_t len)
|
|
{
|
|
size_t pos = 0;
|
|
while (pos < len - 1)
|
|
{
|
|
char c;
|
|
int r = recv(sock, &c, 1, 0);
|
|
if (r <= 0) {
|
|
break;
|
|
}
|
|
buf[pos++] = c;
|
|
if (pos >= 2 && buf[pos-2] == '\r' && buf[pos-1] == '\n') {
|
|
break;
|
|
}
|
|
}
|
|
buf[pos] = '\0';
|
|
return (int)pos;
|
|
}
|
|
|
|
/* Send a command followed by \r\n */
|
|
static int ftp_send_cmd(int sock, const char *cmd)
|
|
{
|
|
char line[256];
|
|
int len = snprintf(line, sizeof(line), "%s\r\n", cmd);
|
|
if (len <= 0) return -1;
|
|
return send(sock, line, len, 0);
|
|
}
|
|
|
|
/* Resolve hostname or dotted IP and connect */
|
|
static int ftp_connect_control(const char *host, int port)
|
|
{
|
|
char port_str[8];
|
|
snprintf(port_str, sizeof(port_str), "%d", port);
|
|
|
|
struct addrinfo hints = {0};
|
|
hints.ai_family = AF_INET;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
struct addrinfo *res = NULL;
|
|
int err = getaddrinfo(host, port_str, &hints, &res);
|
|
if (err != 0 || res == NULL) {
|
|
ESP_LOGE(TAG, "getaddrinfo failed for host '%s': %d", host, err);
|
|
return -1;
|
|
}
|
|
|
|
int sock = -1;
|
|
struct addrinfo *p;
|
|
for (p = res; p != NULL; p = p->ai_next) {
|
|
sock = socket(p->ai_family, p->ai_socktype, 0);
|
|
if (sock < 0) continue;
|
|
|
|
if (connect(sock, p->ai_addr, p->ai_addrlen) == 0) {
|
|
break; // success
|
|
}
|
|
|
|
ESP_LOGW(TAG, "connect() failed to one addr, retrying next");
|
|
close(sock);
|
|
sock = -1;
|
|
}
|
|
|
|
freeaddrinfo(res);
|
|
|
|
if (sock < 0) {
|
|
ESP_LOGE(TAG, "Could not connect to FTP server %s:%d", host, port);
|
|
return -1;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Connected to FTP server %s:%d", host, port);
|
|
return sock;
|
|
}
|
|
|
|
ftp_err_t ftp_manager_download(const ftp_config_t *cfg,
|
|
ftp_progress_cb_t cb)
|
|
{
|
|
if (!cfg || !cfg->server || !cfg->remote_path || !cfg->local_path) {
|
|
ESP_LOGE(TAG, "Invalid FTP config");
|
|
return FTP_ERR_CONNECT;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "FTP download start: server=%s port=%d remote=%s local=%s",
|
|
cfg->server, cfg->port, cfg->remote_path, cfg->local_path);
|
|
|
|
int ctrl_sock = ftp_connect_control(cfg->server, cfg->port);
|
|
if (ctrl_sock < 0) {
|
|
return FTP_ERR_CONNECT;
|
|
}
|
|
|
|
char line[256];
|
|
|
|
/* Read welcome line */
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
/* USER */
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd), "USER %s", cfg->username ? cfg->username : "anonymous");
|
|
ftp_send_cmd(ctrl_sock, cmd);
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
/* PASS */
|
|
snprintf(cmd, sizeof(cmd), "PASS %s", cfg->password ? cfg->password : "anonymous");
|
|
ftp_send_cmd(ctrl_sock, cmd);
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
/* VERY SIMPLE IMPLEMENTATION: use passive mode for data connection */
|
|
|
|
/* PASV */
|
|
ftp_send_cmd(ctrl_sock, "PASV");
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
/* Parse "(h1,h2,h3,h4,p1,p2)" from 227 response */
|
|
int h1,h2,h3,h4,p1,p2;
|
|
char *p = strchr(line, '(');
|
|
if (!p || sscanf(p+1, "%d,%d,%d,%d,%d,%d",
|
|
&h1,&h2,&h3,&h4,&p1,&p2) != 6) {
|
|
ESP_LOGE(TAG, "Failed to parse PASV response");
|
|
close(ctrl_sock);
|
|
return FTP_ERR_DOWNLOAD;
|
|
}
|
|
|
|
char data_host[32];
|
|
snprintf(data_host, sizeof(data_host), "%d.%d.%d.%d", h1,h2,h3,h4);
|
|
int data_port = p1*256 + p2;
|
|
ESP_LOGI(TAG, "FTP data connection to %s:%d", data_host, data_port);
|
|
|
|
int data_sock = ftp_connect_control(data_host, data_port);
|
|
if (data_sock < 0) {
|
|
close(ctrl_sock);
|
|
return FTP_ERR_CONNECT;
|
|
}
|
|
|
|
/* RETR */
|
|
snprintf(cmd, sizeof(cmd), "RETR %s", cfg->remote_path);
|
|
ftp_send_cmd(ctrl_sock, cmd);
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
/* Now receive file data on data_sock */
|
|
FILE *fp = fopen(cfg->local_path, "wb");
|
|
if (!fp) {
|
|
ESP_LOGE(TAG, "Failed to open local file %s", cfg->local_path);
|
|
close(data_sock);
|
|
close(ctrl_sock);
|
|
return FTP_ERR_FILE;
|
|
}
|
|
|
|
uint8_t buf[2048];
|
|
size_t total = 0;
|
|
|
|
while (1) {
|
|
int r = recv(data_sock, buf, sizeof(buf), 0);
|
|
if (r < 0) {
|
|
ESP_LOGE(TAG, "Error reading data socket");
|
|
fclose(fp);
|
|
close(data_sock);
|
|
close(ctrl_sock);
|
|
return FTP_ERR_DOWNLOAD;
|
|
} else if (r == 0) {
|
|
break; // done
|
|
}
|
|
fwrite(buf, 1, r, fp);
|
|
total += r;
|
|
|
|
if (cb) cb(total, 0);
|
|
}
|
|
|
|
fclose(fp);
|
|
close(data_sock);
|
|
|
|
/* Read final transfer response */
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
/* QUIT */
|
|
ftp_send_cmd(ctrl_sock, "QUIT");
|
|
ftp_recv_line(ctrl_sock, line, sizeof(line));
|
|
ESP_LOGI(TAG, "FTP: %s", line);
|
|
|
|
close(ctrl_sock);
|
|
|
|
ESP_LOGI(TAG, "FTP download complete, total bytes=%u", (unsigned)total);
|
|
return FTP_OK;
|
|
}
|