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

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);
  }
};
SharpCoder
источник
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);
    }
}
thybzi
источник
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()функцию. Удалите актуальный каталог в конце. Исправить пропущенную рекурсию.

Sukima
источник
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)
            })
        })
    })
}
oconnecp
источник
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);
    }
}
RonZ
источник
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);
    }
};
Ошибка 404
источник
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))
}
clarkttfu
источник
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

GOTO 0
источник
2

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

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

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

OldBoy
источник
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 ! это легко и просто.

Aminovski
источник
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);
    });
}
b01
источник
Может быть, 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