Move exporter command-line...
Marco Ricci authored 2 months ago
|
1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4)
5) """Command-line interface for derivepassphrase_export."""
6)
7) from __future__ import annotations
8)
9) import base64
10) import importlib
11) import json
12) import logging
13) from typing import TYPE_CHECKING, Any, Literal
14)
15) import click
16) from typing_extensions import assert_never
17)
18) import derivepassphrase as dpp
19) from derivepassphrase import _types, exporter
20)
21) if TYPE_CHECKING:
22) import os
23) import types
24) from collections.abc import Sequence
25)
26) __author__ = dpp.__author__
27) __version__ = dpp.__version__
28)
29) __all__ = ('derivepassphrase_export',)
30)
31) PROG_NAME = 'derivepassphrase_export'
32)
33)
|
Test exporter data loading...
Marco Ricci authored 2 months ago
|
34) def _load_data(
35) fmt: Literal['v0.2', 'v0.3', 'storeroom'],
36) path: str | bytes | os.PathLike[str],
37) key: bytes,
38) ) -> Any:
39) contents: bytes
40) module: types.ModuleType
41) match fmt:
42) case 'v0.2':
43) module = importlib.import_module(
44) 'derivepassphrase.exporter.vault_v03_and_below'
45) )
46) if module.STUBBED:
47) raise ModuleNotFoundError
48) with open(path, 'rb') as infile:
49) contents = base64.standard_b64decode(infile.read())
50) return module.V02Reader(contents, key).run()
51) case 'v0.3':
52) module = importlib.import_module(
53) 'derivepassphrase.exporter.vault_v03_and_below'
54) )
55) if module.STUBBED:
56) raise ModuleNotFoundError
57) with open(path, 'rb') as infile:
58) contents = base64.standard_b64decode(infile.read())
59) return module.V03Reader(contents, key).run()
60) case 'storeroom':
61) module = importlib.import_module(
62) 'derivepassphrase.exporter.storeroom'
63) )
64) if module.STUBBED:
65) raise ModuleNotFoundError
66) return module.export_storeroom_data(path, key)
67) case _: # pragma: no cover
68) assert_never(fmt)
69)
70)
|
Move exporter command-line...
Marco Ricci authored 2 months ago
|
71) @click.command(
72) context_settings={'help_option_names': ['-h', '--help']},
73) )
74) @click.option(
75) '-f',
76) '--format',
77) 'formats',
78) metavar='FMT',
79) multiple=True,
80) default=('v0.3', 'v0.2', 'storeroom'),
81) type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
82) help='try the following storage formats, in order (default: v0.3, v0.2)',
83) )
84) @click.option(
85) '-k',
86) '--key',
87) metavar='K',
88) help=(
89) 'use K as the storage master key '
90) '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
91) '`USERNAME` environment variables)'
92) ),
93) )
94) @click.argument('path', metavar='PATH', required=True)
95) @click.pass_context
96) def derivepassphrase_export(
97) ctx: click.Context,
98) /,
99) *,
100) path: str | bytes | os.PathLike[str],
101) formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
102) key: str | bytes | None = None,
103) ) -> None:
104) """Export a vault-native configuration to standard output.
105)
106) Read the vault-native configuration at PATH, extract all information
107) from it, and export the resulting configuration to standard output.
108) Depending on the configuration format, this may either be a file or
109) a directory.
110)
111) If PATH is explicitly given as `VAULT_PATH`, then use the
112) `VAULT_PATH` environment variable to determine the correct path.
113) (Use `./VAULT_PATH` or similar to indicate a file/directory actually
114) named `VAULT_PATH`.)
115)
116) """
117)
118) logging.basicConfig()
119) if path in {'VAULT_PATH', b'VAULT_PATH'}:
120) path = exporter.get_vault_path()
121) if key is None:
122) key = exporter.get_vault_key()
123) elif isinstance(key, str): # pragma: no branch
124) key = key.encode('utf-8')
125) for fmt in formats:
126) try:
|
Move exporter command-line...
Marco Ricci authored 2 months ago
|
128) except (
129) IsADirectoryError,
130) NotADirectoryError,
131) ValueError,
132) RuntimeError,
133) ):
134) logging.info('Cannot load as %s: %s', fmt, path)
135) continue
136) except OSError as exc:
137) click.echo(
138) (
139) f'{PROG_NAME}: ERROR: Cannot parse {path!r} as '
140) f'a valid config: {exc.strerror}: {exc.filename!r}'
141) ),
142) err=True,
143) )
144) ctx.exit(1)
145) except ModuleNotFoundError:
146) # TODO(the-13th-letter): Use backslash continuation.
147) # https://github.com/nedbat/coveragepy/issues/1836
148) msg = f"""
149) {PROG_NAME}: ERROR: Cannot load the required Python module "cryptography".
150) {PROG_NAME}: INFO: pip users: see the "export" extra.
151) """.lstrip('\n')
152) click.echo(msg, nl=False, err=True)
153) ctx.exit(1)
154) else:
155) if not _types.is_vault_config(config):
156) click.echo(
157) f'{PROG_NAME}: ERROR: Invalid vault config: {config!r}',
158) err=True,
159) )
160) ctx.exit(1)
161) click.echo(json.dumps(config, indent=2, sort_keys=True))
162) break
163) else:
164) click.echo(
165) f'{PROG_NAME}: ERROR: Cannot parse {path!r} as a valid config.',
166) err=True,
167) )
168) ctx.exit(1)
|