Разбить массив путей к файлам на иерархический объект в JavaScript

Использование JSZip, который при распаковке файла дает мне список папок и файлов. Например, когда я бегу

files.forEach((relativePath, file) => {
  console.log(relativePath);
});

Я получил:

three-dxf-master/
three-dxf-master/.DS_Store
three-dxf-master/.gitignore
three-dxf-master/LICENSE
three-dxf-master/README.md
three-dxf-master/bower.json
three-dxf-master/bower_components/

Некоторые из этих элементов являются каталогами, а некоторые - файлами. Я могу сказать, какие из них являются каталогами, проверив file.dir, Я хотел бы разделить это на иерархическую структуру данных. Я хочу разделить это так:

{
  "three-dxf-master": [
    ".DS_Store",
    ".gitignore",
    "LICENSE",
    "README.md",
    "bower.json",
    {
      "bower_components": [
        ".DS_Store",
        {
          "dxf-parser": [...]
        }
      ]
    }
  ]
}

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

3 ответа

Решение

Вот пример кода, который также обрабатывает файлы в корне.

См. Объяснение кода ниже фрагмента.

var paths = [
    "three-dxf-master/",
    "three-dxf-master/.DS_Store",
    "three-dxf-master/.gitignore",
    "three-dxf-master/LICENSE",
    "three-dxf-master/README.md",
    "three-dxf-master/bower.json",
    "three-dxf-master/bower_components/",
    "three-dxf-master/bower_components/.DS_Store",
    "three-dxf-master/bower_components/dxf-parser/",
    "three-dxf-master/bower_components/dxf-parser/foo",
    "three-dxf-master/bower_components/dxf-parser/bar",
    "three-dxf-master/dummy_folder/",
    "three-dxf-master/dummy_folder/foo",
    "three-dxf-master/dummy_folder/hello/",
    "three-dxf-master/dummy_folder/hello/hello",
]

// Extract a filename from a path
function getFilename(path) {
    return path.split("/").filter(function(value) {
        return value && value.length;
    }).reverse()[0];
}

// Find sub paths
function findSubPaths(path) {
    // slashes need to be escaped when part of a regexp
    var rePath = path.replace("/", "\\/");
    var re = new RegExp("^" + rePath + "[^\\/]*\\/?$");
    return paths.filter(function(i) {
        return i !== path && re.test(i);
    });
}

// Build tree recursively
function buildTree(path) {
    path = path || "";
    var nodeList = [];
    findSubPaths(path).forEach(function(subPath) {
        var nodeName = getFilename(subPath);
        if (/\/$/.test(subPath)) {
            var node = {};
            node[nodeName] = buildTree(subPath);
            nodeList.push(node);
        } else {
            nodeList.push(nodeName);
        }
    });
    return nodeList;
}

// Build tree from root
var tree = buildTree();

// By default, tree is an array
// If it contains only one element which is an object, 
// return this object instead to match OP request
if (tree.length == 1 && (typeof tree[0] === 'object')) {
    tree = tree[0];
}

// Serialize tree for debug purposes
console.log(JSON.stringify(tree, null, 2));

объяснение

function getFilename(path) {
    return path.split("/").filter(function(value) {
        return value && value.length;
    } ).reverse()
    [0];
}

Чтобы получить имя файла, путь разделяется на /,

/ путь / к / каталог / => ['path', 'to', 'dir', '']

/ путь / к / файлу => ['path', 'to', 'file']

Сохраняются только значения с длиной, этот дескриптор dir path.

Имя файла является последним значением нашего массива, чтобы получить его, мы просто обращаем массив и получаем первый элемент.

function findSubPaths(path) {
    // slashes need to be escaped when part of a regexp
    var rePath = path.replace("/", "\\/");
    var re = new RegExp("^" + rePath + "[^\\/]*\\/?$");
    return paths.filter(function(i) {
        return i !== path && re.test(i);
    });
}

Чтобы найти подпуть пути, мы используем фильтр по списку путей.

Фильтр использует регулярное выражение (демоверсия доступна здесь), чтобы проверить, начинается ли путь с родительского пути и заканчивается либо / (это путь к каталогу) или конец строки (это путь к файлу).

Если проверенный путь не равен родительскому пути и не соответствует регулярному выражению, он принимается фильтром. В противном случае это отклонено.

function buildTree(path) {
    path = path || "";
    var nodeList = [];
    findSubPaths(path).forEach(function(subPath) {
        var nodeName = getFilename(subPath);
        if(/\/$/.test(subPath)) {
            var node = {};
            node[nodeName] = buildTree(subPath);
            nodeList.push(node);            
        }
        else {
            nodeList.push(nodeName);
        }   
    });
    return nodeList;
}

Теперь, когда у нас есть методы для извлечения имени файла из пути и для поиска подпутей, очень легко построить наше дерево. Дерево - это список узлов.

Если суб-путь заканчивается / тогда это реж, и мы называем buildTree рекурсивно перед добавлением узла в nodeList.

В противном случае мы просто добавляем имя файла в nodeList.

Дополнительный код

if (tree.length == 1 && (typeof tree[0] === 'object')) {
    tree = tree[0];
}

По умолчанию возвращаемое дерево является массивом.

Чтобы соответствовать OP-запросу, если он содержит только один элемент, который является объектом, мы возвращаем этот объект.

Информация

Искал реализацию после того, как попробовал все решения на этой странице, у каждого были ошибки.

Наконец я нашел это

Решение

Вам нужно будет добавить "/" к выводу путей jszip, чтобы использовать алгоритм, вы можете использовать цикл forEach.

var paths = [
    '/FolderA/FolderB/FolderC/Item1',
    '/FolderA/FolderB/Item1',
    '/FolderB/FolderD/FolderE/Item1',
    '/FolderB/FolderD/FolderE/Item2',
    '/FolderA/FolderF/Item1',
    '/ItemInRoot'
];

function arrangeIntoTree(paths, cb) {
    var tree = [];

    // This example uses the underscore.js library.
    _.each(paths, function(path) {

        var pathParts = path.split('/');
        pathParts.shift(); // Remove first blank element from the parts array.

        var currentLevel = tree; // initialize currentLevel to root

        _.each(pathParts, function(part) {

            // check to see if the path already exists.
            var existingPath = _.findWhere(currentLevel, {
                name: part
            });

            if (existingPath) {
                // The path to this item was already in the tree, so don't add it again.
                // Set the current level to this path's children
                currentLevel = existingPath.children;
            } else {
                var newPart = {
                    name: part,
                    children: [],
                }

                currentLevel.push(newPart);
                currentLevel = newPart.children;
            }
        });
    });

    cb(tree);
}

arrangeIntoTree(paths, function(tree) {
    console.log('tree: ', tree);
});

Мне также нужно было отобразить данные в интерактивном дереве, я использовал https://github.com/wix/angular-tree-control, который принимает точный формат.

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

Если это файл, просто нажмите в текущем каталоге.

Формат в OP не допускает наличие файлов в корневом каталоге, поэтому в следующем случае выдается сообщение об ошибке. Чтобы разрешить файлы в корне, базовый объект должен быть массивом (но, похоже, это объект).

Следующее также позволяет путям быть в любом порядке и создаваться не последовательно, например, он будет принимать:

foobar/fum

это не нужно:

foobar/
foobar/fum

Надеюсь, комментариев достаточно.

var data = 'three-dxf-master/' +
           '\nfoobar/fumm' +
           '\nthree-dxf-master/.DS_Store' +
           '\nthree-dxf-master/.gitignore' +
           '\nthree-dxf-master/LICENSE' +
           '\nthree-dxf-master/README.md' +
           '\nthree-dxf-master/bower.json' +
           '\nthree-dxf-master/bower_components/' +
           '\nthree-dxf-master/bower_components/.DS_Store' +
           '\nthree-dxf-master/bower_components/dxf-parser/';

function parseData(data) {
  var records = data.split(/\n/);
  var result = records.reduce(function(acc, record) {
    var fields = record.match(/[^\/]+\/?/g) || [];
    var currentDir = acc;
       
    fields.forEach(function (field, idx) {

      // If field is a directory...
      if (/\/$/.test(field)) {
        
        // If first one and not an existing directory, add it
        if (idx == 0) {
          if (!(field in currentDir)) {
            currentDir[field] = [];
          }
          
          // Move into subdirectory
          currentDir = currentDir[field];
          
        // If not first, see if it's a subdirectory of currentDir
        } else {
          // Look for field as a subdirectory of currentDir
          var subDir = currentDir.filter(function(element){
            return typeof element == 'object' && element[field];
          })[0];
          
          // If didn't find subDir, add it and set as currentDir
          if (!subDir) {
            var t = Object.create(null);
            t[field] = [];
            currentDir.push(t);
            currentDir = t[field];
            
          // If found, set as currentDir
          } else {
            currentDir = subDir[field];
          }
        }
        
      // Otherwise it's a file. Make sure currentDir is a directory and not the root
      } else {
        if (Array.isArray(currentDir)) {
          currentDir.push(field);
          
        // Otherwise, must be at root where files aren't allowed
        } else {
          throw new Error('Files not allowed in root: ' + field);
        }
      }
    });
    
    return acc;
    
  }, Object.create(null));
  return result;
}

//console.log(JSON.stringify(parseData(data)));
console.log(parseData(data));

Другие вопросы по тегам