finishing up some of the mount parsing. it now successfully builds a dictionary map at least. docs updated and print formatter done.
This commit is contained in:
		
							parent
							
								
									58accf8c7e
								
							
						
					
					
						commit
						236af1ea37
					
				@ -58,6 +58,7 @@ One of either:footnote:optelem[]
 | 
				
			|||||||
one of either:
 | 
					one of either:
 | 
				
			||||||
.... `auth` (see <<Auth>> section below), or
 | 
					.... `auth` (see <<Auth>> section below), or
 | 
				
			||||||
.... `authGpg`, an <<Auth>> config snippet encrypted with GPG. See the section on <<GPG-Encrypted Elements>>.
 | 
					.... `authGpg`, an <<Auth>> config snippet encrypted with GPG. See the section on <<GPG-Encrypted Elements>>.
 | 
				
			||||||
 | 
					... An optional `mounts` container.footnote:optelem[] See the section on <<Mounts>>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Let's look at an example configuration.
 | 
					Let's look at an example configuration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -78,6 +79,11 @@ Let's look at an example configuration.
 | 
				
			|||||||
    <auth>
 | 
					    <auth>
 | 
				
			||||||
        <token/>
 | 
					        <token/>
 | 
				
			||||||
    </auth>
 | 
					    </auth>
 | 
				
			||||||
 | 
					    <mounts>
 | 
				
			||||||
 | 
					        <mount type="kv1">secret_legacy</mount>
 | 
				
			||||||
 | 
					        <mount type="kv2">secret</mount>
 | 
				
			||||||
 | 
					        <mount type="cubbyhole">cubbyhole</mount>
 | 
				
			||||||
 | 
					    </mounts>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</vaultpass>
 | 
					</vaultpass>
 | 
				
			||||||
----
 | 
					----
 | 
				
			||||||
@ -85,7 +91,7 @@ Let's look at an example configuration.
 | 
				
			|||||||
In the above, we can see that it would use the vault server at `http://localhost:8200/` using whatever token is either
 | 
					In the above, we can see that it would use the vault server at `http://localhost:8200/` using whatever token is either
 | 
				
			||||||
in the **`VAULT_TOKEN`** environment variable or, if empty, the `~/.vault-token` file. Because an unseal shard was
 | 
					in the **`VAULT_TOKEN`** environment variable or, if empty, the `~/.vault-token` file. Because an unseal shard was
 | 
				
			||||||
provided, it will be able to attempt to automatically unseal the Vault (assuming its shard will complete the threshold
 | 
					provided, it will be able to attempt to automatically unseal the Vault (assuming its shard will complete the threshold
 | 
				
			||||||
needed).
 | 
					needed). Because we specify mounts, we do not need permissions in Vault to list `/sys/mounts`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
=== Auth
 | 
					=== Auth
 | 
				
			||||||
Vault itself supports a https://www.vaultproject.io/docs/auth/[large number of authentication methods^]. However, in
 | 
					Vault itself supports a https://www.vaultproject.io/docs/auth/[large number of authentication methods^]. However, in
 | 
				
			||||||
@ -221,6 +227,25 @@ If not specified, the default is `userpass`.
 | 
				
			|||||||
<!-- SNIP -->
 | 
					<!-- SNIP -->
 | 
				
			||||||
----
 | 
					----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=== Mounts
 | 
				
			||||||
 | 
					VaultPass has the ability to automatically detect (some) mounts and their paths.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					So why, then, should you specify them in the configuration file? Simple: because you might not have permission to list
 | 
				
			||||||
 | 
					them! Even if you can see the mounts in the web UI that you have permission to, that **doesn't guarantee** that they're
 | 
				
			||||||
 | 
					accessible/viewable https://www.vaultproject.io/api-docs/[via the API^] (which is how VaultPass, and even the upstream
 | 
				
			||||||
 | 
					Vault binary client, operates). So by specifying them in the configuration file, you're able to "bootstrap" the process.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The optional `mounts` footnote:optelem[] container contains one or more `mount` child elements, with the name of the
 | 
				
			||||||
 | 
					mountpoint as the content.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each `mount` element has one optional attribute, `type` footnote:optelem[], which can be one of:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* https://www.vaultproject.io/docs/secrets/cubbyhole/[`cubbyhole`^]
 | 
				
			||||||
 | 
					* https://www.vaultproject.io/docs/secrets/kv/kv-v1/[`kv1`^]
 | 
				
			||||||
 | 
					* https://www.vaultproject.io/docs/secrets/kv/kv-v2/[`kv2`^] _(this is the default if not specified)_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					https://www.vaultproject.io/docs/secrets/[More mount types^] may be added upon popular demand and technical feasability.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
=== GPG-Encrypted Elements
 | 
					=== GPG-Encrypted Elements
 | 
				
			||||||
Understandably, in order to have a persistent configuration, that means storing on disk. That also means that they need
 | 
					Understandably, in order to have a persistent configuration, that means storing on disk. That also means that they need
 | 
				
			||||||
to be able to be accessed with no or minimal user interruption. Pass used GPG natively, so it didn't have an issue with
 | 
					to be able to be accessed with no or minimal user interruption. Pass used GPG natively, so it didn't have an issue with
 | 
				
			||||||
@ -257,7 +282,7 @@ Let's look at an example of GPG-encrypted elements.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
==== GPG-Encrypted Elements Example
 | 
					==== GPG-Encrypted Elements Example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.`~/.config/vaultpass.xml` snippet:
 | 
					.`~/.config/vaultpass.xml`:
 | 
				
			||||||
[source,xml]
 | 
					[source,xml]
 | 
				
			||||||
----
 | 
					----
 | 
				
			||||||
<?xml version="1.0" encoding="UTF-8" ?>
 | 
					<?xml version="1.0" encoding="UTF-8" ?>
 | 
				
			||||||
 | 
				
			|||||||
@ -14,8 +14,8 @@
 | 
				
			|||||||
         be able to iterate through available mounts if not. -->
 | 
					         be able to iterate through available mounts if not. -->
 | 
				
			||||||
    <!-- Default type if not specified is kv2. -->
 | 
					    <!-- Default type if not specified is kv2. -->
 | 
				
			||||||
    <mounts>
 | 
					    <mounts>
 | 
				
			||||||
        <mount type="kv">secrets_legacy</mount>
 | 
					        <mount type="kv1">secret_legacy</mount>
 | 
				
			||||||
        <mount type="kv2">secrets</mount>
 | 
					        <mount type="kv2">secret</mount>
 | 
				
			||||||
        <mount type="cubbyhole">cubbyhole</mount>
 | 
					        <mount type="cubbyhole">cubbyhole</mount>
 | 
				
			||||||
    </mounts>
 | 
					    </mounts>
 | 
				
			||||||
</vaultpass>
 | 
					</vaultpass>
 | 
				
			||||||
 | 
				
			|||||||
@ -41,7 +41,7 @@ class PassMan(object):
 | 
				
			|||||||
        return(None)
 | 
					        return(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _getMount(self):
 | 
					    def _getMount(self):
 | 
				
			||||||
        mounts_xml = self.xml.find('.//mounts')
 | 
					        mounts_xml = self.cfg.xml.find('.//mounts')
 | 
				
			||||||
        self.mount = mounts.MountHandler(self.client, mounts_xml = mounts_xml)
 | 
					        self.mount = mounts.MountHandler(self.client, mounts_xml = mounts_xml)
 | 
				
			||||||
        return(None)
 | 
					        return(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import logging
 | 
				
			|||||||
import re
 | 
					import re
 | 
				
			||||||
import warnings
 | 
					import warnings
 | 
				
			||||||
##
 | 
					##
 | 
				
			||||||
import dpath  # https://pypi.org/project/dpath/
 | 
					import dpath.util  # https://pypi.org/project/dpath/
 | 
				
			||||||
import hvac.exceptions
 | 
					import hvac.exceptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,10 +23,11 @@ class CubbyHandler(object):
 | 
				
			|||||||
        resp = self.client._adapter.list(url = uri)
 | 
					        resp = self.client._adapter.list(url = uri)
 | 
				
			||||||
        return(resp.json())
 | 
					        return(resp.json())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def read_secret(self, *args, **kwargs):
 | 
					    def read_secret(self, path, mount_point = 'cubbyhole'):
 | 
				
			||||||
        # https://github.com/hashicorp/vault/issues/8644
 | 
					        path = path.lstrip('/')
 | 
				
			||||||
        _logger.warning('Cannot get path info from a cubbyhole')
 | 
					        uri = '/v1/{0}/{1}'.format(mount_point, path)
 | 
				
			||||||
        return({'data': {}})
 | 
					        resp = self.client._adapter.get(url = uri)
 | 
				
			||||||
 | 
					        return(resp.json())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MountHandler(object):
 | 
					class MountHandler(object):
 | 
				
			||||||
@ -40,6 +41,105 @@ class MountHandler(object):
 | 
				
			|||||||
        self.paths = {}
 | 
					        self.paths = {}
 | 
				
			||||||
        self.getSysMounts()
 | 
					        self.getSysMounts()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getMountType(self, mount):
 | 
				
			||||||
 | 
					        if not self.mounts:
 | 
				
			||||||
 | 
					            self.getSysMounts()
 | 
				
			||||||
 | 
					        mtype = self.mounts.get(mount)
 | 
				
			||||||
 | 
					        if not mtype:
 | 
				
			||||||
 | 
					            _logger.error('Mount not found in defined mounts')
 | 
				
			||||||
 | 
					            _logger.debug('The mount {0} was not found in the defined mounts.'.format(mount))
 | 
				
			||||||
 | 
					            raise ValueError('Mount not found in defined mounts')
 | 
				
			||||||
 | 
					        return(mtype)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getSecret(self, path, mount, version = None):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getSecretNames(self, path, mount, version = None):
 | 
				
			||||||
 | 
					        reader = None
 | 
				
			||||||
 | 
					        mtype = self.getMountType(mount)
 | 
				
			||||||
 | 
					        secrets_list = []
 | 
				
			||||||
 | 
					        keypath = ['data']
 | 
				
			||||||
 | 
					        args = {'path': path,
 | 
				
			||||||
 | 
					                'mount_point': mount}
 | 
				
			||||||
 | 
					        if mtype == 'cubbyhole':
 | 
				
			||||||
 | 
					            reader = self.cubbyhandler.read_secret
 | 
				
			||||||
 | 
					        elif mtype == 'kv1':
 | 
				
			||||||
 | 
					            reader = self.client.secrets.kv.v1.read_secret
 | 
				
			||||||
 | 
					        elif mtype == 'kv2':
 | 
				
			||||||
 | 
					            if not any(((version is None), isinstance(version, int))):
 | 
				
			||||||
 | 
					                _logger.error('version parameter must be an integer or None')
 | 
				
			||||||
 | 
					                _logger.debug('The version parameter ({0}) must be an integer or None'.format(version))
 | 
				
			||||||
 | 
					                raise ValueError('version parameter must be an integer or None')
 | 
				
			||||||
 | 
					            reader = self.client.secrets.kv.v2.read_secret_version
 | 
				
			||||||
 | 
					            args['version'] = version
 | 
				
			||||||
 | 
					            keypath = ['data', 'data']
 | 
				
			||||||
 | 
					        data = reader(**args)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            # secrets_list = list(data.get('data', {}).keys())
 | 
				
			||||||
 | 
					            secrets_list = list(dpath.util.get(data, keypath, {}).keys())
 | 
				
			||||||
 | 
					        except (KeyError, TypeError):
 | 
				
			||||||
 | 
					            secrets_list = []
 | 
				
			||||||
 | 
					        return(secrets_list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getSecretsTree(self, path = '/', mounts = None, version = None):
 | 
				
			||||||
 | 
					        if not mounts:
 | 
				
			||||||
 | 
					            mounts = self.mounts
 | 
				
			||||||
 | 
					        if isinstance(mounts, dict):
 | 
				
			||||||
 | 
					            mounts = list(mounts.keys())
 | 
				
			||||||
 | 
					        if not isinstance(mounts, list):
 | 
				
			||||||
 | 
					            mounts = [mounts]
 | 
				
			||||||
 | 
					        for mount in mounts:
 | 
				
			||||||
 | 
					            mtype = self.getMountType(mount)
 | 
				
			||||||
 | 
					            handler = None
 | 
				
			||||||
 | 
					            args = {'path': path,
 | 
				
			||||||
 | 
					                    'mount_point': mount}
 | 
				
			||||||
 | 
					            relpath = path.replace('//', '/').lstrip('/')
 | 
				
			||||||
 | 
					            fullpath = '/'.join((mount, relpath)).replace('//', '/').lstrip('/')
 | 
				
			||||||
 | 
					            if mtype == 'cubbyhole':
 | 
				
			||||||
 | 
					                handler = self.cubbyhandler
 | 
				
			||||||
 | 
					            elif mtype == 'kv1':
 | 
				
			||||||
 | 
					                handler = self.client.secrets.kv.v1
 | 
				
			||||||
 | 
					            elif mtype == 'kv2':
 | 
				
			||||||
 | 
					                if not any(((version is None), isinstance(version, int))):
 | 
				
			||||||
 | 
					                    _logger.error('version parameter must be an integer or None')
 | 
				
			||||||
 | 
					                    _logger.debug('The version parameter ({0}) must be an integer or None'.format(version))
 | 
				
			||||||
 | 
					                    raise ValueError('version parameter must be an integer or None')
 | 
				
			||||||
 | 
					                handler = self.client.secrets.kv.v2
 | 
				
			||||||
 | 
					            if mount not in self.paths.keys():
 | 
				
			||||||
 | 
					                self.paths[mount] = {}
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                paths = handler.list_secrets(**args)
 | 
				
			||||||
 | 
					            except hvac.exceptions.InvalidPath:
 | 
				
			||||||
 | 
					                # It's a secret name.
 | 
				
			||||||
 | 
					                _logger.debug('Path {0} on mount {1} is a secret, not a subdir.'.format(path, mount))
 | 
				
			||||||
 | 
					                dpath.util.new(self.paths, fullpath, self.getSecretNames(path, mount, version = version))
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            # if 'data' not in paths.keys() or 'keys' not in paths['data'].keys():
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                paths_list = paths['data']['keys']
 | 
				
			||||||
 | 
					            except (KeyError, TypeError):
 | 
				
			||||||
 | 
					                _logger.warning('Mount has no secrets/subdirs')
 | 
				
			||||||
 | 
					                _logger.debug('The mount {0} has no secrets or subdirectories'.format(mount))
 | 
				
			||||||
 | 
					                warnings.warn('Mount has no secrets/subdirs')
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            for p in paths_list:
 | 
				
			||||||
 | 
					                p_relpath = '/'.join((relpath, p)).replace('//', '/').lstrip('/')
 | 
				
			||||||
 | 
					                p_fullpath = '/'.join((fullpath, p)).replace('//', '/').lstrip('/')
 | 
				
			||||||
 | 
					                _logger.debug(('Recursing getSecretsTree. '
 | 
				
			||||||
 | 
					                               'path={0} '
 | 
				
			||||||
 | 
					                               'fullpath={1} '
 | 
				
			||||||
 | 
					                               'relpath={2} '
 | 
				
			||||||
 | 
					                               'p={3} '
 | 
				
			||||||
 | 
					                               'p_relpath={4} '
 | 
				
			||||||
 | 
					                               'p_fullpath={5}').format(path,
 | 
				
			||||||
 | 
					                                                        fullpath,
 | 
				
			||||||
 | 
					                                                        relpath,
 | 
				
			||||||
 | 
					                                                        p,
 | 
				
			||||||
 | 
					                                                        p_relpath,
 | 
				
			||||||
 | 
					                                                        p_fullpath))
 | 
				
			||||||
 | 
					                self.getSecretsTree(path = p_relpath, mounts = mount)
 | 
				
			||||||
 | 
					        return(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def getSysMounts(self):
 | 
					    def getSysMounts(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            for mount, mount_info in self.client.sys.list_mounted_secrets_engines()['data'].items():
 | 
					            for mount, mount_info in self.client.sys.list_mounted_secrets_engines()['data'].items():
 | 
				
			||||||
@ -62,7 +162,7 @@ class MountHandler(object):
 | 
				
			|||||||
                _logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
 | 
					                _logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
 | 
				
			||||||
        except hvac.exceptions.Forbidden:
 | 
					        except hvac.exceptions.Forbidden:
 | 
				
			||||||
            _logger.warning('Client does not have permission to read /sys/mounts.')
 | 
					            _logger.warning('Client does not have permission to read /sys/mounts.')
 | 
				
			||||||
        # TODO: should I blindly merge in instead or no?
 | 
					        # TODO: should I blindly merge in instead?
 | 
				
			||||||
        if self.xml:
 | 
					        if self.xml:
 | 
				
			||||||
            for mount in self.xml.findall('.//mount'):
 | 
					            for mount in self.xml.findall('.//mount'):
 | 
				
			||||||
                mname = mount.text
 | 
					                mname = mount.text
 | 
				
			||||||
@ -72,56 +172,47 @@ class MountHandler(object):
 | 
				
			|||||||
                    _logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
 | 
					                    _logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
 | 
				
			||||||
        return(None)
 | 
					        return(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def getSecrets(self, path = '/', mounts = None):
 | 
					    def printer(self, output = None, indent = 4):
 | 
				
			||||||
        if not mounts:
 | 
					        def treePrint(obj, s = 'Password Store\n', level = 0):
 | 
				
			||||||
            mounts = self.mounts
 | 
					            prefix = '├──'
 | 
				
			||||||
        if isinstance(mounts, dict):
 | 
					            leading_prefix = '│'
 | 
				
			||||||
            mounts = list(mounts.keys())
 | 
					            last_prefix = '└──'
 | 
				
			||||||
        if not isinstance(mounts, list):
 | 
					            pass
 | 
				
			||||||
            mounts = [mounts]
 | 
					            return(s)
 | 
				
			||||||
        for mount in mounts:
 | 
					        if output:
 | 
				
			||||||
            mtype = self.mounts.get(mount)
 | 
					            output = output.lower()
 | 
				
			||||||
            if not mtype:
 | 
					        if output and output not in ('pretty', 'yaml', 'json'):
 | 
				
			||||||
                _logger.error('Mount not found in defined mounts')
 | 
					        # if output and output not in ('pretty', 'yaml', 'json', 'tree'):
 | 
				
			||||||
                _logger.debug('The mount {0} was not found in the defined mounts.'.format(mount))
 | 
					            _logger.error('Invalid output format')
 | 
				
			||||||
                raise ValueError('Mount not found in defined mounts')
 | 
					            _logger.debug('The output parameter ("{0}") must be one of: pretty, yaml, json, or None'.format(output))
 | 
				
			||||||
            handler = None
 | 
					            # _logger.debug(('The output parameter ("{0}") must be one of: '
 | 
				
			||||||
            if mtype == 'cubbyhole':
 | 
					            #                'pretty, yaml, json, tree, or None').format(output))
 | 
				
			||||||
                handler = self.cubbyhandler
 | 
					            raise ValueError('Invalid output format')
 | 
				
			||||||
            elif mtype == 'kv':
 | 
					        if output in ('pretty', 'yaml', 'json'):
 | 
				
			||||||
                handler = self.client.secrets.kv.v1
 | 
					            if not any(((indent is None), isinstance(indent, int))):
 | 
				
			||||||
            elif mtype == 'kv2':
 | 
					                _logger.error('indent parameter must be an integer or None')
 | 
				
			||||||
                handler = self.client.secrets.kv.v2
 | 
					                _logger.debug('The indent parameter ({0}) must be an integer or None'.format(indent))
 | 
				
			||||||
            if mount not in self.paths.keys():
 | 
					                raise ValueError('indent parameter must be an integer or None')
 | 
				
			||||||
                self.paths[mount] = {}
 | 
					        if not self.paths:
 | 
				
			||||||
            try:
 | 
					            self.getSecretsTree()
 | 
				
			||||||
                paths = handler.list_secrets(path = path, mount_point = mount)
 | 
					        if output == 'json':
 | 
				
			||||||
            except hvac.exceptions.InvalidPath:
 | 
					            import json
 | 
				
			||||||
                _logger.error('Path does not exist')
 | 
					            return(json.dumps(self.paths, indent = indent))
 | 
				
			||||||
                _logger.debug('Path {0} on mount {1} does not exist.'.format(path, mount))
 | 
					        elif output == 'yaml':
 | 
				
			||||||
                continue
 | 
					            import yaml  # https://pypi.org/project/PyYAML/
 | 
				
			||||||
            if 'data' not in paths.keys() or 'keys' not in paths['data'].keys():
 | 
					            # import pyaml  # https://pypi.python.org/pypi/pyaml
 | 
				
			||||||
                _logger.warning('Mount has no secrets/subdirs')
 | 
					            return(yaml.dump(self.paths, indent = indent))
 | 
				
			||||||
                _logger.debug('The mount {0} has no secrets or subdirectories'.format(mount))
 | 
					        elif output == 'pretty':
 | 
				
			||||||
                warnings.warn('Mount has no secrets/subdirs')
 | 
					            import pprint
 | 
				
			||||||
            for p2 in paths['data']['keys']:
 | 
					            if indent is None:
 | 
				
			||||||
                is_dir = False
 | 
					                indent = 1
 | 
				
			||||||
                fullpath = '/'.join((path, p2)).replace('//', '/')
 | 
					            return(pprint.pformat(self.paths, indent = indent))
 | 
				
			||||||
                if p2.endswith('/'):
 | 
					        # elif output == 'tree':
 | 
				
			||||||
                    r = _mount_re.search(fullpath)
 | 
					        #     # UNIX tree command output.
 | 
				
			||||||
                    fullpath = r.group('mount')
 | 
					        #     # has prefixes like ├──, │   ├──, └──, etc.
 | 
				
			||||||
                    is_dir = True
 | 
					        #     import tree
 | 
				
			||||||
                    self.paths[mount][fullpath] = None
 | 
					        elif not output:
 | 
				
			||||||
                    self.getSecrets(path = p2, mounts = mount)
 | 
					            return(str(self.paths))
 | 
				
			||||||
                sep_p2 = [i for i in fullpath.split('/') if i.strip() != '']
 | 
					 | 
				
			||||||
                if is_dir:
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
                # print(mount, sep_p2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def print(self):
 | 
					 | 
				
			||||||
        import pprint
 | 
					 | 
				
			||||||
        pprint.pprint(self.paths)
 | 
					 | 
				
			||||||
        return(None)
 | 
					        return(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def search(self):
 | 
					    def search(self):
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								vaultpass/tree.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								vaultpass/tree.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					# Thanks, dude: https://stackoverflow.com/a/49912639/733214
 | 
				
			||||||
 | 
					# TODO?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Tree(object):
 | 
				
			||||||
 | 
					    prefix_middle = '├──'
 | 
				
			||||||
 | 
					    prefix_last = '└──'
 | 
				
			||||||
 | 
					    spacer_middle = (' ' * 4)
 | 
				
			||||||
 | 
					    spacer_last = ('│' + (' ' * 3))
 | 
				
			||||||
 | 
					    parent_fmt = '\033[01;34m{0}/\033[00m'
 | 
				
			||||||
 | 
					    depth = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user