1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-05 13:13:49 +00:00

add chmod preserve for #368

This commit is contained in:
deadc0de6
2022-12-07 15:46:31 +01:00
parent 3b38d45c39
commit 9d7f2381ed
6 changed files with 155 additions and 58 deletions

View File

@@ -8,7 +8,7 @@ Entry | Description
`src` | Dotfile path within the `dotpath` (dotfiles with empty `src` are ignored and considered installed, can use `variables`, make sure to quote)
`link` | Defines how this dotfile is installed. Possible values: *nolink*, *absolute*, *relative*, *link_children* (See [Symlinking dotfiles](config-file.md#symlinking-dotfiles)) (defaults to value of `link_dotfile_default`)
`actions` | List of action keys that need to be defined in the **actions** entry below (See [actions](config-actions.md))
`chmod` | Defines the file permissions in octal notation to apply during installation (See [permissions](config-file.md#permissions))
`chmod` | Defines the file permissions in octal notation to apply during installation or the special keyword `preserve` (See [permissions](config-file.md#permissions))
`cmpignore` | List of patterns to ignore when comparing (enclose in quotes when using wildcards; see [ignore patterns](config-file.md#ignore-patterns))
`ignore_missing_in_dotdrop` | Ignore missing files in dotdrop when comparing and importing (see [Ignore missing](config-file.md#ignore-missing))
`ignoreempty` | If true, an empty template will not be deployed (defaults to the value of `ignoreempty`)

View File

@@ -90,8 +90,20 @@ dotfiles:
src: dir
dst: ~/dir
chmod: 744
f_preserve:
src: preserve
dst: ~/preserve
chmod: preserve
```
The `chmod` value defines the file permissions in octal notation to apply on dotfiles. If undefined
new files will get the system default permissions (see `umask`, `777-<umask>` for directories and
`666-<umask>` for files).
The special keyword `preserve` allows to ensure that if the dotfiles already exists
on the filesystem, it is not altered during `install` and the `chmod` value won't
be changed during `update`.
On `import`, the following rules are applied:
* If the `-m`/`--preserve-mode` switch is provided or the config option
@@ -107,12 +119,13 @@ On `install`, the following rules are applied:
* Otherwise, the permissions of the dotfile in the `dotpath` are applied.
* If the global setting `force_chmod` is set to true, dotdrop will not ask
for confirmation to apply permissions.
* If `chmod` is `preserve` and the destination exists with a different permission set
than system default, then it is not altered
On `update`, the following rule is applied:
* If the permissions of the file in the filesystem differ from the dotfile in the `dotpath`,
then the dotfile entry `chmod` is added/updated accordingly.
then the dotfile entry `chmod` is added/updated accordingly (unless `chmod` value is `preserve`)
## Symlinking dotfiles

View File

@@ -67,6 +67,9 @@ class CfgYaml:
key_dotfile_template = 'template'
key_dotfile_chmod = 'chmod'
# chmod value
chmod_ignore = 'preserve'
# profile
key_profile_dotfiles = 'dotfiles'
key_profile_include = 'include'
@@ -378,14 +381,17 @@ class CfgYaml:
"""return all existing dotfile keys"""
return self.dotfiles.keys()
def update_dotfile(self, key, chmod):
"""update an existing dotfile"""
if key not in self.dotfiles.keys():
return False
dotfile = self._yaml_dict[self.key_dotfiles][key]
def _update_dotfile_chmod(self, key, dotfile, chmod):
old = None
if self.key_dotfile_chmod in dotfile:
old = dotfile[self.key_dotfile_chmod]
if old == self.chmod_ignore:
msg = (
'ignore chmod change since '
f'{self.chmod_ignore}'
)
self._dbg(msg)
return False
if old == chmod:
return False
if self._debug:
@@ -397,6 +403,18 @@ class CfgYaml:
del dotfile[self.key_dotfile_chmod]
else:
dotfile[self.key_dotfile_chmod] = str(format(chmod, 'o'))
return True
def update_dotfile(self, key, chmod):
"""
update an existing dotfile
return true if updated
"""
if key not in self.dotfiles.keys():
return False
dotfile = self._yaml_dict[self.key_dotfiles][key]
if not self._update_dotfile_chmod(key, dotfile, chmod):
return False
self._dirty = True
return True
@@ -743,62 +761,77 @@ class CfgYaml:
new[k] = val
return new
def _norm_dotfile_chmod(self, entry):
value = str(entry[self.key_dotfile_chmod])
if value == self.chmod_ignore:
# is preserve
return
if len(value) < 3:
# bad format
err = f'bad format for chmod: {value}'
self._log.err(err)
raise YamlException(f'config content error: {err}')
# check is valid value
try:
int(value)
except Exception as exc:
err = f'bad format for chmod: {value}'
self._log.err(err)
err = f'config content error: {err}'
raise YamlException(err) from exc
# normalize chmod value
for chmodv in list(value):
chmodint = int(chmodv)
if chmodint < 0 or chmodint > 7:
err = f'bad format for chmod: {value}'
self._log.err(err)
raise YamlException(
f'config content error: {err}'
)
# octal
entry[self.key_dotfile_chmod] = int(value, 8)
def _norm_dotfiles(self, dotfiles):
"""normalize and check dotfiles entries"""
if not dotfiles:
return dotfiles
new = {}
for k, val in dotfiles.items():
# add 'src' as key' if not present
if self.key_dotfile_src not in val:
# add 'src' as key' if not present
val[self.key_dotfile_src] = k
new[k] = val
else:
new[k] = val
# fix deprecated trans key
if self.old_key_trans_r in val:
msg = '\"trans\" is deprecated, please use \"trans_read\"'
# fix deprecated trans key
msg = f'{k} \"trans\" is deprecated, please use \"trans_read\"'
self._log.warn(msg)
val[self.key_trans_r] = val[self.old_key_trans_r]
del val[self.old_key_trans_r]
new[k] = val
if self.key_dotfile_link not in val:
# apply link value if undefined
value = self.settings[self.key_settings_link_dotfile_default]
val[self.key_dotfile_link] = value
# apply noempty if undefined
if self.key_dotfile_noempty not in val:
# apply noempty if undefined
value = self.settings.get(self.key_settings_noempty, False)
val[self.key_dotfile_noempty] = value
# apply template if undefined
if self.key_dotfile_template not in val:
# apply template if undefined
value = self.settings.get(self.key_settings_template, True)
val[self.key_dotfile_template] = value
# validate value of chmod if defined
if self.key_dotfile_chmod in val:
value = str(val[self.key_dotfile_chmod])
if len(value) < 3:
err = f'bad format for chmod: {value}'
self._log.err(err)
raise YamlException(f'config content error: {err}')
try:
int(value)
except Exception as exc:
err = f'bad format for chmod: {value}'
self._log.err(err)
err = f'config content error: {err}'
raise YamlException(err) from exc
# normalize chmod value
for chmodv in list(value):
chmodint = int(chmodv)
if chmodint < 0 or chmodint > 7:
err = f'bad format for chmod: {value}'
self._log.err(err)
raise YamlException(
f'config content error: {err}'
)
val[self.key_dotfile_chmod] = int(value, 8)
if self.key_dotfile_chmod in val:
# validate value of chmod if defined
self._norm_dotfile_chmod(val)
return new
def _add_variables(self, new, shell=False, template=True, prio=False):

View File

@@ -117,7 +117,10 @@ class Dotfile(DictParser):
msg += f', link:\"{self.link}\"'
msg += f', template:{self.template}'
if self.chmod:
msg += f', chmod:{self.chmod:o}'
if isinstance(self.chmod, int) or len(self.chmod) == 3:
msg += f', chmod:{self.chmod:o}'
else:
msg += f', chmod:\"{self.chmod}\"'
return msg
def prt(self):
@@ -129,7 +132,10 @@ class Dotfile(DictParser):
out += f'\n{indent}link: \"{self.link}\"'
out += f'\n{indent}template: \"{self.template}\"'
if self.chmod:
out += f'\n{indent}chmod: \"{self.chmod:o}\"'
if isinstance(self.chmod, int) or len(self.chmod) == 3:
out += f'\n{indent}chmod: \"{self.chmod:o}\"'
else:
out += f'\n{indent}chmod: \"{self.chmod}\"'
out += f'\n{indent}pre-action:'
some = self.get_pre_actions()

View File

@@ -14,6 +14,7 @@ from dotdrop.logger import Logger
from dotdrop.linktypes import LinkTypes
from dotdrop import utils
from dotdrop.exceptions import UndefinedException
from dotdrop.cfg_yaml import CfgYaml
class Installer:
@@ -138,13 +139,15 @@ class Installer:
ret, err = self._link_absolute(templater, src, dst,
actionexec=actionexec,
is_template=is_template,
ignore=ignore)
ignore=ignore,
chmod=chmod)
elif linktype == LinkTypes.RELATIVE:
# symlink
ret, err = self._link_relative(templater, src, dst,
actionexec=actionexec,
is_template=is_template,
ignore=ignore)
ignore=ignore,
chmod=chmod)
elif linktype == LinkTypes.LINK_CHILDREN:
# symlink direct children
if not isdir:
@@ -158,7 +161,16 @@ class Installer:
is_template=is_template,
ignore=ignore)
self.log.dbg(f'before chmod: {ret} err:{err}')
if self.log.debug and chmod:
cur = utils.get_file_perm(dst)
if chmod == CfgYaml.chmod_ignore:
chmodstr = CfgYaml.chmod_ignore
else:
chmodstr = f'{chmod:o}'
self.log.dbg(
f'before chmod (cur:{cur:o}, new:{chmodstr}): '
f'installed:{ret} err:{err}'
)
if self.dry:
return self._log_install(ret, err)
@@ -169,9 +181,15 @@ class Installer:
# but not when
# - error (not r, err)
# - aborted (not r, err)
if os.path.exists(dst) and (ret or (not ret and not err)):
# - special keyword "preserve"
apply_chmod = linktype in [LinkTypes.NOLINK, LinkTypes.LINK_CHILDREN]
apply_chmod = apply_chmod and os.path.exists(dst)
apply_chmod = apply_chmod and (ret or (not ret and not err))
apply_chmod = apply_chmod and chmod != CfgYaml.chmod_ignore
if apply_chmod:
if not chmod:
chmod = utils.get_file_perm(src)
self.log.dbg(f'applying chmod {chmod:o} to {dst}')
dstperms = utils.get_file_perm(dst)
if dstperms != chmod:
# apply mode
@@ -187,6 +205,8 @@ class Installer:
else:
ret = False
err = 'chmod failed'
else:
self.log.dbg('no chmod applied')
return self._log_install(ret, err)
@@ -255,7 +275,8 @@ class Installer:
def _link_absolute(self, templater, src, dst,
actionexec=None,
is_template=True,
ignore=None):
ignore=None,
chmod=None):
"""
install link:absolute|link
@@ -269,12 +290,14 @@ class Installer:
actionexec=actionexec,
is_template=is_template,
ignore=ignore,
absolute=True)
absolute=True,
chmod=chmod)
def _link_relative(self, templater, src, dst,
actionexec=None,
is_template=True,
ignore=None):
ignore=None,
chmod=None):
"""
install link:relative
@@ -288,13 +311,18 @@ class Installer:
actionexec=actionexec,
is_template=is_template,
ignore=ignore,
absolute=False)
absolute=False,
chmod=chmod)
def _link_dotfile(self, templater, src, dst, actionexec=None,
is_template=True, ignore=None, absolute=True):
is_template=True, ignore=None, absolute=True,
chmod=None):
"""
symlink
chmod is only used if the dotfile is a template
and needs to be installed to the workdir first
return
- True, None : success
- False, error_msg : error
@@ -302,15 +330,15 @@ class Installer:
- False, 'aborted' : user aborted
"""
if is_template:
self.log.dbg('is a template')
self.log.dbg(f'install to {self.workdir}')
self.log.dbg(f'is a template, installing to {self.workdir}')
tmp = utils.pivot_path(dst, self.workdir,
striphome=True, logger=self.log)
ret, err = self.install(templater, src, tmp,
LinkTypes.NOLINK,
actionexec=actionexec,
is_template=is_template,
ignore=ignore)
ignore=ignore,
chmod=chmod)
if not ret and not os.path.exists(tmp):
return ret, err
src = tmp
@@ -467,6 +495,10 @@ class Installer:
dstrel = os.path.dirname(dstrel)
lnk_src = os.path.relpath(src, dstrel)
os.symlink(lnk_src, dst)
self.log.dbg(
f'symlink {dst} to {lnk_src} '
f'(mode:{utils.get_file_perm(dst):o})'
)
if not self.comparing:
self.log.sub(f'linked {dst} to {lnk_src}')
return True, None
@@ -527,7 +559,10 @@ class Installer:
ret, err = self._write(src, dst,
content=content,
actionexec=actionexec)
if ret and not err:
rights = f'{utils.get_file_perm(src):o}'
self.log.dbg(f'installed file {src} to {dst} ({rights})')
if not self.dry and not self.comparing:
self.log.sub(f'install {src} to {dst}')
return ret, err
@@ -587,13 +622,12 @@ class Installer:
@classmethod
def _write_content_to_file(cls, content, src, dst):
"""write content to file"""
if content:
# write content the file
try:
with open(dst, 'wb') as file:
file.write(content)
shutil.copymode(src, dst)
# shutil.copymode(src, dst)
except NotADirectoryError as exc:
err = f'opening dest file: {exc}'
return False, err
@@ -605,7 +639,7 @@ class Installer:
# copy file
try:
shutil.copyfile(src, dst)
shutil.copymode(src, dst)
# shutil.copymode(src, dst)
except OSError as exc:
return False, str(exc)
return True, None
@@ -665,7 +699,7 @@ class Installer:
if not ret:
return False, err
self.log.dbg(f'install file to \"{dst}\"')
self.log.dbg(f'installing file to \"{dst}\"')
# re-check in case action created the file
if self.safe and not overwrite and \
os.path.lexists(dst) and \
@@ -674,7 +708,10 @@ class Installer:
return False, 'aborted'
# writing to file
return self._write_content_to_file(content, src, dst)
self.log.dbg(f'before writing to {dst} ({utils.get_file_perm(src):o})')
ret = self._write_content_to_file(content, src, dst)
self.log.dbg(f'written to {dst} ({utils.get_file_perm(src):o})')
return ret
########################################################
# helpers
@@ -749,7 +786,13 @@ class Installer:
return
dst = path.rstrip(os.sep) + self.backup_suffix
self.log.log(f'backup {path} to {dst}')
os.rename(path, dst)
# os.rename(path, dst)
# copy to preserve mode on chmod=preserve
# since we expect dotfiles this shouldn't have
# such a big impact but who knows.
shutil.copy2(path, dst)
stat = os.stat(path)
os.chown(dst, stat.st_uid, stat.st_gid)
def _exec_pre_actions(self, actionexec):
"""execute action executor"""

View File

@@ -439,7 +439,9 @@ def get_default_file_perms(path, umask):
def get_file_perm(path):
"""return file permission"""
return os.stat(path).st_mode & 0o777
if not os.path.exists(path):
return 0o777
return os.stat(path, follow_symlinks=True).st_mode & 0o777
def chmod(path, mode, debug=False):