49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
"""Named local selections for individual devices; no hardware identifiers."""
|
||
import json
|
||
import re
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
|
||
def path(repo, name):
|
||
if not re.fullmatch(r'[a-z0-9][a-z0-9_-]{0,47}', name):
|
||
raise ValueError('Use 1–48 lowercase letters, digits, underscores or hyphens for a device preset name.')
|
||
root = repo / 'local/devices'
|
||
if not root.resolve().is_relative_to(repo.resolve()):
|
||
raise RuntimeError('Device preset directory points outside the checkout.')
|
||
target = root / (name + '.json')
|
||
if target.is_symlink():
|
||
raise RuntimeError('Device preset is a symlink; preserving it.')
|
||
return target
|
||
|
||
|
||
def list_presets(repo, load):
|
||
root = repo / 'local/devices'
|
||
result = {}
|
||
for candidate in sorted(root.glob('*.json')):
|
||
try:
|
||
checked = path(repo, candidate.stem)
|
||
result[candidate.stem] = load(checked)
|
||
except (ValueError, OSError, RuntimeError):
|
||
# A malformed local file cannot block the normal installer.
|
||
continue
|
||
return result
|
||
|
||
|
||
def save(repo, name, selection, ask):
|
||
destination = path(repo, name)
|
||
if destination.exists() and ask('Replace saved device preset ' + name + '?', ['save', 'keep'], 'keep') != 'save':
|
||
print('Kept device preset: ' + name)
|
||
return
|
||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||
# Use a new inode and refuse links; saved selections stay user-readable only.
|
||
with tempfile.NamedTemporaryFile(mode='w', dir=destination.parent, prefix='.device-', delete=False) as stream:
|
||
temporary = Path(stream.name)
|
||
json.dump(dict(selection, name=name), stream, indent=2)
|
||
stream.write('\n')
|
||
try:
|
||
temporary.chmod(0o600)
|
||
temporary.replace(destination)
|
||
finally:
|
||
temporary.unlink(missing_ok=True)
|
||
print('Saved device preset: ' + name)
|