文件下载是常见的需求,在嵌入式中使用Mongoose 网路库可以进行一次封装更通用些,且受限于网络环境下载可能中断,本工程实践实现一种断点续传方案。
本文对应工程实现:src/network/http_lfs_download.c / http_lfs_download.h。在 Mongoose 事件模型上做了一层阻塞式 封装:HTTP 流式写入 LittleFS (经 littlefs_adapt / fs_adapt_*),支持 Range 断点续传 、临时文件 + rename 原子落盘 、以及 Content-Length / Content-Range 层面的完整性校验。
1. 设计目标与适用场景
| 目标 | 说明 |
|---|---|
| 与业务解耦 | 不绑定字库/OTA;上层只传 url、base_dir、basename |
| 断点续传 | 存在未完成的临时文件时,带 Range: bytes=N- 请求,服务端返回 206 后从偏移 N 追加写 |
| 原子替换 | 默认先写 basename.part(后缀可配),成功后再 rename 覆盖正式文件,避免半成品直接顶替旧文件 |
| 校验 | 已解析到期望总大小时,对比「已写字节数」与「最终文件 stat 大小」 |
约束(与当前工程 Mongoose 配置一致):
- URL 仅支持
http://(MG_TLS=0时无 HTTPS)。 basename不得含/、..,长度受限(实现中 ≤ 48)。- 需已挂载 LittleFS,且
fs_adapt_*可用。
2. 对外 API
2.1 参数结构体 http_lfs_download_param_t
c
typedef struct {
const char *url; /* 仅 http:// */
const char *base_dir; /* 如 "/system" */
const char *basename; /* 本地文件名,如 "font.bin" */
const char *part_suffix; /* ATOMIC 时临时文件后缀;NULL/"" 等价 ".part";DIRECT 时忽略 */
http_lfs_store_mode_t store_mode;
} http_lfs_download_param_t;
2.2 落盘模式 http_lfs_store_mode_t
-
HTTP_LFS_STORE_ATOMIC(默认 0)- 写入路径:
base_dir/basename+part_suffix(默认basename.part)。 - 成功后:
delete(正式)→rename(临时 → 正式)。 - 中断时:正式文件仍为上一次完整版本(若曾成功过);临时文件保留,供下次续传。
- 写入路径:
-
HTTP_LFS_STORE_DIRECT(1)- 直接写
base_dir/basename,无临时文件与 rename。 - 中断可能留下不完整 正式文件;断点逻辑仍可能基于该文件大小发 Range(若你选择 DIRECT 且文件已存在非零大小,行为与 ATOMIC 类似,但损坏的「正式」文件会参与续传,需自行权衡)。
- 直接写
2.3 阻塞接口
c
int http_lfs_download_blocking(const http_lfs_download_param_t *param);
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| -1 | 失败(参数、HTTP 非 200/206、写盘、rename、校验失败等) |
HTTP_LFS_RES_FALLBACK_FULL(-2) |
仅内部使用 :如 416 Range Not Satisfiable 或 Content-Range 与本地偏移不一致 时触发删掉临时文件并全量重下 ;http_lfs_download_blocking 内部 goto retry_connect 后对外仍返回 0 或 -1 |
调用线程内会 mg_mgr_init → mg_http_connect → 轮询 mg_mgr_poll + osal_msleep(1) 直到完成或总超时,属于同步阻塞封装,适合独立下载任务线程。
3. 断点续传实现要点
3.1 何时启用 Range
在 http_lfs_download_blocking 中:
- 若
fs_adapt_stat(临时路径, &sz)成功且sz > 0,且本会话尚未禁用 Range,则:resume_offset = sztry_range = true
- 若不 续传(无临时文件或大小为 0):会
fs_adapt_delete(临时路径),从空文件开始全量下载(200 +O_TRUNC)。
临时路径在 ATOMIC 模式下为 base_dir/basename + part_suffix(默认 .part)。
3.2 HTTP 请求
MG_EV_CONNECT 中若 try_range && resume_offset > 0,在 GET 中增加:
http
Range: bytes=<resume_offset>-
仍带 Host 、Connection: close 、Accept: */* (与当前源码一致;若需与 font_download.c 一样去掉 Connection: close 或加 User-Agent,可在此统一改)。
3.3 响应处理
- 416 :认为 Range 无法满足,
fd_abort(..., HTTP_LFS_RES_FALLBACK_FULL),外层 关闭本次连接 后 重新发起一次全量下载 (并range_disabled_for_session避免再次 Range 死循环)。 - 200 / 206 以外:失败。
- 206 Partial Content :
- 必须存在
Content-Range,且解析出的区间起点bytes <start>-<end>/<total>与resume_offset一致,否则 回退全量。 - 打开临时文件
O_WRONLY | O_CREAT(不O_TRUNC),fs_adapt_seek(fd, resume_offset, SEEK_SET),bytes_written初始化为resume_offset。 expected_bytes = Content-Length + resume_offset(整文件逻辑总字节数)。
- 必须存在
- 200 OK (服务端忽略 Range):
O_TRUNC从头写;若本地曾以为要续传,日志会提示 server ignored range。
3.4 传输编码
- 支持 Content-Length 、chunked 、直到关闭(无 CL 非 chunked)。
- 断点续传 依赖明确的 CL + 206 + Content-Range 组合做总长度校验;chunked/Until-Close 路径下
expected_known可能为 false,校验策略与 CL 路径不同(见源码fd_finish_ok)。
3.5 完成与校验
fd_finish_ok 中:
- 若
expected_known:bytes_written必须等于expected_bytes。 - ATOMIC :
rename临时 → 正式 后,对final_path再fs_adapt_stat,与expected_bytes比对。 - 失败则删正式/临时并置错。
4. 内部结构(便于对照源码)
hd_ctx_t:保存 URL、base_dir/final_path/path(当前写入路径) 、resume_offset、try_range、bytes_written、expected_bytes、mode(CL/chunked/close)等。hd_ev_handler:处理MG_EV_OPEN/MG_EV_POLL(连接超时、正文空闲超时) /MG_EV_CONNECT(发 GET) /MG_EV_HTTP_HDRS(解析状态与头、打开文件) /MG_EV_READ/MG_EV_CLOSE/MG_EV_ERROR。- 正文消费逻辑与早期
font_download.c同类:从recv缓冲写 Flash、删已消费字节、更新last_body_ms。
可调宏(源码顶部,可按 WiFi 大文件经验调大,与 font_download.c 思路类似):
HD_BODY_IDLE_MS(默认 30000):两次收到 body 数据的最大间隔。HD_OVERALL_MS(默认 300000):单次连接总等待时间。HD_MG_POLL_MS(默认 10):mg_mgr_poll阻塞参数,可通过#ifndef覆盖。
代码封装实现:
c
/**
* @file http_lfs_download.c
* @brief Mongoose HTTP:流式下载到 LittleFS(Range、校验、.part 原子替换),与业务无关。
*/
#include "http_lfs_download.h"
#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cmsis_os2.h"
#include "littlefs_adapt.h"
#include "mongoose_config.h"
#include "mongoose.h"
#include "soc_osal.h"
#if defined(__has_include)
#if __has_include("mongoose_protocol.h")
#include "mongoose_protocol.h"
#define FD_MG_LOCK() mongoose_api_lock()
#define FD_MG_UNLOCK() mongoose_api_unlock()
#endif
#endif
#ifndef FD_MG_LOCK
#define FD_MG_LOCK() ((void)0)
#define FD_MG_UNLOCK() ((void)0)
#endif
#define HD_BODY_IDLE_MS 30000UL
#define HD_CONNECT_MS 30000UL
#define HD_OVERALL_MS 300000UL
#ifndef HD_MGR_POLL_MS
#define HD_MGR_POLL_MS 10
#endif
#define HD_PATH_MAX 96
#define HD_URL_MAX 256
typedef enum {
FD_BODY_NONE = 0,
FD_BODY_CONTENT_LENGTH,
FD_BODY_CHUNKED,
FD_BODY_UNTIL_CLOSE,
} fd_body_mode_t;
typedef struct {
int fd;
int result;
volatile bool done;
fd_body_mode_t mode;
size_t content_remaining;
size_t chunk_payload_left;
bool hdrs_ok;
uint64_t last_body_ms;
uint64_t connect_deadline_ms;
size_t bytes_written;
size_t expected_bytes;
bool expected_known;
bool try_range;
size_t resume_offset;
char base_dir[HD_PATH_MAX];
char final_path[HD_PATH_MAX];
char path[HD_PATH_MAX];
char url_buf[HD_URL_MAX];
bool direct_write;
} hd_ctx_t;
static int fd_parse_content_length(const struct mg_str *s, size_t *out)
{
char tmp[32];
if (s == NULL || s->buf == NULL) {
return -1;
}
size_t n = s->len < sizeof(tmp) - 1U ? s->len : sizeof(tmp) - 1U;
memcpy(tmp, s->buf, n);
tmp[n] = '\0';
char *endp = NULL;
unsigned long v = strtoul(tmp, &endp, 10);
if (endp == tmp) {
return -1;
}
*out = (size_t)v;
return 0;
}
static bool fd_is_hex_line_char(int c)
{
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
static int fd_parse_chunk_size_line(const char *s, size_t line_len, unsigned long *size_out)
{
size_t i = 0;
while (i < line_len && fd_is_hex_line_char((unsigned char)s[i])) {
i++;
}
if (i == 0U) {
return -1;
}
char tmp[12];
if (i >= sizeof(tmp)) {
return -1;
}
memcpy(tmp, s, i);
tmp[i] = '\0';
char *endp = NULL;
unsigned long v = strtoul(tmp, &endp, 16);
if (endp == tmp || v == ULONG_MAX) {
return -1;
}
*size_out = v;
return 0;
}
static bool fd_header_contains_token_ci(const struct mg_str *s, const char *token)
{
size_t i;
size_t j;
size_t slen;
size_t tlen;
if (s == NULL || s->buf == NULL || token == NULL) {
return false;
}
slen = s->len;
tlen = strlen(token);
if (tlen == 0U || slen < tlen) {
return false;
}
for (i = 0; i + tlen <= slen; i++) {
for (j = 0; j < tlen; j++) {
char a = (char)tolower((unsigned char)s->buf[i + j]);
char b = (char)tolower((unsigned char)token[j]);
if (a != b) {
break;
}
}
if (j == tlen) {
return true;
}
}
return false;
}
static int fd_parse_content_range_start(const struct mg_str *s, size_t *start_out)
{
char tmp[64];
unsigned long start_ul = 0;
unsigned long end_ul = 0;
unsigned long total_ul = 0;
if (s == NULL || s->buf == NULL || start_out == NULL) {
return -1;
}
size_t n = s->len < sizeof(tmp) - 1U ? s->len : sizeof(tmp) - 1U;
memcpy(tmp, s->buf, n);
tmp[n] = '\0';
/* e.g. "bytes 123-456/789" */
if (sscanf(tmp, "bytes %lu-%lu/%lu", &start_ul, &end_ul, &total_ul) < 2) {
return -1;
}
(void)end_ul;
(void)total_ul;
*start_out = (size_t)start_ul;
return 0;
}
static void fd_abort(struct mg_connection *c, hd_ctx_t *ctx, int err)
{
if (ctx->done) {
return;
}
if (ctx->fd >= 0) {
(void)fs_adapt_close(ctx->fd);
ctx->fd = -1;
}
ctx->result = err;
ctx->done = true;
c->is_closing = 1;
}
static void fd_finish_ok(struct mg_connection *c, hd_ctx_t *ctx)
{
unsigned int final_size = 0U;
if (ctx->done) {
return;
}
if (ctx->fd >= 0) {
(void)fs_adapt_sync(ctx->fd);
(void)fs_adapt_close(ctx->fd);
ctx->fd = -1;
}
/*
* 第一层完整性校验:接收写入总字节必须等于 HTTP 头推导的期望总长
* (200 + Content-Length) 或 (206 + resume_offset + Content-Length)。
*/
if (ctx->expected_known && (ctx->bytes_written != ctx->expected_bytes)) {
ctx->result = -1;
ctx->done = true;
c->is_closing = 1;
printf("[HttpLfs] size mismatch: got=%u expected=%u\r\n",
(unsigned int)ctx->bytes_written, (unsigned int)ctx->expected_bytes);
return;
}
/* ATOMIC:先删旧正式文件,再把临时文件 rename 覆盖;DIRECT:已在目标路径上写完 */
if (!ctx->direct_write) {
(void)fs_adapt_delete(ctx->final_path);
if (fs_adapt_rename(ctx->path, ctx->final_path) != 0) {
(void)fs_adapt_delete(ctx->path);
ctx->result = -1;
ctx->done = true;
c->is_closing = 1;
printf("[HttpLfs] rename failed: %s -> %s\r\n", ctx->path, ctx->final_path);
return;
}
}
/* 第二层完整性校验:落盘后的最终文件大小再对比一次 */
if (ctx->expected_known) {
if (fs_adapt_stat(ctx->final_path, &final_size) != 0 ||
(size_t)final_size != ctx->expected_bytes) {
(void)fs_adapt_delete(ctx->final_path);
ctx->result = -1;
ctx->done = true;
c->is_closing = 1;
printf("[HttpLfs] final file size verify failed: stat=%u expected=%u\r\n",
final_size, (unsigned int)ctx->expected_bytes);
return;
}
}
ctx->result = 0;
ctx->done = true;
if (ctx->expected_known) {
printf("[HttpLfs] http done %u/%u bytes\r\n",
(unsigned int)ctx->bytes_written, (unsigned int)ctx->expected_bytes);
} else {
printf("[HttpLfs] http done %u bytes (until-close)\r\n",
(unsigned int)ctx->bytes_written);
}
c->is_closing = 1;
}
static void fd_strip_http_headers(struct mg_connection *c, struct mg_http_message *hm)
{
size_t hdr_len = (size_t)(hm->body.buf - (char *)c->recv.buf);
if (hdr_len > c->recv.len) {
return;
}
mg_iobuf_del(&c->recv, 0, hdr_len);
}
static void fd_consume_content_length(struct mg_connection *c, hd_ctx_t *ctx)
{
while (ctx->content_remaining > 0U && c->recv.len > 0U) {
size_t n = c->recv.len;
if (n > ctx->content_remaining) {
n = ctx->content_remaining;
}
int w = fs_adapt_write(ctx->fd, (const char *)c->recv.buf, (unsigned int)n);
if (w < 0 || (size_t)w != n) {
fd_abort(c, ctx, -1);
return;
}
mg_iobuf_del(&c->recv, 0, n);
ctx->content_remaining -= n;
ctx->bytes_written += n;
ctx->last_body_ms = mg_millis();
}
if (ctx->content_remaining == 0U) {
fd_finish_ok(c, ctx);
}
}
static void fd_consume_until_close(struct mg_connection *c, hd_ctx_t *ctx)
{
while (c->recv.len > 0U) {
size_t n = c->recv.len;
int w = fs_adapt_write(ctx->fd, (const char *)c->recv.buf, (unsigned int)n);
if (w < 0 || (size_t)w != n) {
fd_abort(c, ctx, -1);
return;
}
mg_iobuf_del(&c->recv, 0, n);
ctx->bytes_written += n;
ctx->last_body_ms = mg_millis();
}
}
static void fd_consume_chunked(struct mg_connection *c, hd_ctx_t *ctx)
{
for (;;) {
if (c->recv.len == 0U) {
return;
}
if (ctx->chunk_payload_left == 0U) {
char *buf = (char *)c->recv.buf;
size_t i = 0;
while (i + 1U < c->recv.len && !(buf[i] == '\r' && buf[i + 1U] == '\n')) {
i++;
}
if (i + 1U >= c->recv.len) {
return;
}
unsigned long csz = 0;
if (fd_parse_chunk_size_line(buf, i, &csz) != 0) {
fd_abort(c, ctx, -1);
return;
}
mg_iobuf_del(&c->recv, 0, i + 2U);
if (csz == 0UL) {
fd_finish_ok(c, ctx);
return;
}
ctx->chunk_payload_left = (size_t)csz;
ctx->last_body_ms = mg_millis();
continue;
}
size_t n = c->recv.len;
if (n > ctx->chunk_payload_left) {
n = ctx->chunk_payload_left;
}
int w = fs_adapt_write(ctx->fd, (const char *)c->recv.buf, (unsigned int)n);
if (w < 0 || (size_t)w != n) {
fd_abort(c, ctx, -1);
return;
}
mg_iobuf_del(&c->recv, 0, n);
ctx->chunk_payload_left -= n;
ctx->bytes_written += n;
ctx->last_body_ms = mg_millis();
if (ctx->chunk_payload_left == 0U) {
if (c->recv.len < 2U) {
return;
}
if (((char *)c->recv.buf)[0] != '\r' || ((char *)c->recv.buf)[1] != '\n') {
fd_abort(c, ctx, -1);
return;
}
mg_iobuf_del(&c->recv, 0, 2U);
}
}
}
static void fd_dispatch_body(struct mg_connection *c, hd_ctx_t *ctx)
{
switch (ctx->mode) {
case FD_BODY_CONTENT_LENGTH:
fd_consume_content_length(c, ctx);
break;
case FD_BODY_CHUNKED:
fd_consume_chunked(c, ctx);
break;
case FD_BODY_UNTIL_CLOSE:
fd_consume_until_close(c, ctx);
break;
default:
break;
}
}
static void hd_ev_handler(struct mg_connection *c, int ev, void *ev_data)
{
hd_ctx_t *ctx = (hd_ctx_t *)c->fn_data;
if (ctx == NULL) {
return;
}
if (ev == MG_EV_OPEN) {
(void)ev_data;
ctx->connect_deadline_ms = mg_millis() + HD_CONNECT_MS;
return;
}
if (ev == MG_EV_POLL) {
if (!ctx->hdrs_ok) {
if (mg_millis() > ctx->connect_deadline_ms &&
(c->is_connecting || c->is_resolving)) {
mg_error(c, "http_lfs connect timeout");
}
return;
}
if (ctx->done) {
return;
}
if ((mg_millis() - ctx->last_body_ms) > HD_BODY_IDLE_MS) {
mg_error(c, "http_lfs body idle timeout");
}
return;
}
if (ev == MG_EV_CONNECT) {
/*
* mg_url_host() 不含端口;非 80 端口必须在 Host 中带 :port,否则虚拟主机/网关可能直接断连。
*/
struct mg_str host = mg_url_host(ctx->url_buf);
unsigned short port = mg_url_port(ctx->url_buf);
if (port == 80U) {
if (ctx->try_range && ctx->resume_offset > 0U) {
mg_printf(c,
"GET %s HTTP/1.1\r\n"
"Host: %.*s\r\n"
"Connection: close\r\n"
"Accept: */*\r\n"
"Range: bytes=%u-\r\n"
"\r\n",
mg_url_uri(ctx->url_buf), (int)host.len, host.buf,
(unsigned int)ctx->resume_offset);
} else {
mg_printf(c,
"GET %s HTTP/1.1\r\n"
"Host: %.*s\r\n"
"Connection: close\r\n"
"Accept: */*\r\n"
"\r\n",
mg_url_uri(ctx->url_buf), (int)host.len, host.buf);
}
} else {
if (ctx->try_range && ctx->resume_offset > 0U) {
mg_printf(c,
"GET %s HTTP/1.1\r\n"
"Host: %.*s:%u\r\n"
"Connection: close\r\n"
"Accept: */*\r\n"
"Range: bytes=%u-\r\n"
"\r\n",
mg_url_uri(ctx->url_buf), (int)host.len, host.buf, (unsigned int)port,
(unsigned int)ctx->resume_offset);
} else {
mg_printf(c,
"GET %s HTTP/1.1\r\n"
"Host: %.*s:%u\r\n"
"Connection: close\r\n"
"Accept: */*\r\n"
"\r\n",
mg_url_uri(ctx->url_buf), (int)host.len, host.buf, (unsigned int)port);
}
}
return;
}
if (ev == MG_EV_HTTP_HDRS) {
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
int st = mg_http_status(hm);
if (st == 416 && ctx->try_range) {
printf("[HttpLfs] range not satisfiable, fallback full download\r\n");
fd_abort(c, ctx, HTTP_LFS_RES_FALLBACK_FULL);
return;
}
if (st != 200 && st != 206) {
printf("[HttpLfs] unexpected http status=%d\r\n", st);
fd_abort(c, ctx, -1);
return;
}
struct mg_str *te = mg_http_get_header(hm, "Transfer-Encoding");
bool chunked = fd_header_contains_token_ci(te, "chunked");
struct mg_str *clh = mg_http_get_header(hm, "Content-Length");
struct mg_str *crh = mg_http_get_header(hm, "Content-Range");
size_t cl = 0;
if (clh != NULL && fd_parse_content_length(clh, &cl) == 0) {
printf("[HttpLfs] http %d, te=%s, cl=%u, range_try=%u off=%u\r\n",
st, (te != NULL) ? "yes" : "no", (unsigned int)cl,
(unsigned int)(ctx->try_range ? 1U : 0U), (unsigned int)ctx->resume_offset);
} else {
printf("[HttpLfs] http %d, te=%s, cl=n/a, range_try=%u off=%u\r\n",
st, (te != NULL) ? "yes" : "no",
(unsigned int)(ctx->try_range ? 1U : 0U), (unsigned int)ctx->resume_offset);
}
if (chunked) {
ctx->mode = FD_BODY_CHUNKED;
} else if (clh != NULL) {
if (fd_parse_content_length(clh, &cl) != 0) {
fd_abort(c, ctx, -1);
return;
}
ctx->mode = FD_BODY_CONTENT_LENGTH;
ctx->content_remaining = cl;
ctx->expected_known = true;
ctx->expected_bytes = cl + ((st == 206) ? ctx->resume_offset : 0U);
} else {
ctx->mode = FD_BODY_UNTIL_CLOSE;
}
if (fs_adapt_mkdir(ctx->base_dir) != 0) {
fd_abort(c, ctx, -1);
return;
}
if (st == 206) {
size_t range_start = 0U;
if (ctx->resume_offset == 0U || crh == NULL ||
fd_parse_content_range_start(crh, &range_start) != 0 ||
range_start != ctx->resume_offset) {
printf("[HttpLfs] bad content-range, fallback full\r\n");
fd_abort(c, ctx, HTTP_LFS_RES_FALLBACK_FULL);
return;
}
ctx->fd = fs_adapt_open(ctx->path, O_WRONLY | O_CREAT);
} else {
ctx->fd = fs_adapt_open(ctx->path, O_WRONLY | O_CREAT | O_TRUNC);
}
if (ctx->fd < 0) {
fd_abort(c, ctx, -1);
return;
}
if (st == 206) {
if (fs_adapt_seek(ctx->fd, (int)ctx->resume_offset, SEEK_SET) < 0) {
fd_abort(c, ctx, -1);
return;
}
ctx->bytes_written = ctx->resume_offset;
printf("[HttpLfs] resume from offset=%u\r\n", (unsigned int)ctx->resume_offset);
} else if (ctx->resume_offset > 0U) {
printf("[HttpLfs] server ignored range, restart from zero\r\n");
}
fd_strip_http_headers(c, hm);
ctx->hdrs_ok = true;
ctx->last_body_ms = mg_millis();
fd_dispatch_body(c, ctx);
return;
}
if (ev == MG_EV_READ) {
if (!ctx->hdrs_ok || ctx->done) {
return;
}
fd_dispatch_body(c, ctx);
return;
}
if (ev == MG_EV_CLOSE) {
if (ctx->done) {
return;
}
if (!ctx->hdrs_ok) {
ctx->result = -1;
ctx->done = true;
return;
}
if (ctx->mode == FD_BODY_UNTIL_CLOSE) {
if (ctx->fd >= 0) {
fd_finish_ok(c, ctx);
} else {
ctx->result = -1;
ctx->done = true;
}
return;
}
if (ctx->expected_known) {
printf("[HttpLfs] http closed early: got %u/%u bytes, remain=%u\r\n",
(unsigned int)ctx->bytes_written, (unsigned int)ctx->expected_bytes,
(unsigned int)ctx->content_remaining);
} else {
printf("[HttpLfs] http closed early in non-until-close mode, got=%u bytes\r\n",
(unsigned int)ctx->bytes_written);
}
fd_abort(c, ctx, -1);
return;
}
if (ev == MG_EV_ERROR) {
if (ev_data != NULL) {
printf("[HttpLfs] MG_EV_ERROR: %s\r\n", (const char *)ev_data);
} else {
printf("[HttpLfs] MG_EV_ERROR\r\n");
}
fd_abort(c, ctx, -1);
}
}
int http_lfs_download_blocking(const http_lfs_download_param_t *param)
{
size_t resume_bytes = 0U;
bool try_range_once = false;
bool range_disabled_for_session = false;
const char *part_suffix;
bool direct;
if (param == NULL || param->url == NULL || param->base_dir == NULL || param->basename == NULL) {
return -1;
}
direct = (param->store_mode == HTTP_LFS_STORE_DIRECT);
part_suffix = (param->part_suffix != NULL && param->part_suffix[0] != '\0') ? param->part_suffix : ".part";
retry_connect:
if (param->url[0] == '\0' || param->base_dir[0] == '\0' || param->basename[0] == '\0') {
return -1;
}
if (strncmp(param->url, "http://", 7) != 0) {
return -1;
}
if (strchr(param->basename, '/') != NULL || strstr(param->basename, "..") != NULL) {
return -1;
}
if (strlen(param->basename) > 48U) {
return -1;
}
if (strstr(param->base_dir, "..") != NULL) {
return -1;
}
struct mg_mgr mgr;
mg_mgr_init(&mgr);
hd_ctx_t *ctx = (hd_ctx_t *)calloc(1, sizeof(*ctx));
if (ctx == NULL) {
mg_mgr_free(&mgr);
return -1;
}
ctx->fd = -1;
ctx->result = -1;
if (snprintf(ctx->url_buf, sizeof(ctx->url_buf), "%s", param->url) >= (int)sizeof(ctx->url_buf)) {
free(ctx);
mg_mgr_free(&mgr);
return -1;
}
if (snprintf(ctx->base_dir, sizeof(ctx->base_dir), "%s", param->base_dir) >= (int)sizeof(ctx->base_dir)) {
free(ctx);
mg_mgr_free(&mgr);
return -1;
}
if (snprintf(ctx->final_path, sizeof(ctx->final_path), "%s/%s", ctx->base_dir, param->basename) >=
(int)sizeof(ctx->final_path)) {
free(ctx);
mg_mgr_free(&mgr);
return -1;
}
ctx->direct_write = direct;
if (direct) {
if (snprintf(ctx->path, sizeof(ctx->path), "%s/%s", ctx->base_dir, param->basename) >= (int)sizeof(ctx->path)) {
free(ctx);
mg_mgr_free(&mgr);
return -1;
}
} else {
if (snprintf(ctx->path, sizeof(ctx->path), "%s/%s%s", ctx->base_dir, param->basename, part_suffix) >=
(int)sizeof(ctx->path)) {
free(ctx);
mg_mgr_free(&mgr);
return -1;
}
}
/*
* Range 续传:若存在临时文件,则先尝试从其大小处续传。
* 416 等情况会回退全量(HTTP_LFS_RES_FALLBACK_FULL)。
*/
{
unsigned int sz = 0U;
if (!range_disabled_for_session && try_range_once == false &&
fs_adapt_stat(ctx->path, &sz) == 0 && sz > 0U) {
resume_bytes = (size_t)sz;
try_range_once = true;
}
}
ctx->try_range = try_range_once;
ctx->resume_offset = try_range_once ? resume_bytes : 0U;
if (!ctx->try_range) {
(void)fs_adapt_delete(ctx->path);
}
FD_MG_LOCK();
struct mg_connection *c = mg_http_connect(&mgr, ctx->url_buf, hd_ev_handler, ctx);
FD_MG_UNLOCK();
if (c == NULL) {
free(ctx);
mg_mgr_free(&mgr);
return -1;
}
uint64_t overall_deadline = mg_millis() + HD_OVERALL_MS;
while (!ctx->done && mg_millis() < overall_deadline) {
FD_MG_LOCK();
mg_mgr_poll(&mgr, HD_MGR_POLL_MS);
FD_MG_UNLOCK();
osal_msleep(1U);
}
if (!ctx->done) {
FD_MG_LOCK();
mg_error(c, "http_lfs overall timeout");
mg_mgr_poll(&mgr, HD_MGR_POLL_MS);
FD_MG_UNLOCK();
while (!ctx->done && mg_millis() < overall_deadline + 2000UL) {
FD_MG_LOCK();
mg_mgr_poll(&mgr, HD_MGR_POLL_MS);
FD_MG_UNLOCK();
osal_msleep(1U);
}
}
int res = ctx->result;
FD_MG_LOCK();
mg_mgr_free(&mgr);
FD_MG_UNLOCK();
free(ctx);
if (res == HTTP_LFS_RES_FALLBACK_FULL) {
range_disabled_for_session = true;
try_range_once = false;
resume_bytes = 0U;
goto retry_connect;
}
return res;
}
5. 字库下载使用示例
将 font.bin 下载到 LittleFS /system/font.bin ,中断后可从 /system/font.bin.part 续传,成功后原子替换:
c
#include "http_lfs_download.h"
static int download_font_to_system(void)
{
http_lfs_download_param_t p = {
.url = "http://192.168.1.100/static/font.bin",
.base_dir = "/system",
.basename = "font.bin",
.part_suffix = ".part", /* 可改为 NULL 使用默认 ".part" */
.store_mode = HTTP_LFS_STORE_ATOMIC,
};
int r = http_lfs_download_blocking(&p);
if (r != 0) {
printf("font download failed, ret=%d\r\n", r);
return -1;
}
printf("font.bin ready under /system\r\n");
return 0;
}
首次下载 :无 .part 时先删再下,得到完整 font.bin.part → 校验通过 → font.bin 。
中途断电/断网 :保留 font.bin.part (已有字节数 N ),再次调用同一参数:自动 Range: bytes=N- ,206 后 seek 到 N 继续写,完成后 rename 为 font.bin。
若业务必须 直接写 font.bin(不推荐用于需断点场景):
c
p.store_mode = HTTP_LFS_STORE_DIRECT;
p.basename = "font.bin";
/* part_suffix 忽略;续传基于已存在的 /system/font.bin 大小 */
6. 与 font_download.c 的关系
| 项目 | font_download.c |
http_lfs_download.c |
|---|---|---|
| 路径 | 固定 /system + 单文件名 |
任意 base_dir + basename |
| 断点 | 无 Range,每次 O_TRUNC |
Range + .part + 416 回退 |
| 原子性 | 直接写目标文件 | 默认 临时文件 + rename |
| 适用 | 简单一次性拉取 | 通用大文件 / 字库 / 资源包 |
字库场景建议:优先使用 http_lfs_download_blocking ;仅在不需要续传、希望代码路径极简时再用专用 font_download。
7. 服务端与调试建议
- 服务器需支持
Range/206 Partial Content且返回正确Content-Range(静态文件服务器如 Nginx、多数对象存储一般支持)。 - 若总是 200 且从头传:检查 CDN/网关是否剥离 Range;实现上仍会 截断重写 临时文件,不会错误拼接双份内容。
- 日志关键字:
[HttpLfs],便于串口排查 status、CL、resume offset、size mismatch、rename failed。
8. 小结
本封装在 Mongoose 上以 mg_http_connect + 状态机回调 实现 HTTP/1.1 下载 ,通过 临时文件 + Range/206/Content-Range 完成 断点续传 ,并以 rename + 双重长度校验 保证 ATOMIC 模式下的落盘一致性。字库等大体量文件可 base_dir="/system"、basename="font.bin" 直接复用;超时与 poll 间隔可按现场网络在 http_lfs_download.c 宏中调整。
关联源文件:src/network/http_lfs_download.c、src/network/http_lfs_download.h;参考:src/network/font_download.c、src/littlefs/littlefs_adapt.c。