4. SPIFFS 目录操作:创建目录、遍历目录、删除目录与文件

文件系统嘛,光会读写文件肯定不够。你想想看,一个正经的嵌入式项目,尤其是带网页服务器的,文件一多就得分类管理。CSS 放一个文件夹,JS 放一个,图片再放一个——这时候目录操作就派上用场了。

我个人习惯,在项目初期就把 SPIFFS 的目录结构规划好。别等到文件堆了一堆再回头整理,那可就麻烦了。这一节,咱们就聊聊怎么在 SPIFFS 里创建目录、遍历目录,以及怎么安全地删除文件和目录。

核心要点: SPIFFS 是扁平文件系统,不支持嵌套子目录。所有路径都是平的,但你可以用路径分隔符 "/" 来模拟目录结构。

4.1 创建目录:mkdir() 的正确用法

SPIFFS 的 mkdir() 函数,说白了就是告诉文件系统:「嘿,我要在这个路径下放文件了,你帮我记一下。」但它跟 PC 上的 mkdir 不太一样——它不会真的创建「文件夹」实体,只是注册一个路径前缀。

#include <SPIFFS.h>

void setup() {
  Serial.begin(115200);
  
  if (!SPIFFS.begin(true)) {
    Serial.println("SPIFFS 挂载失败");
    return;
  }

  // 创建目录(实际上是注册路径)
  if (SPIFFS.mkdir("/www")) {
    Serial.println("/www 目录创建成功");
  } else {
    Serial.println("/www 目录创建失败");
  }

  // 创建多级路径
  if (SPIFFS.mkdir("/www/css")) {
    Serial.println("/www/css 创建成功");
  }

  if (SPIFFS.mkdir("/www/js")) {
    Serial.println("/www/js 创建成功");
  }
}

我在项目中遇到过一个问题:明明调用了 mkdir(),返回也是 true,但用 openDir() 遍历时却看不到这个目录。后来才发现,空目录在 SPIFFS 里是不存在的。只有当你往这个路径下写了一个文件,目录才会真正「出现」。

小技巧: 创建目录后,立刻在里面放一个占位文件(比如 .keep),这样目录就能被遍历到了。

4.2 遍历目录:openDir() 与文件枚举

遍历目录,说白了就是「翻翻这个文件夹里都有啥」。SPIFFS 提供了 openDir() 函数,返回一个目录迭代器,你可以用它来逐个读取文件和子目录。

void listDir(const char *dirname) {
  Serial.printf("列出目录: %s\n", dirname);
  
  File root = SPIFFS.open(dirname);
  if (!root) {
    Serial.println("打开目录失败");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("这不是一个目录");
    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()。我记得有一次调试,发现内存占用越来越高,排查了半天才发现是遍历时没关文件句柄。

避坑指南: 我曾经在遍历目录时同时进行文件删除操作,结果导致迭代器混乱,程序崩溃。正确的做法是:先遍历收集文件名,再统一删除。

4.3 删除文件与目录:remove() 与 rmdir()

删除操作看起来简单,但坑不少。先看基本用法:

// 删除单个文件
if (SPIFFS.remove("/www/style.css")) {
  Serial.println("文件删除成功");
} else {
  Serial.println("文件删除失败");
}

// 删除目录(目录必须为空)
if (SPIFFS.rmdir("/www/css")) {
  Serial.println("目录删除成功");
} else {
  Serial.println("目录删除失败,可能不为空");
}

rmdir() 有个硬性要求:目录必须为空。如果目录里还有文件,它会返回 false。那怎么删除非空目录呢?得自己写递归:

bool deleteRecursive(const char *path) {
  File file = SPIFFS.open(path);
  if (!file) return false;

  // 如果是目录,先清空里面的内容
  if (file.isDirectory()) {
    File child = file.openNextFile();
    while (child) {
      char childPath[64];
      snprintf(childPath, sizeof(childPath), "%s/%s", 
               path, child.name());
      
      if (child.isDirectory()) {
        deleteRecursive(childPath);  // 递归删除子目录
      } else {
        SPIFFS.remove(childPath);    // 删除文件
      }
      
      child = file.openNextFile();
    }
    file.close();
    return SPIFFS.rmdir(path);  // 目录清空后删除
  } else {
    file.close();
    return SPIFFS.remove(path);  // 直接删除文件
  }
}
重要提醒: SPIFFS 的 remove() 和 rmdir() 操作是不可逆的。删除前最好做个确认,尤其是 OTA 升级时,别把正在运行的网页文件给删了。

4.4 目录操作的核心流程

下面这张图,是我在实际项目中总结的目录操作流程。你照着这个顺序来,基本不会出问题。

SPIFFS 目录操作核心流程 挂载 SPIFFS 检查目录是否存在 存在? 是 遍历/删除/写入 否 创建目录 mkdir() 操作完成 关闭 SPIFFS ⚠ 注意: 空目录在 SPIFFS 中不可见 删除前务必确认文件未使用

4.5 目录操作函数速查表

为了方便你查阅,我把常用的目录操作函数整理成了表格。建议你收藏一下,写代码时直接翻出来看。

函数 功能 返回值 注意事项
SPIFFS.mkdir(path) 创建目录 bool 空目录不可见,需放占位文件
SPIFFS.openDir(path) 打开目录迭代器 Dir 对象 路径不存在时返回空迭代器
dir.next() 移动到下一个文件 bool 返回 false 表示遍历结束
dir.fileName() 获取当前文件名 String 包含完整路径
dir.fileSize() 获取当前文件大小 size_t 目录大小为 0
SPIFFS.remove(path) 删除文件 bool 只能删文件,不能删目录
SPIFFS.rmdir(path) 删除空目录 bool 目录不为空时返回 false
SPIFFS.exists(path) 检查路径是否存在 bool 文件和目录都适用

4.6 实战:网页资源目录管理

说了这么多理论,咱们来点实际的。假设你要做一个智能家居控制面板,网页资源需要分类存放。下面这段代码,展示了完整的目录初始化流程:

bool initWebDirectories() {
  // 定义需要的目录结构
  const char* dirs[] = {
    "/www",
    "/www/css",
    "/www/js",
    "/www/img",
    "/www/fonts"
  };
  
  for (int i = 0; i < 5; i++) {
    if (!SPIFFS.exists(dirs[i])) {
      if (!SPIFFS.mkdir(dirs[i])) {
        Serial.printf("创建目录失败: %s\n", dirs[i]);
        return false;
      }
      // 创建占位文件,确保目录可见
      String placeholder = String(dirs[i]) + "/.keep";
      File f = SPIFFS.open(placeholder, "w");
      if (f) f.close();
    }
  }
  
  // 验证目录结构
  Serial.println("目录结构验证:");
  listDir("/www");
  
  return true;
}

void setup() {
  Serial.begin(115200);
  SPIFFS.begin(true);
  
  if (initWebDirectories()) {
    Serial.println("网页目录初始化成功");
  } else {
    Serial.println("网页目录初始化失败");
  }
}
经验之谈: 我习惯在项目初始化时,用 exists() 检查目录是否存在。如果不存在就创建,存在就跳过。这样即使设备意外断电重启,目录结构也不会乱。

4.7 避坑指南:目录操作的常见问题

做嵌入式开发这么多年,我在目录操作上踩过的坑,说出来都是泪。下面这几个问题,你大概率也会遇到:

  • 路径分隔符问题: SPIFFS 统一用 "/",别用 Windows 的 "\"。我见过有人从 PC 复制路径过来,结果死活打不开文件。
  • 文件名大小写: SPIFFS 是大小写敏感的。"Index.html" 和 "index.html" 是两个不同的文件。建议统一用小写。
  • 路径长度限制: 单个路径不要超过 32 个字符。太长的话,open() 会返回空指针。
  • 并发访问: 不要在中断服务程序里操作 SPIFFS。文件系统不是线程安全的,同时操作会导致数据损坏。
  • 删除后的空间释放: 删除文件后,SPIFFS 不会立即回收空间。需要调用 SPIFFS.info() 查看实际使用情况。
曾经踩过的坑: 有一次我在 OTA 升级后,发现网页打不开了。排查了半天,原来是升级脚本把 /www 目录整个删除了,但新的固件还没来得及写入文件。从那以后,我都在升级前先备份目录结构,升级完再恢复。