752 lines
18 KiB
JavaScript
752 lines
18 KiB
JavaScript
class FileTree
|
|
{
|
|
/**
|
|
* Create tree and add ./ folder
|
|
* @param {Object} _options
|
|
* @param {Boolean} _options.auto_wash - Should clean empty folder on removes
|
|
*/
|
|
constructor(_options = {})
|
|
{
|
|
// Options
|
|
this.autoWash = typeof _options.autoWash === 'undefined' ? false : _options.autoWash
|
|
this.filesCount = 0
|
|
this.foldersCount = 0
|
|
this.allCount = 0
|
|
|
|
// Set up
|
|
this.folders = []
|
|
this.addFolder('.', {})
|
|
}
|
|
|
|
/**
|
|
* Clean path
|
|
* @param {String} _path - Path to clean
|
|
* @return {String} Cleaned path
|
|
*/
|
|
cleanPath(_path = '')
|
|
{
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('cleanPath: _path should be a string')
|
|
return false
|
|
}
|
|
|
|
let path = _path
|
|
|
|
// Trim
|
|
path = path.trim()
|
|
|
|
// Repeating `/`
|
|
path = path.replace(/\/+/g, '/')
|
|
|
|
// Ending `/`
|
|
path = path.replace(/\/$/, '')
|
|
|
|
// Starting `/`
|
|
path = path.replace(/^\//, '')
|
|
|
|
// Missing starting `./`
|
|
if(path !== '.' && path.search('./') !== 0)
|
|
{
|
|
path = './' + path
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
/**
|
|
* Add a folder
|
|
* Can create nested folder in one path
|
|
* @param {String} _path - Path to the folder (start with `./`)
|
|
* @param {Object} _data - Properties to add to the folder
|
|
* @returns {Object} Created folder
|
|
*/
|
|
addFolder(_path = '', _data = {})
|
|
{
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('addFolder: _path should be a string')
|
|
return false
|
|
}
|
|
if(typeof _data !== 'object')
|
|
{
|
|
console.warn('addFolder: _data should be an object')
|
|
return false
|
|
}
|
|
|
|
// Set up
|
|
const path = this.cleanPath(_path)
|
|
const pathParts = path.split('/')
|
|
|
|
let folders = this.folders
|
|
let folder = null
|
|
|
|
// Each path part
|
|
for(const _part of pathParts)
|
|
{
|
|
const index = folders.findIndex((folder) => _part === folder.name)
|
|
|
|
// Already exist
|
|
if(index !== -1)
|
|
{
|
|
folder = folders[index]
|
|
folders = folder.folders
|
|
}
|
|
|
|
// Folder doesn't exist
|
|
else
|
|
{
|
|
// Create folder
|
|
folder = {
|
|
folders: [],
|
|
files: [],
|
|
name: _part
|
|
}
|
|
|
|
// Add data
|
|
for(const dataKey in _data)
|
|
{
|
|
const data = _data[dataKey]
|
|
folder[dataKey] = data
|
|
}
|
|
|
|
// Save
|
|
folders.push(folder)
|
|
folders = folder.folders
|
|
}
|
|
}
|
|
|
|
// Update counts
|
|
this.updateCounts()
|
|
|
|
// Return
|
|
return folder
|
|
}
|
|
|
|
/**
|
|
* Add a file
|
|
* Will create folders if needed
|
|
* @param {String} _path - Path to the folder (start with `./`)
|
|
* @param {Object} _data - Properties to add to the file
|
|
* @returns {Object} Created file
|
|
*/
|
|
addFile(_path = '', _data = {})
|
|
{
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('addFile: _path should be a string')
|
|
return false
|
|
}
|
|
if(typeof _data !== 'object')
|
|
{
|
|
console.warn('addFile: _data should be an object')
|
|
return false
|
|
}
|
|
|
|
// Set up
|
|
const path = this.cleanPath(_path)
|
|
const pathParts = path.split('/')
|
|
const filePart = pathParts.pop()
|
|
|
|
// Create folder
|
|
const folder = this.addFolder(pathParts.join('/'))
|
|
|
|
// Create file
|
|
const file = {
|
|
name: filePart
|
|
}
|
|
|
|
// Add data
|
|
for(const dataKey in _data)
|
|
{
|
|
const data = _data[dataKey]
|
|
file[dataKey] = data
|
|
}
|
|
|
|
// Save
|
|
folder.files.push(file)
|
|
|
|
// Update counts
|
|
this.updateCounts()
|
|
|
|
return file
|
|
}
|
|
|
|
/**
|
|
* Remove folder
|
|
* Will delete contained folders and contained files
|
|
* @param {String} _path - Folder to delete (start with `./`)
|
|
* @returns {Boolean} Folder deleted or not
|
|
*/
|
|
removeFolder(_path = '')
|
|
{
|
|
// Recursive emptying
|
|
const emptyFolder = function(folder)
|
|
{
|
|
// Delete folders
|
|
for(const _folderKey in folder.folders)
|
|
{
|
|
const _folder = folder.folders[_folderKey]
|
|
|
|
emptyFolder(_folder)
|
|
|
|
folder.folders.splice(_folderKey, 1)
|
|
|
|
// Callback
|
|
if(typeof _folder.onRemove === 'function')
|
|
{
|
|
_folder.onRemove.apply(this, [_folder])
|
|
}
|
|
}
|
|
|
|
// Delete files
|
|
for(const _fileKey in folder.files)
|
|
{
|
|
const _file = folder.files[_fileKey]
|
|
|
|
folder.files.splice(_fileKey, 1)
|
|
|
|
// Callback
|
|
if(typeof _file.onRemove === 'function')
|
|
{
|
|
_file.onRemove.apply(this, [_file])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('removeFolder: _path should be a string')
|
|
return false
|
|
}
|
|
|
|
// Set up
|
|
const path = this.cleanPath(_path)
|
|
const pathParts = path.split('/')
|
|
const folderPart = pathParts.pop()
|
|
|
|
let folders = this.folders
|
|
let folder = null
|
|
|
|
// Each path part
|
|
for(const _part of pathParts)
|
|
{
|
|
const index = folders.findIndex((folder) => _part === folder.name)
|
|
|
|
// Found
|
|
if(index !== -1)
|
|
{
|
|
folder = folders[index]
|
|
folders = folder.folders
|
|
}
|
|
|
|
// Not found
|
|
else
|
|
{
|
|
folder = null
|
|
folders = null
|
|
break
|
|
}
|
|
}
|
|
|
|
// Found
|
|
const index = folders.findIndex((folder) => folderPart === folder.name)
|
|
if(folders && index !== -1)
|
|
{
|
|
const folder = folders[index]
|
|
|
|
// Delete
|
|
emptyFolder(folder)
|
|
folders.splice(index, 1)
|
|
|
|
// Callback
|
|
if(typeof folder.onRemove === 'function')
|
|
{
|
|
folder.onRemove.apply(this, [folder])
|
|
}
|
|
|
|
// Auto wash
|
|
if(this.autoWash)
|
|
{
|
|
this.removeEmptyFolders()
|
|
}
|
|
|
|
// Update counts
|
|
this.updateCounts()
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Remove file
|
|
* Will delete contained folders and contained files
|
|
* @param {String} _path - File to delete (start with `./`)
|
|
* @returns {Boolean} File deleted or not
|
|
*/
|
|
removeFile(_path = '')
|
|
{
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('removeFile: _path should be a string')
|
|
return false
|
|
}
|
|
|
|
// Set up
|
|
const path = this.cleanPath(_path)
|
|
const pathParts = path.split('/')
|
|
const filePart = pathParts.pop()
|
|
|
|
let folders = this.folders
|
|
let folder = null
|
|
|
|
// Each path part
|
|
for(const _part of pathParts)
|
|
{
|
|
const index = folders.findIndex((folder) => _part === folder.name)
|
|
|
|
// Found
|
|
if(index !== -1)
|
|
{
|
|
folder = folders[index]
|
|
folders = folder.folders
|
|
}
|
|
|
|
// Not found
|
|
else
|
|
{
|
|
folder = null
|
|
folders = null
|
|
break
|
|
}
|
|
}
|
|
|
|
// Folder found
|
|
if(folders && folder)
|
|
{
|
|
const index = folder.files.findIndex((file) => filePart === file.name)
|
|
|
|
// File found
|
|
if(index !== -1)
|
|
{
|
|
const file = folder.files[index]
|
|
|
|
// Delete
|
|
folder.files.splice(index, 1)
|
|
|
|
// Auto wash
|
|
if(this.autoWash)
|
|
{
|
|
this.removeEmptyFolders()
|
|
}
|
|
|
|
// Callback
|
|
if(typeof file.onRemove === 'function')
|
|
{
|
|
file.onRemove.apply(this, [file])
|
|
}
|
|
|
|
// Update counts
|
|
this.updateCounts()
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get file
|
|
* @param {String} _path - Path to file
|
|
* @returns {Object} File
|
|
*/
|
|
getFile(_path = '')
|
|
{
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('getFile: _path should be a string')
|
|
return false
|
|
}
|
|
|
|
// Set up
|
|
const path = this.cleanPath(_path)
|
|
const pathParts = path.split('/')
|
|
const filePart = pathParts.pop()
|
|
|
|
let folders = this.folders
|
|
let folder = null
|
|
|
|
// Each path part
|
|
for(const _part of pathParts)
|
|
{
|
|
const index = folders.findIndex((folder) => _part === folder.name)
|
|
|
|
// Found
|
|
if(index !== -1)
|
|
{
|
|
folder = folders[index]
|
|
folders = folder.folders
|
|
}
|
|
|
|
// Not found
|
|
else
|
|
{
|
|
folder = null
|
|
folders = null
|
|
break
|
|
}
|
|
}
|
|
|
|
// Folder found
|
|
if(folders && folder)
|
|
{
|
|
const index = folder.files.findIndex((file) => filePart === file.name)
|
|
|
|
if(index !== -1)
|
|
{
|
|
const file = folder.files[index]
|
|
return file
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get folder
|
|
* @param {String} _path - Path to folder
|
|
* @returns {Object} Folder
|
|
*/
|
|
getFolder(_path = '')
|
|
{
|
|
// Errors
|
|
if(typeof _path !== 'string')
|
|
{
|
|
console.warn('getFolder: _path should be a string')
|
|
return false
|
|
}
|
|
|
|
// Set up
|
|
const path = this.cleanPath(_path)
|
|
const pathParts = path.split('/')
|
|
|
|
let folders = this.folders
|
|
let folder = null
|
|
|
|
// Each path part
|
|
for(const _part of pathParts)
|
|
{
|
|
const index = folders.findIndex((folder) => _part === folder.name)
|
|
|
|
// Found
|
|
if(index !== -1)
|
|
{
|
|
folder = folders[index]
|
|
folders = folder.folders
|
|
}
|
|
|
|
// Folder doesn't exist
|
|
else
|
|
{
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Return
|
|
return folder
|
|
}
|
|
|
|
/**
|
|
* Browse every folders and remove empty ones
|
|
* @return {Number} Number of removed folders
|
|
*/
|
|
removeEmptyFolders()
|
|
{
|
|
// Set up
|
|
let removedCount = 0
|
|
|
|
// Recursive remove
|
|
const canRemoveFolder = function(folder)
|
|
{
|
|
// Each folder inside current folder
|
|
for(const _folderKey in folder.folders)
|
|
{
|
|
// Try to remove folder
|
|
const _folder = folder.folders[_folderKey]
|
|
const canRemove = canRemoveFolder(_folder)
|
|
|
|
// Remove folder
|
|
if(canRemove)
|
|
{
|
|
removedCount++
|
|
|
|
// Delete
|
|
folder.folders.splice(_folderKey, 1)
|
|
|
|
// Callback
|
|
if(typeof _folder.onRemove === 'function')
|
|
{
|
|
_folder.onRemove.apply(this, [_folder])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Can be removed
|
|
const folderKeys = Object.keys(folder.folders)
|
|
const filesKeys = Object.keys(folder.files)
|
|
|
|
if(folderKeys.length === 0 && filesKeys.length === 0)
|
|
{
|
|
return true
|
|
}
|
|
else
|
|
{
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Try from ./
|
|
canRemoveFolder(this.folders[0])
|
|
|
|
// Update counts
|
|
this.updateCounts()
|
|
|
|
return removedCount
|
|
}
|
|
|
|
/**
|
|
* Update counts
|
|
*/
|
|
updateCounts()
|
|
{
|
|
let filesCount = 0
|
|
let foldersCount = 0
|
|
|
|
// Recursive emptying
|
|
const traverseFolder = function(folder)
|
|
{
|
|
// Each folder
|
|
for(const _folderKey in folder.folders)
|
|
{
|
|
const _folder = folder.folders[_folderKey]
|
|
|
|
traverseFolder(_folder)
|
|
|
|
foldersCount++
|
|
}
|
|
|
|
// Each file
|
|
for(const _fileKey in folder.files)
|
|
{
|
|
filesCount++
|
|
}
|
|
}
|
|
|
|
traverseFolder(this.folders[0])
|
|
|
|
this.filesCount = filesCount
|
|
this.foldersCount = foldersCount
|
|
this.allCount = this.filesCount + this.foldersCount
|
|
}
|
|
|
|
/**
|
|
* Describe the tree in ASCII (├ ─ │ └)
|
|
* @param {Boolean} _log - Directly log to console
|
|
* @param {Boolean} _colored - Colored tree (only work well in Chrome)
|
|
* @return {String} Tree
|
|
*/
|
|
describe(_log = false, _colored = false)
|
|
{
|
|
// Set up
|
|
const depth = 0
|
|
const styles = []
|
|
|
|
let stringTree = ''
|
|
|
|
const addToString = function(value = '', type = null)
|
|
{
|
|
if(_colored)
|
|
{
|
|
stringTree += '%c'
|
|
|
|
switch(type)
|
|
{
|
|
case 'structure':
|
|
styles.push('color:#999;')
|
|
break
|
|
|
|
case 'folder':
|
|
styles.push('color:#999;')
|
|
break
|
|
case 'file':
|
|
styles.push('color:#333;font-weight:bold;')
|
|
break
|
|
|
|
default:
|
|
styles.push('')
|
|
break
|
|
}
|
|
}
|
|
|
|
stringTree += value
|
|
}
|
|
|
|
// Recursive describe
|
|
const describeFolder = function(folder, depth, last = [])
|
|
{
|
|
// Each folders
|
|
for(let i = 0; i < folder.folders.length; i++)
|
|
{
|
|
// Set up
|
|
const _folder = folder.folders[i]
|
|
|
|
// Add to tree string
|
|
addToString('\n')
|
|
|
|
for(let j = 0; j < depth; j++)
|
|
{
|
|
if(j === depth - 1)
|
|
{
|
|
if(i === folder.folders.length - 1 && folder.files.length === 0)
|
|
{
|
|
addToString(' └', 'structure')
|
|
}
|
|
else
|
|
{
|
|
addToString(' ├', 'structure')
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(last[j])
|
|
{
|
|
addToString(' ', 'structure')
|
|
}
|
|
else
|
|
{
|
|
addToString(' │', 'structure')
|
|
}
|
|
}
|
|
}
|
|
|
|
addToString('─', 'structure')
|
|
addToString(_folder.name, 'folder')
|
|
addToString('/', 'structure')
|
|
|
|
// Last
|
|
last.push(i === folder.folders.length - 1 && folder.files.length === 0)
|
|
|
|
// Continue
|
|
describeFolder(_folder, depth + 1, last)
|
|
}
|
|
|
|
// Each files
|
|
for(let i = 0; i < folder.files.length; i++)
|
|
{
|
|
// Set up
|
|
const _file = folder.files[i]
|
|
|
|
// Add to tree string
|
|
stringTree += '\n'
|
|
|
|
for(let j = 0; j < depth; j++)
|
|
{
|
|
if(j === depth - 1)
|
|
{
|
|
if(i === folder.files.length - 1)
|
|
{
|
|
addToString(' └', 'structure')
|
|
}
|
|
else
|
|
{
|
|
addToString(' ├', 'structure')
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(last[j])
|
|
{
|
|
addToString(' ', 'structure')
|
|
}
|
|
else
|
|
{
|
|
addToString(' │', 'structure')
|
|
}
|
|
}
|
|
}
|
|
|
|
addToString('─', 'structure')
|
|
addToString(_file.name, 'file')
|
|
}
|
|
}
|
|
|
|
// Describe from ./
|
|
addToString('.', 'folder')
|
|
addToString('/', 'structure')
|
|
describeFolder(this.folders[0], depth + 1)
|
|
|
|
// Log
|
|
if(_log)
|
|
{
|
|
console.log(...[stringTree].concat(styles))
|
|
}
|
|
|
|
return stringTree
|
|
}
|
|
}
|
|
|
|
|
|
// /**
|
|
// * Tests
|
|
// */
|
|
// const filesTree = new FileTree()
|
|
|
|
// filesTree.addFolder('./hey/hoy', { active: false, notifs: 0 })
|
|
// filesTree.addFolder('./hey/hoy/toto', { active: false, notifs: 0 })
|
|
// filesTree.addFolder('./hey/hoy/tata', { active: false, notifs: 0 })
|
|
|
|
// filesTree.addFile('./test-1.txt', { active: false, notifs: 0 })
|
|
// filesTree.addFile('./hey/hoy/test-2.txt', { active: false, notifs: 0 })
|
|
// filesTree.addFile('./hey/hoy/test-3.txt', { active: false, notifs: 0 })
|
|
// filesTree.addFile('./hey/hoy/tata/test-4.txt', { active: false, notifs: 0 })
|
|
// filesTree.addFile('./hey/hoy/toto/test-5.txt', { active: false, notifs: 0 })
|
|
|
|
// filesTree.addFile('./hey/hoy/toto/test-6.txt', {
|
|
// active: false,
|
|
// notifs: 0,
|
|
// onRemove: (file) =>
|
|
// {
|
|
// console.log('removed file :', file)
|
|
// }
|
|
// })
|
|
// filesTree.removeFile('./hey/hoy/toto/test-6.txt')
|
|
|
|
// filesTree.addFolder('./pwet', {
|
|
// onRemove: (folder) =>
|
|
// {
|
|
// console.log('removed folder :', folder)
|
|
// }
|
|
// })
|
|
// filesTree.addFolder('./pwet/uh', {
|
|
// onRemove: (folder) =>
|
|
// {
|
|
// console.log('removed folder :', folder)
|
|
// }
|
|
// })
|
|
// filesTree.removeFolder('./pwet')
|
|
|
|
// filesTree.removeFile('./hey/hoy/test-2.txt')
|
|
// filesTree.removeFolder('./hey')
|
|
|
|
// filesTree.describe(true, true)
|
|
|
|
export default FileTree |