Видаліть каталог, який не порожній


299

У моєму додатку 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/isissue
— Morgan ARR Allen

35
Це те, що можна легко зробити за допомогою NodeJS Core libs. Навіщо встановлювати непідтримуваний сторонній пакет?
— SudoKid

4
@EmettSpeer Коли ви маєте на увазі під «зробити все легко»? Самостійно записати функцію, як deleteFolderRecursiveу наступній відповіді?
— Freewind

23
"але навіть з функцією нижче її краще, ніж додавати непотрібний пакет у вашу систему." Я категорично не згоден. Ви винаходите колесо в 19-мільйонний раз без абсолютно ніяких причин і ризикуєте впроваджувати помилки або вразливості безпеки в процесі. Принаймні, це марна трата часу. Inb4 "що робити, якщо вони скинуть пакет": вкрай малоймовірно, що пакет буде видалено з реєстру npm, ви завжди можете замінити його на свій власний . Немає сенсу перев’язувати голову, перш ніж розбити її.
— Demonblack

3
Тепер ви можете використовувати recursiveопцію: stackoverflow.com/a/57866165/6269864

243

Щоб синхронно видалити папку

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 є кореневим dir.
— Jake_Howard

9
Більш надійна реалізація: замініть var curPath = path + "/" + file;за var curPath = p.join(path, file);умови, що ви включили модуль шляху:var p = require("path")
— Андрій

9
У Windows є \ косі риски, тому path.join(dirpath, file)має бути краще, ніжpath + "/" + file
— thybzi

5
Ви можете отримати "Максимальний розмір стека викликів" із цим кодом через занадто багато операцій за один час галочки. @Walf, якщо ви запускаєте консольну програму, у вас є 1 клієнт, а не більше. Отже, не потрібно використовувати асинхронізацію для консольного додатка в цьому випадку
— Леонід Дашко,

4
Я отримую "Помилка: ENOTEMPTY: каталог не порожній"
— Чайка

168

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

fs-extra містить методи, які не входять у ванільний пакет Node.js fs. Такі як mkdir -p, cp -r і rm -rf.

Ще краще, що fs-extra - це падіння заміни на рідні. Усі методи у 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')
— olidem

148

Станом на 2019 рік ...

Станом на Node.js 12.10.0 , fs.rmdirSyncпідтримує recursiveваріанти, так що ви можете нарешті -то зробити:

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

Де recursiveпараметр видаляє весь каталог рекурсивно.


5
@anneb Це трапляється, якщо ви використовуєте старішу версію Node.js (<12.10). Остання версія розпізнає цю опцію recursive: trueта видаляє непорожні папки без нарікань.
— GOTO 0

9
Рекурсивне видалення досі експериментально станом на вузол v13.0.1
— Тім

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

@Тим, що ти маєш на увазі під експериментальним?
— Emerica

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. Сим-посилання не викликають занепокоєння, як .isDirectory()і falseдля сим-посилань, і вони не пов'язані, не повторюються.

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

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);
};

EDIT: Зробіть isDirectory()функцію. Видаліть фактичний каталог наприкінці. Виправити відсутні рекурсії.


1
Це дійсно акуратне рішення. Питання про другий прикладі коду: ви не викликаєте awaitна вашому Promise.all(…); це навмисно? Схоже, що в його нинішньому стані він results.forEachби повторював обіцянки, тоді як код очікує ітерації над результатами. Я щось пропускаю?
— Антон Строгонов

@Тоні ви праві, це помилка помилки. Гарний улов!
— Сукіма

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

@GTPV Чому? Це збільшує відповідальність цієї функції. readdirвидасть помилку як слід. Якщо у вас rmdir non-existing-dirвихідний код - це помилка. Спроба / виловлення повинна бути відповідальною за споживача. Це той самий метод, описаний у документах Node, коли мова йде про використання функцій fs. Вони очікують, що ви спробуєте / наздоганите і подивіться на помилки, codeщоб визначити, що робити. Додаткова перевірка вводить умову гонки.
— Сукіма

Я точно бачу вашу думку. Я хоч би інтуїтивно очікував, що спроба видалити папку, яка не існує, була б успішною, оскільки вона просто нічого не зробить. Немає умови перегонів, якщо використовується синхронна версія 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

Згідно з 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 секунд на старий жорсткий диск rimraf .

const lignator = require('lignator');

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

1

Синхронізуйте видалення папки з файлами або лише файлом.

Я не є великим дачником і не працюючим, але я не зміг знайти гарне рішення цієї проблеми, і я повинен був знайти свій шлях ... тож сподіваюся, вам це сподобається :)

Мені ідеально підходить для будь-якої кількості вкладені каталоги та підкаталоги. Застереження щодо обсягу "цього" під час повторної функції, ваша реалізація може бути різною. У моєму випадку ця функція перетворюється на повернення іншої функції, саме тому я називаю це саме цим.

    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 для опцій додано рекурсивну опцію.

Зауважте, що рекурсивне видалення експериментальне .

Отже, ви б зробили для синхронізації:

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)

Примітка: для асинхронізації / очікування потрібна остання версія nodejs (7.6+)


0

Швидкий і брудний спосіб (можливо, для тестування) може бути прямим використанням методу execабо 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 секунд після того, як ви видалили всі свої файли !!
— Матіас

Завжди є способи зіпсувати і видалити кореневу файлову систему. У цьому випадку ОП може зняти -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

Це один підхід, який використовує промотифікацію та дві функції допомоги (до і всім) для вирішення обіцянки.

Він робить усі дії асинхронними.

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);
— об'єднувати

-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
Я думаю, що щось подібне повинно працювати для вкладених каталогів.
— crazy4jesus

Так, і для вкладеного каталогу, і для вкладеного
— Ерісан Олашені
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.