Удалить каталог, который не пуст


300

В моем приложении Node мне нужно удалить каталог, в котором есть несколько файлов, но который fs.rmdirработает только с пустыми каталогами. Как я могу это сделать?


1
Вкратце: fs.readdir(dirPath)для массива путей в папке выполните итерацию, fs.unlink(filename)чтобы удалить каждый файл, а затем, наконец, fs.rmdir(dirPath)удалить пустую папку. Если вам нужно пройти курс, проверьте fs.lstat(filename).isDirectory().
— ионосферные

Ответы:


319

Для этого есть модуль rimraf( https://npmjs.org/package/rimraf ). Он обеспечивает ту же функциональность, что иrm -Rf

Асинхронное использование:

var rimraf = require("rimraf");
rimraf("/some/directory", function () { console.log("done"); });

Синхронизация использования:

rimraf.sync("/some/directory");

1
Странно, я никогда не видел такого поведения. Я бы посоветовал поискать и / или зарегистрировать ошибку. github.com/isaacs/rimraf/issues
— Morgan ARR Allen

35
Это легко сделать с помощью NodeJS Core libs. Зачем устанавливать неподдерживаемый сторонний пакет?
— SudoKid

4
@EmettSpeer Когда вы имеете в виду под "быть легко сделано"? Самостоятельная запись функции, как deleteFolderRecursiveв следующем ответе?
— Freewind

23
«но даже с функцией ниже ее лучше, чем добавлять ненужный пакет в вашу систему». Я категорически не согласен. Вы заново изобретаете колесо в 19 миллионов раз абсолютно без причины и рискуете ввести в процесс ошибки или уязвимости. По крайней мере, это пустая трата времени. Inb4 «что если они отбросят пакет»: в крайне маловероятном случае удаления пакета из реестра npm вы всегда можете заменить его своим собственным . Нет смысла перевязывать голову, пока ты ее не сломал.
— Demonblack

3
Теперь вы можете использовать recursiveопцию: stackoverflow.com/a/57866165/6269864

245

Удалить папку синхронно

const fs = require('fs');
const Path = require('path');

const deleteFolderRecursive = function(path) {
  if (fs.existsSync(path)) {
    fs.readdirSync(path).forEach((file, index) => {
      const curPath = Path.join(path, file);
      if (fs.lstatSync(curPath).isDirectory()) { // recurse
        deleteFolderRecursive(curPath);
      } else { // delete file
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(path);
  }
};

33
Возможно, вы захотите добавить некоторые проверки, которые вы не собираетесь случайно запускать на «/». Например, пропуск пустого пути и опечатка в файле может привести к тому, что curPath будет корневым каталогом.
— Jake_Howard

10
Более надежная реализация: заменить var curPath = path + "/" + file;с var curPath = p.join(path, file);условием , что вы включили модуль пути:var p = require("path")
— Andry

9
Windows имеет \ косые черты, так что path.join(dirpath, file)должно быть лучшеpath + "/" + file
— Thybzi

5
С этим кодом вы можете получить «Превышен максимальный размер стека вызовов» из-за слишком большого количества операций за один такт. @Walf, если вы запускаете консольное приложение, у вас 1 клиент, не более. Так что в этом случае не нужно использовать async для консольного приложения
— Леонид Дашко

4
Я получаю сообщение «Ошибка: ENOTEMPTY: директория не пуста»
— Seagull

168

Большинство людей, использующих fsNode.js, хотели бы, чтобы функции были близки к «Unix-способу» работы с файлами. Я использую fs-extra, чтобы принести все классные вещи:

fs-extra содержит методы, которые не включены в пакет vanilla Node.js fs. Такие как mkdir -p, cp -r и rm -rf.

Более того, fs-extra - капля на замену нативным fs. Все методы в fs не изменены и привязаны к нему. Это означает, что вы можете заменить fs на fs-extra :

// this can be replaced
const fs = require('fs')

// by this
const fs = require('fs-extra')

И тогда вы можете удалить папку следующим образом:

fs.removeSync('/tmp/myFolder'); 
//or
fs.remove('/tmp/myFolder', callback);

для removeSync('/tmp/myFolder')
— нужной

149

По состоянию на 2019 год ...

Начиная с Node.js 12.10.0 , fs.rmdirSyncподдерживает recursiveпараметры, так что вы можете, наконец, сделать:

fs.rmdirSync(dir, { recursive: true });

Где recursiveопция рекурсивно удаляет весь каталог.


5
@anneb Это происходит, если вы используете старую версию Node.js (<12.10). Последняя версия распознает эту опцию recursive: trueи удаляет непустые папки без жалоб.
— 0

9
Начиная с узла v13.0.1, рекурсивное удаление все еще является экспериментальным
— Тим

5
Функция подписи на самом деле fs.rmdir(path[, options], callback)илиfs.rmdirSync(path[, options])
— conceptdeluxe

@ Тим, что ты подразумеваешь под экспериментом?
— Эмерика

2
@Emerica В официальных документах node.js есть большое оранжевое уведомление, в котором говорится, что fs.rmdirоно экспериментально со стабильностью 1. «Стабильность: 1 - экспериментально. Эта функция не подпадает под правила семантического контроля версий. Изменения или удаление, несовместимые с предыдущими версиями, могут произойти в любом случае. будущий выпуск. Использование этой функции в производственных средах не рекомендуется ».
— Тим

24

Мой измененный ответ от @oconnecp ( https://stackoverflow.com/a/25069828/3027390 )

Использует path.join для лучшего кроссплатформенного опыта. Так что не забывайте требовать этого.

var path = require('path');

Также переименован в функцию rimraf;)

/**
 * Remove directory recursively
 * @param {string} dir_path
 * @see https://stackoverflow.com/a/42505874/3027390
 */
function rimraf(dir_path) {
    if (fs.existsSync(dir_path)) {
        fs.readdirSync(dir_path).forEach(function(entry) {
            var entry_path = path.join(dir_path, entry);
            if (fs.lstatSync(entry_path).isDirectory()) {
                rimraf(entry_path);
            } else {
                fs.unlinkSync(entry_path);
            }
        });
        fs.rmdirSync(dir_path);
    }
}

17

Я обычно не воскрешаю старые темы, но здесь есть много чего, и без ответа, все это кажется мне слишком сложным.

Во-первых, в современном Node (> = v8.0.0) вы можете упростить процесс, используя только основные модули узла, полностью асинхронные, и распараллеливать параллельное связывание файлов одновременно в функции из пяти строк и при этом сохранять читабельность:

const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const readdir = promisify(fs.readdir);
const rmdir = promisify(fs.rmdir);
const unlink = promisify(fs.unlink);

exports.rmdirs = async function rmdirs(dir) {
  let entries = await readdir(dir, { withFileTypes: true });
  await Promise.all(entries.map(entry => {
    let fullPath = path.join(dir, entry.name);
    return entry.isDirectory() ? rmdirs(fullPath) : unlink(fullPath);
  }));
  await rmdir(dir);
};

С другой стороны, защита для атак обхода пути не подходит для этой функции, потому что

  1. Это выходит за рамки, основанные на принципе единой ответственности .
  2. Должен быть обработан вызывающей стороной, а не этой функцией. Это похоже на командную строку rm -rfв том, что она принимает аргумент и позволяет пользователю, rm -rf /если будет предложено. Сценарий должен защищать не rmсаму программу.
  3. Эта функция не сможет определить такую ​​атаку, поскольку у нее нет системы отсчета. Опять же, это ответственность вызывающего абонента, который будет иметь контекст намерения, который предоставит ему ссылку для сравнения обхода пути.
  4. Sym-ссылка не является проблемой , как .isDirectory()это falseдля SYM-ссылок и НЕСВЯЗАННЫЕ не рекурсия в.

И последнее, но не менее важное: существует редкое условие состязания, при котором рекурсия может привести к ошибке, если одна из записей была отсоединена или удалена вне этого сценария как раз в нужное время во время выполнения этой рекурсии. Поскольку этот сценарий не является типичным в большинстве сред, его, вероятно, можно пропустить. Однако, если требуется (для некоторых крайних случаев), эта проблема может быть смягчена с помощью этого немного более сложного примера:

exports.rmdirs = async function rmdirs(dir) {
  let entries = await readdir(dir, { withFileTypes: true });
  let results = await Promise.all(entries.map(entry => {
    let fullPath = path.join(dir, entry.name);
    let task = entry.isDirectory() ? rmdirs(fullPath) : unlink(fullPath);
    return task.catch(error => ({ error }));
  }));
  results.forEach(result => {
    // Ignore missing files/directories; bail on other errors
    if (result && result.error.code !== 'ENOENT') throw result.error;
  });
  await rmdir(dir);
};

РЕДАКТИРОВАТЬ: сделать isDirectory()функцию. Удалите актуальный каталог в конце. Исправить пропущенную рекурсию.


1
Это действительно аккуратное решение. Вопрос относительно второго примера кода: вы не звоните awaitпо своему Promise.all(…); это намеренно? Кажется, что в его текущем состоянии results.forEachитерации по обещаниям, в то время как код ожидает итерации по результатам. Я что-то упускаю?
— Антон Строгонов

@ Тони, ты прав, это опечатка / ошибка. Хороший улов!
— Sukima

Может быть, проверка в первую очередь, чтобы убедиться, что каталог существует? что-то вродеif (!fs.existsSync(dir)) return
— GTPV

@GTPV Почему? Это увеличивает ответственность этой функции. readdirвыдаст ошибку как надо. Если у вас rmdir non-existing-dirкод выхода, это ошибка. Это будет обязанностью потребителя попробовать / поймать. Это тот же метод, который описан в документации по Node, когда дело доходит до использования функций fs. Они ожидают, что вы попробуете / поймаете и посмотрите на ошибки, codeчтобы определить, что делать. Дополнительная проверка вводит условия гонки.
— Sukima

Я определенно вижу вашу точку зрения. Хотя я бы интуитивно ожидал, что попытка удалить несуществующую папку будет успешной, поскольку она просто ничего не сделает. Нет условия гонки, если используется синхронная версия fs.exists. PS это отличное решение.
— GTPV

12

Вот асинхронная версия ответа @ SharpCoder

const fs = require('fs');
const path = require('path');

function deleteFile(dir, file) {
    return new Promise(function (resolve, reject) {
        var filePath = path.join(dir, file);
        fs.lstat(filePath, function (err, stats) {
            if (err) {
                return reject(err);
            }
            if (stats.isDirectory()) {
                resolve(deleteDirectory(filePath));
            } else {
                fs.unlink(filePath, function (err) {
                    if (err) {
                        return reject(err);
                    }
                    resolve();
                });
            }
        });
    });
};

function deleteDirectory(dir) {
    return new Promise(function (resolve, reject) {
        fs.access(dir, function (err) {
            if (err) {
                return reject(err);
            }
            fs.readdir(dir, function (err, files) {
                if (err) {
                    return reject(err);
                }
                Promise.all(files.map(function (file) {
                    return deleteFile(dir, file);
                })).then(function () {
                    fs.rmdir(dir, function (err) {
                        if (err) {
                            return reject(err);
                        }
                        resolve();
                    });
                }).catch(reject);
            });
        });
    });
};

10

Я написал эту функцию под названием удалить папку. Он рекурсивно удалит все файлы и папки в определенном месте. Единственный пакет, который требуется, это асинхронный.

var async = require('async');

function removeFolder(location, next) {
    fs.readdir(location, function (err, files) {
        async.each(files, function (file, cb) {
            file = location + '/' + file
            fs.stat(file, function (err, stat) {
                if (err) {
                    return cb(err);
                }
                if (stat.isDirectory()) {
                    removeFolder(file, cb);
                } else {
                    fs.unlink(file, function (err) {
                        if (err) {
                            return cb(err);
                        }
                        return cb();
                    })
                }
            })
        }, function (err) {
            if (err) return next(err)
            fs.rmdir(location, function (err) {
                return next(err)
            })
        })
    })
}

4
Идея в том, чтобы на самом деле не писать свой собственный код, если он уже был написан кем-то другим. Лучший способ сделать это - использовать rimraf, fs-extra или любой другой модуль узла, чтобы выполнить эту работу за вас.
— Виктор Пудеев

90
Да, писать свой собственный код ужасно, потому что использование десятков сторонних модулей для относительно тривиальных операций никогда не оказывало никаких недостатков в крупномасштабных приложениях.
— Эрик

8

Если вы используете узел 8+, хотите асинхронность и не хотите внешних зависимостей, вот версия async / await:

const path = require('path');
const fs = require('fs');
const util = require('util');

const readdir = util.promisify(fs.readdir);
const lstat = util.promisify(fs.lstat);
const unlink = util.promisify(fs.unlink);
const rmdir = util.promisify(fs.rmdir);

const removeDir = async (dir) => {
    try {
        const files = await readdir(dir);
        await Promise.all(files.map(async (file) => {
            try {
                const p = path.join(dir, file);
                const stat = await lstat(p);
                if (stat.isDirectory()) {
                    await removeDir(p);
                } else {
                    await unlink(p);
                    console.log(`Removed file ${p}`);
                }
            } catch (err) {
                console.error(err);
            }
        }))
        await rmdir(dir);
        console.log(`Removed dir ${dir}`);
    } catch (err) {
      console.error(err);
    }
}

4

Асинхронная версия ответа @ SharpCoder с использованием fs.promises:

const fs = require('fs');
const afs = fs.promises;

const deleteFolderRecursive = async path =>  {
    if (fs.existsSync(path)) {
        for (let entry of await afs.readdir(path)) {
            const curPath = path + "/" + entry;
            if ((await afs.lstat(curPath)).isDirectory())
                await deleteFolderRecursive(curPath);
            else await afs.unlink(curPath);
        }
        await afs.rmdir(path);
    }
};

3

Я достиг здесь, пытаясь покончить с этим, gulpи я пишу для дальнейших достижений.

Если вы хотите удалить файлы и папки, используя del, вы должны добавить /**для рекурсивного удаления.

gulp.task('clean', function () {
    return del(['some/path/to/delete/**']);
});

2

Пакет де-факто есть rimraf, но вот моя крошечная асинхронная версия:

const fs = require('fs')
const path = require('path')
const Q = require('q')

function rmdir (dir) {
  return Q.nfcall(fs.access, dir, fs.constants.W_OK)
    .then(() => {
      return Q.nfcall(fs.readdir, dir)
        .then(files => files.reduce((pre, f) => pre.then(() => {
          var sub = path.join(dir, f)
          return Q.nfcall(fs.lstat, sub).then(stat => {
            if (stat.isDirectory()) return rmdir(sub)
            return Q.nfcall(fs.unlink, sub)
          })
        }), Q()))
    })
    .then(() => Q.nfcall(fs.rmdir, dir))
}

2

В последней версии Node.js (12.10.0 или более поздняя версия), то rmdirфункция стиля fs.rmdir(), fs.rmdirSync()и fs.promises.rmdir()есть новый экспериментальный вариант , recursiveкоторый позволяет удалять непустые каталоги, например ,

fs.rmdir(path, { recursive: true });

Связанный PR на GitHub: https://github.com/nodejs/node/pull/29168


2

Согласно fsдокументации , в fsPromisesнастоящее время эта recursiveопция предоставляется на экспериментальной основе, которая, по крайней мере, в моем случае в Windows, удаляет каталог и все файлы в нем.

fsPromises.rmdir(path, {
  recursive: true
})

Удаляет ли recursive: trueфайлы на Linux и MacOS?


1

Сверхскоростной и безотказный

Вы можете использовать lignatorпакет ( https://www.npmjs.com/package/lignator ), он быстрее любого асинхронного кода (например, rimraf) и более надежен (особенно в Windows, где удаление файлов происходит не мгновенно и файлы могут быть заблокирован другими процессами).

4,36 ГБ данных, 28 042 файла, 4 217 папок в Windows были удалены за 15 секунд против 60 секунд на старом жестком диске.

const lignator = require('lignator');

lignator.remove('./build/');

1

Синхронизировать папку удалить с файлами или только с файлом.

Я не большой даритель и не вкладчик, но я не смог найти хорошее решение этой проблемы, и мне пришлось найти свой путь ... так что я надеюсь, что вам понравится :)

Идеально подходит для меня с любым количеством вложенные каталоги и подкаталоги. Предостережение для области действия this при повторном использовании функции может отличаться от реализации. В моем случае эта функция остается в возвращении другой функции, поэтому я вызываю ее с этим.

    const fs = require('fs');

    deleteFileOrDir(path, pathTemp = false){
            if (fs.existsSync(path)) {
                if (fs.lstatSync(path).isDirectory()) {
                    var files = fs.readdirSync(path);
                    if (!files.length) return fs.rmdirSync(path);
                    for (var file in files) {
                        var currentPath = path + "/" + files[file];
                        if (!fs.existsSync(currentPath)) continue;
                        if (fs.lstatSync(currentPath).isFile()) {
                            fs.unlinkSync(currentPath);
                            continue;
                        }
                        if (fs.lstatSync(currentPath).isDirectory() && !fs.readdirSync(currentPath).length) {
                            fs.rmdirSync(currentPath);
                        } else {
                            this.deleteFileOrDir(currentPath, path);
                        }
                    }
                    this.deleteFileOrDir(path);
                } else {
                    fs.unlinkSync(path);
                }
            }
            if (pathTemp) this.deleteFileOrDir(pathTemp);
        }

1

Пока recursiveэто экспериментальный вариантfs.rmdir

function rm (path, cb) {
    fs.stat(path, function (err, stats) {
        if (err)
            return cb(err);

        if (stats.isFile())
            return fs.unlink(path, cb);

        fs.rmdir(path, function (err) {
            if (!err || err && err.code != 'ENOTEMPTY') 
                return cb(err);

            fs.readdir(path, function (err, files) {
                if (err)
                    return cb(err);

                let next = i => i == files.length ? 
                    rm(path, cb) : 
                    rm(path + '/' + files[i], err => err ? cb(err) : next(i + 1));

                next(0);
            });
        });
    });
}

1

2020 Обновление

С версии 12.10.0 для опций добавлен recursiveOption .

Обратите внимание, что рекурсивное удаление является экспериментальным .

Так что вы бы сделали для синхронизации:

fs.rmdirSync(dir, {recursive: true});

или для асинхронного:

fs.rmdir(dir, {recursive: true});

0

Просто используйте модуль rmdir ! это легко и просто.


6
Не всегда хорошая идея использовать модуль для каждого небольшого фрагмента кода. Если вам нужно создать лицензионное соглашение, например, это создает реальную боль.
— Мияго

4
вам нужно добавить пример кода, чтобы ваш ответ был более интересным
— Xeltor

0

Другой альтернативой является использование fs-promiseмодуля, который предоставляет обещанные версии fs-extraмодулей

Вы могли бы тогда написать как этот пример:

const { remove, mkdirp, writeFile, readFile } = require('fs-promise')
const { join, dirname } = require('path')

async function createAndRemove() {
  const content = 'Hello World!'
  const root = join(__dirname, 'foo')
  const file = join(root, 'bar', 'baz', 'hello.txt')

  await mkdirp(dirname(file))
  await writeFile(file, content)
  console.log(await readFile(file, 'utf-8'))
  await remove(join(__dirname, 'foo'))
}

createAndRemove().catch(console.error)

примечание: async / await требует последней версии nodejs (7.6+)


0

Быстрый и грязный способ (возможно, для тестирования) может состоять в том, чтобы напрямую использовать метод execor spawnдля вызова вызова ОС для удаления каталога. Узнайте больше о NodeJs child_process .

let exec = require('child_process').exec
exec('rm -Rf /tmp/*.zip', callback)

Недостатками являются:

  1. Вы зависите от базовой ОС, то есть тот же метод будет работать в Unix / Linux, но, вероятно, не в Windows.
  2. Вы не можете угнать процесс на условиях или ошибках. Вы просто даете задачу базовой ОС и ждете возврата кода завершения.

Преимущества:

  1. Эти процессы могут выполняться асинхронно.
  2. Вы можете прослушивать вывод / ошибку команды, следовательно, вывод команды не теряется. Если операция не завершена, вы можете проверить код ошибки и повторить попытку.

2
Идеально подходит для случаев, когда вы пишете скрипт и не хотите устанавливать зависимости, потому что вы собираетесь удалить этот скрипт через 30 секунд после того, как вы удалили все свои файлы !!
— Матиас

Всегда есть способы испортить и удалить корневую файловую систему. В этом случае OP может убрать -fфлаг, чтобы быть безопасным, или удостовериться, печатая, что он / она не собирается удалять все. exec + rmявляется верной и полезной командой в узле, которую я часто использую во время тестирования.
— Сыпь

0

Хотелось бы, чтобы был способ сделать это без дополнительных модулей для чего-то крошечного и общего, но это лучшее, что я мог придумать.

Обновление: теперь должно работать в Windows (проверено Windows 10), а также должно работать в системах Linux / Unix / BSD / Mac.

const
    execSync = require("child_process").execSync,
    fs = require("fs"),
    os = require("os");

let removeDirCmd, theDir;

removeDirCmd = os.platform() === 'win32' ? "rmdir /s /q " : "rm -rf ";

theDir = __dirname + "/../web-ui/css/";

// WARNING: Do not specify a single file as the windows rmdir command will error.
if (fs.existsSync(theDir)) {
    console.log(' removing the ' + theDir + ' directory.');
    execSync(removeDirCmd + '"' + theDir + '"', function (err) {
        console.log(err);
    });
}

Может быть, fs-extra - это то, что нужно, если вам нужен один модуль.
— b01

3
Этот метод совершенно опасен. Строковая конкатенация команды оболочки, особенно без экранирования, предлагает уязвимости выполнения кода и тому подобное. Если вы собираетесь использовать rmdir, используйте child_process.execFileкоторого не вызывать оболочку, а вместо этого явно передавайте аргументы.
— Невин

@nevyn Я попробую и обновлю свой ответ, если он сработает.
— b01

Всегда предпочитайте не использовать третьих лиц! Спасибо!
— Антон Мицев

Кроме того, этот метод довольно медленный. Nodejs родной API гораздо быстрее.
— Мерси

0

Это один из подходов, использующий Promisify и две вспомогательные функции (to и toAll) для разрешения обещания.

Это делает все действия асинхронными.

const fs = require('fs');
const { promisify } = require('util');
const to = require('./to');
const toAll = require('./toAll');

const readDirAsync = promisify(fs.readdir);
const rmDirAsync = promisify(fs.rmdir);
const unlinkAsync = promisify(fs.unlink);

/**
    * @author Aécio Levy
    * @function removeDirWithFiles
    * @usage: remove dir with files
    * @param {String} path
    */
const removeDirWithFiles = async path => {
    try {
        const file = readDirAsync(path);
        const [error, files] = await to(file);
        if (error) {
            throw new Error(error)
        }
        const arrayUnlink = files.map((fileName) => {
            return unlinkAsync(`${path}/${fileName}`);
        });
        const [errorUnlink, filesUnlink] = await toAll(arrayUnlink);
        if (errorUnlink) {
            throw new Error(errorUnlink);
        }
        const deleteDir = rmDirAsync(path);
        const [errorDelete, result] = await to(deleteDir);
        if (errorDelete) {
            throw new Error(errorDelete);
        }
    } catch (err) {
        console.log(err)
    }
}; 

0

// без использования каких-либо сторонних библиотек

const fs = require('fs');
var FOLDER_PATH = "./dirname";
var files = fs.readdirSync(FOLDER_PATH);
files.forEach(element => {
    fs.unlinkSync(FOLDER_PATH + "/" + element);
});
fs.rmdirSync(FOLDER_PATH);

1
Это будет работать для того, что мне нужно, но вы можете использовать путь вместо конкатенации косой черты:fs.unllinkSync(path.join(FOLDER_PATH, element);
— jackofallcode

-1
const fs = require("fs")
const path = require("path")

let _dirloc = '<path_do_the_directory>'

if (fs.existsSync(_dirloc)) {
  fs.readdir(path, (err, files) => {
    if (!err) {
      for (let file of files) {
        // Delete each file
        fs.unlinkSync(path.join(_dirloc, file))
      }
    }
  })
  // After the 'done' of each file delete,
  // Delete the directory itself.
  if (fs.unlinkSync(_dirloc)) {
    console.log('Directory has been deleted!')
  }
}

1
Я думаю, что-то вроде этого должно работать для вложенных каталогов.
— fool4jesus

Да, как для вложенного каталога, так и для не вложенного
— Erisan Olasheni
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.