Add file/directory management

This commit is contained in:
Kivi Kaitaniemi
2024-04-25 23:02:17 +03:00
parent 0e22d22a69
commit 68b93a008c
11 changed files with 310 additions and 0 deletions
+166
View File
@@ -3,6 +3,153 @@ Module for writing system configurations for decman.
""" """
import typing import typing
import pwd
import grp
import shutil
import os
class File:
"""
A simple file that gets copied to the target.
"""
def __init__(
self,
source_file: typing.Optional[str] = None,
content: typing.Optional[str] = None,
bin_file: bool = False,
encoding: str = "utf-8",
owner: typing.Optional[str] = None,
group: typing.Optional[str] = None,
permissions: int = 0o644,
):
if source_file is None and content is None:
raise ValueError("Both source_file and content cannot be None.")
if source_file is not None and content is not None:
raise ValueError("Both source_file and content cannot be set.")
self.source_file = source_file
self.content = content
self.permissions = permissions
self.bin_file = bin_file
self.encoding = encoding
self.uid = None
self.gid = None
if owner is not None:
self.uid = pwd.getpwnam(owner).pw_uid
self.gid = pwd.getpwnam(owner).pw_gid
if group is not None:
self.gid = grp.getgrnam(group).gr_gid
def copy_to(self,
target: str,
variables: typing.Optional[dict[str, str]] = None):
"""
Copies the contents of this file to the target file.
"""
if variables is None:
variables = {}
target_directory = os.path.dirname(target)
os.makedirs(target_directory, exist_ok=True)
self._write_content(target, variables)
if self.uid is not None:
assert self.gid is not None, "If uid is set, then gid is set."
os.chown(target, self.uid, self.gid)
os.chmod(target, self.permissions)
def _write_content(self, target: str, variables: dict[str, str]):
if self.source_file is not None and (self.bin_file
or len(variables) == 0):
shutil.copy(self.source_file, target)
elif self.bin_file and self.content is not None:
with open(target, "wb") as file:
file.write(self.content.encode(encoding=self.encoding))
elif self.source_file is not None:
with open(self.source_file, "rt", encoding=self.encoding) as src:
content = src.read()
for var, value in variables.items():
content = content.replace(var, value)
with open(target, "wt", encoding=self.encoding) as file:
file.write(content)
else:
assert self.content is not None, "Content should be set since source_file was not set."
content = self.content
for var, value in variables.items():
content = content.replace(var, value)
with open(target, "wt", encoding=self.encoding) as file:
file.write(content)
class Directory:
"""
Contents of this directory will be copied to the target.
"""
def __init__(
self,
source_directory: str,
bin_files: bool = False,
encoding: str = "utf-8",
owner: typing.Optional[str] = None,
group: typing.Optional[str] = None,
permissions: int = 0o644,
):
self.source_directory = source_directory
self.bin_files = bin_files
self.encoding = encoding
self.permissions = permissions
self.owner = owner
self.group = group
self.uid = None
self.gid = None
if owner is not None:
self.uid = pwd.getpwnam(owner).pw_uid
self.gid = pwd.getpwnam(owner).pw_gid
if group is not None:
self.gid = grp.getgrnam(group).gr_gid
def copy_to(
self,
target_directory: str,
variables: typing.Optional[dict[str, str]] = None) -> list[str]:
"""
Copies the files in this directory to the target directory.
Returns all created files.
"""
created = []
original_wd = os.getcwd()
try:
os.chdir(self.source_directory)
for src_dir, _, files in os.walk("."):
for src_file in files:
src_path = os.path.join(src_dir, src_file)
file = File(source_file=src_path,
bin_file=self.bin_files,
encoding=self.encoding,
owner=self.owner,
group=self.group,
permissions=self.permissions)
target = os.path.join(target_directory, src_path)
created.append(target)
file.copy_to(target, variables)
finally:
os.chdir(original_wd)
return created
class UserPackage: class UserPackage:
@@ -76,6 +223,25 @@ class Module:
Override this method to run python code after the version of this module has changed. Override this method to run python code after the version of this module has changed.
""" """
def files(self) -> dict[str, File]:
"""
Override this method to return files that should be installed as a part of this module.
"""
return {}
def directories(self) -> dict[str, Directory]:
"""
Override this method to return directories that should be installed as a part of this module.
"""
return {}
def file_variables(self) -> dict[str, str]:
"""
Override this method to return variables that should replaced with a new value inside
this module's text files.
"""
return {}
def pacman_packages(self) -> list[str]: def pacman_packages(self) -> list[str]:
""" """
Override this method to return pacman packages that should be installed as a part of this Override this method to return pacman packages that should be installed as a part of this
+56
View File
@@ -134,6 +134,7 @@ class Store:
self.enabled_systemd_units: list[str] = [] self.enabled_systemd_units: list[str] = []
self.enabled_user_systemd_units: list[tuple[str, str]] = [] self.enabled_user_systemd_units: list[tuple[str, str]] = []
self.enabled_modules: dict[str, str] = {} self.enabled_modules: dict[str, str] = {}
self.created_files: list[str] = []
self.pkgbuild_latest_reviewed_commits: dict[str, str] = {} self.pkgbuild_latest_reviewed_commits: dict[str, str] = {}
self._package_file_cache: dict[str, tuple[str, str]] = {} self._package_file_cache: dict[str, tuple[str, str]] = {}
@@ -170,6 +171,7 @@ class Store:
"enabled_systemd_units": self.enabled_systemd_units, "enabled_systemd_units": self.enabled_systemd_units,
"enabled_user_systemd_units": self.enabled_user_systemd_units, "enabled_user_systemd_units": self.enabled_user_systemd_units,
"enabled_modules": self.enabled_modules, "enabled_modules": self.enabled_modules,
"created_files": self.created_files,
"package_file_cache": self._package_file_cache, "package_file_cache": self._package_file_cache,
"pkgbuild_git_commits": self.pkgbuild_latest_reviewed_commits "pkgbuild_git_commits": self.pkgbuild_latest_reviewed_commits
} }
@@ -208,6 +210,7 @@ class Store:
[], [],
) )
store.enabled_modules = d.get("enabled_modules", {}) store.enabled_modules = d.get("enabled_modules", {})
store.created_files = d.get("created_files", [])
store._package_file_cache = d.get("package_file_cache", {}) store._package_file_cache = d.get("package_file_cache", {})
store.pkgbuild_latest_reviewed_commits = d.get( store.pkgbuild_latest_reviewed_commits = d.get(
"pkgbuild_git_commits", "pkgbuild_git_commits",
@@ -243,6 +246,8 @@ class Source:
ignored_packages: list[str], ignored_packages: list[str],
systemd_units: list[str], systemd_units: list[str],
systemd_user_units: dict[str, list[str]], systemd_user_units: dict[str, list[str]],
files: dict[str, decman.File],
directories: dict[str, decman.Directory],
modules: list[decman.Module], modules: list[decman.Module],
): ):
self.pacman_packages = pacman_packages self.pacman_packages = pacman_packages
@@ -251,6 +256,8 @@ class Source:
self.ignored_packages = ignored_packages self.ignored_packages = ignored_packages
self.systemd_units = systemd_units self.systemd_units = systemd_units
self.systemd_user_units = systemd_user_units self.systemd_user_units = systemd_user_units
self.files = files
self.directories = directories
self.modules = modules self.modules = modules
def run_on_enable(self, store: Store): def run_on_enable(self, store: Store):
@@ -288,6 +295,55 @@ class Source:
elif module.enabled and module.name not in store.enabled_modules: elif module.enabled and module.name not in store.enabled_modules:
module.after_version_change() module.after_version_change()
def create_all_files(self) -> list[str]:
"""
Creates all files and returns them. The files created are based on the specified files,
directories and modules.
"""
created_files = []
def install_files(files: dict[str, decman.File],
variables: typing.Optional[dict[str, str]] = None):
for target, file in files.items():
created_files.append(target)
try:
file.copy_to(target, variables)
print_debug(f"Installing file to {target}.")
except OSError as e:
raise UserFacingError(
f"Failed to install file to {target}.") from e
def install_dirs(dirs: dict[str, decman.Directory],
variables: typing.Optional[dict[str, str]] = None):
for target, directory in dirs.items():
try:
print_debug(f"Installing directory to {target}.")
directory.copy_to(target, variables)
except OSError as e:
raise UserFacingError(
f"Failed to install directory to {target}.") from e
install_files(self.files)
install_dirs(self.directories)
for module in self.modules:
if module.enabled:
install_files(module.files(), module.file_variables())
install_dirs(module.directories(), module.file_variables())
return created_files
def files_to_remove(self, store: Store,
created_files: list[str]) -> list[str]:
"""
Returns all files that should be removed.
"""
to_remove = []
for path in store.created_files:
if path not in created_files:
to_remove.append(path)
return to_remove
def units_to_enable(self, store: Store) -> list[str]: def units_to_enable(self, store: Store) -> list[str]:
""" """
Returns all systemd units that should be enabled. Returns all systemd units that should be enabled.
+3
View File
@@ -0,0 +1,3 @@
Simple text file with a %variable%
twice: %another_variable%
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# This file should be executable.
echo "Hello, world!"
+3
View File
@@ -0,0 +1,3 @@
1
1
1
+3
View File
@@ -0,0 +1,3 @@
2
2
2
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+3
View File
@@ -0,0 +1,3 @@
s1
s1
s1
+3
View File
@@ -0,0 +1,3 @@
s2
s2
s2
+61
View File
@@ -0,0 +1,61 @@
import os
import shutil
import sys
# This test is manual. You'll have to verify the results manually.
# NOTE: Change this if you want to run this script.
user = "kk"
cd = os.path.dirname(os.path.abspath(__file__))
os.chdir(cd)
sys.path.append(os.path.join(cd, "../../src/."))
from decman import File, Directory
#if os.path.exists("/tmp/decman-files"):
# shutil.rmtree("/tmp/decman-files")
#os.makedirs("/tmp/decman-files")
f1 = File(source_file="src/f1.txt")
f1.copy_to(
"/tmp/decman-files/f1.txt",
variables={
"%variable%": "123",
"%another_variable%": "456"
},
)
f2 = File(source_file="src/f2.sh", permissions=0o744)
f2.copy_to("/tmp/decman-files/f2.sh", )
f3 = File(content="%variable% doesn't work here.", bin_file=True)
f3.copy_to(
"/tmp/decman-files/f3.txt",
variables={
"%variable%": "123",
},
)
f4 = File(content="%variable% works here.", bin_file=False, owner=user)
f4.copy_to(
"/tmp/decman-files/f4.txt",
variables={
"%variable%": "123",
},
)
f5 = File(content="%variable% works here.",
bin_file=False,
owner=user,
group="root")
f5.copy_to(
"/tmp/decman-files/f5.txt",
variables={
"%variable%": "123",
},
)
d = Directory("src/srcdir", bin_files=True)
d.copy_to("/tmp/decman-files/targetdir")
+9
View File
@@ -139,6 +139,8 @@ class TestSource(unittest.TestCase):
systemd_units=["1.service", "2.timer"], systemd_units=["1.service", "2.timer"],
systemd_user_units={"user": ["u1.service", "u2.timer"]}, systemd_user_units={"user": ["u1.service", "u2.timer"]},
modules=modules, modules=modules,
files={},
directories={},
) )
store = Store() store = Store()
@@ -151,6 +153,7 @@ class TestSource(unittest.TestCase):
"ExistingChanged": "1", "ExistingChanged": "1",
"Disabled": "1", "Disabled": "1",
} }
store.created_files = ["/test/file1", "/test/file2", "/test/file3"]
currently_installed_packages = [ currently_installed_packages = [
"p1", "p1",
@@ -170,6 +173,12 @@ class TestSource(unittest.TestCase):
self.store = store self.store = store
self.currently_installed_packages = currently_installed_packages self.currently_installed_packages = currently_installed_packages
def test_files_to_remove(self):
created_files = ["/test/file1", "/test/file4"]
self.assertCountEqual(
self.source.files_to_remove(self.store, created_files),
["/test/file2", "/test/file3"])
def test_after_update_executed(self): def test_after_update_executed(self):
self.source.run_after_update() self.source.run_after_update()