3. SPIFFS 目录管理:从创建到遍历,再到文件信息获取
SPIFFS 的文件系统,说白了就是一个扁平的存储空间。但咱们做项目时,总不能把所有文件都扔在根目录下吧?那不乱成一锅粥了。所以,目录管理就成了必备技能。
我个人习惯,在 SPIFFS 里至少分三个目录:/config 放配置文件,/www 放网页资源,/data 放传感器日志。这样找东西快,也方便后续做 OTA 升级时只替换特定目录。
3.1 创建目录:mkdir 的细节
ESP32 的 SPIFFS 库提供了 mkdir() 函数。用法很简单,但有几个坑我得先跟你说。
#include <SPIFFS.h>
void setup() {
Serial.begin(115200);
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS 挂载失败");
return;
}
// 创建单层目录
if (SPIFFS.mkdir("/config")) {
Serial.println("/config 目录创建成功");
} else {
Serial.println("/config 目录创建失败");
}
// 创建多层目录
if (SPIFFS.mkdir("/www/css")) {
Serial.println("/www/css 目录创建成功");
} else {
Serial.println("/www/css 目录创建失败");
}
}
⚠️ 注意: SPIFFS 的 mkdir 不会自动创建父目录。如果你直接创建
/a/b/c,而 /a 不存在,它会失败。所以,要么先创建父目录,要么用递归方式逐层创建。
我曾经在项目里吃过这个亏。当时想一次性创建 /logs/2024/01,结果死活不成功,查了半天才发现是父目录没创建。后来我写了个递归创建函数,一劳永逸。
3.2 列出目录内容:openDir 的正确用法
列出目录内容,用的是 openDir() 函数。它返回一个 File 对象,你可以像迭代器一样遍历它。
void listDir(const char *dirname) {
File root = SPIFFS.open(dirname);
if (!root) {
Serial.printf("打开目录 %s 失败\n", dirname);
return;
}
if (!root.isDirectory()) {
Serial.printf("%s 不是目录\n", dirname);
root.close();
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.printf(" [DIR] %s\n", file.name());
} else {
Serial.printf(" [FILE] %s, 大小: %d 字节\n", file.name(), file.size());
}
file = root.openNextFile();
}
root.close();
}
💡 小技巧:
openNextFile() 返回的 File 对象,用完会自动释放,但建议显式调用 close() 关闭根目录对象。我见过有人不关,结果 SPIFFS 句柄泄漏,跑几天就崩了。
3.3 递归遍历目录:深入每一层
单层遍历不够用?那就递归。递归遍历能让你看到整个目录树的全貌。我一般在做文件系统备份或统计时用这个。
void recursiveListDir(const char *dirname, int level = 0) {
File root = SPIFFS.open(dirname);
if (!root) {
Serial.printf("打开目录 %s 失败\n", dirname);
return;
}
if (!root.isDirectory()) {
Serial.printf("%s 不是目录\n", dirname);
root.close();
return;
}
File file = root.openNextFile();
while (file) {
// 打印缩进
for (int i = 0; i < level; i++) {
Serial.print(" ");
}
if (file.isDirectory()) {
Serial.printf("[DIR] %s\n", file.name());
// 递归进入子目录
recursiveListDir(file.path(), level + 1);
} else {
Serial.printf("[FILE] %s (%d bytes)\n", file.name(), file.size());
}
file = root.openNextFile();
}
root.close();
}
你想想看,这个递归函数其实就做了两件事:遇到目录就打印并进入,遇到文件就打印信息。简单吧?但要注意,SPIFFS 的目录深度最好不要超过 5 层,否则递归栈可能会溢出。嗯,这是经验之谈。
3.4 删除目录:rmdir 与 remove 的区别
删除目录比创建目录麻烦。SPIFFS 的 rmdir() 只能删除空目录。如果目录里有文件,你得先删文件,再删目录。
bool deleteDir(const char *dirname) {
File root = SPIFFS.open(dirname);
if (!root) {
Serial.printf("打开目录 %s 失败\n", dirname);
return false;
}
if (!root.isDirectory()) {
Serial.printf("%s 不是目录\n", dirname);
root.close();
return false;
}
// 先删除目录下的所有文件和子目录
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
// 递归删除子目录
deleteDir(file.path());
} else {
// 删除文件
SPIFFS.remove(file.path());
Serial.printf("删除文件: %s\n", file.path());
}
file = root.openNextFile();
}
root.close();
// 最后删除空目录
if (SPIFFS.rmdir(dirname)) {
Serial.printf("删除目录: %s\n", dirname);
return true;
} else {
Serial.printf("删除目录 %s 失败\n", dirname);
return false;
}
}
📌 核心区别:
SPIFFS.remove():删除文件,不能删目录。SPIFFS.rmdir():删除空目录,不能删文件。- 要删非空目录,必须手动递归清理。
3.5 获取文件信息:大小、时间戳
SPIFFS 的文件信息,主要通过 File 对象的方法获取。常用的有 size() 和 getLastWrite()。
| 方法 | 返回值 | 说明 |
|---|---|---|
size() |
size_t |
文件大小,单位字节 |
name() |
const char* |
文件名(不含路径) |
path() |
const char* |
完整路径 |
isDirectory() |
bool |
是否为目录 |
getLastWrite() |
time_t |
最后写入时间戳(UNIX 时间) |
void printFileInfo(const char *filepath) {
File file = SPIFFS.open(filepath);
if (!file) {
Serial.printf("打开文件 %s 失败\n", filepath);
return;
}
Serial.printf("文件名: %s\n", file.name());
Serial.printf("路径: %s\n", file.path());
Serial.printf("大小: %d 字节\n", file.size());
Serial.printf("是目录: %s\n", file.isDirectory() ? "是" : "否");
time_t lastWrite = file.getLastWrite();
if (lastWrite > 0) {
struct tm *timeinfo = localtime(&lastWrite);
char buffer[30];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeinfo);
Serial.printf("最后写入: %s\n", buffer);
} else {
Serial.println("最后写入: 未知");
}
file.close();
}
💡 关于时间戳: SPIFFS 的时间戳依赖于系统 RTC。如果你的 ESP32 没有联网同步时间,
getLastWrite() 返回的可能是 0 或一个随机值。我建议在项目初始化时,先通过 NTP 获取一次时间,再操作 SPIFFS 文件。
3.6 知识结构总览
下面这张图,把 SPIFFS 目录管理的核心操作串起来了。你可以把它当作一个快速参考。
3.7 避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间。
- 路径格式: SPIFFS 的路径必须以
/开头,比如/config,不能写成config。否则函数会返回失败。 - 句柄泄漏: 每次
open()或openDir()后,记得close()。SPIFFS 同时打开的文件句柄有限,一般是 4 个左右。我曾经在循环里忘了关,结果程序跑着跑着就报错。 - 大小写敏感: SPIFFS 是大小写敏感的。
/Config和/config是两个不同的目录。我建议统一用小写,省得搞混。 - 删除前检查: 删除目录或文件前,最好先检查它是否存在。用
SPIFFS.exists()判断一下,避免误删。
📌 总结一下: 目录管理看似简单,但细节不少。创建时注意父目录,删除时注意递归清理,遍历时注意句柄释放。把这些基础打牢,后面做 Web 服务器文件管理就顺手多了。