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 目录管理的核心操作串起来了。你可以把它当作一个快速参考。

SPIFFS 目录管理核心操作 SPIFFS 目录管理核心操作 SPIFFS 目录管理 创建目录 列出目录内容 删除目录 获取文件信息 递归遍历目录 📌 关键点: • mkdir 不会自动创建父目录,需要逐层创建 • rmdir 只能删除空目录,非空目录需先递归删除内部文件 • 时间戳依赖 RTC,建议先同步 NTP 时间

3.7 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间。

  • 路径格式: SPIFFS 的路径必须以 / 开头,比如 /config,不能写成 config。否则函数会返回失败。
  • 句柄泄漏: 每次 open() 或 openDir() 后,记得 close()。SPIFFS 同时打开的文件句柄有限,一般是 4 个左右。我曾经在循环里忘了关,结果程序跑着跑着就报错。
  • 大小写敏感: SPIFFS 是大小写敏感的。/Config 和 /config 是两个不同的目录。我建议统一用小写,省得搞混。
  • 删除前检查: 删除目录或文件前,最好先检查它是否存在。用 SPIFFS.exists() 判断一下,避免误删。
📌 总结一下: 目录管理看似简单,但细节不少。创建时注意父目录,删除时注意递归清理,遍历时注意句柄释放。把这些基础打牢,后面做 Web 服务器文件管理就顺手多了。