charmlibs-pathops 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- charmlibs/pathops/__init__.py +54 -0
- charmlibs/pathops/_constants.py +31 -0
- charmlibs/pathops/_container_path.py +758 -0
- charmlibs/pathops/_errors.py +120 -0
- charmlibs/pathops/_fileinfo.py +97 -0
- charmlibs/pathops/_functions.py +104 -0
- charmlibs/pathops/_local_path.py +224 -0
- charmlibs/pathops/_types.py +721 -0
- charmlibs/pathops/_version.txt +1 -0
- charmlibs/pathops/py.typed +0 -0
- charmlibs_pathops-0.0.0.dist-info/METADATA +27 -0
- charmlibs_pathops-0.0.0.dist-info/RECORD +14 -0
- charmlibs_pathops-0.0.0.dist-info/WHEEL +5 -0
- charmlibs_pathops-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Methods for matching Python Exceptions to Pebble Errors and creating Exception objects."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import errno
|
|
20
|
+
import os
|
|
21
|
+
from typing import NoReturn
|
|
22
|
+
|
|
23
|
+
from ops import pebble
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def raise_if_matches_directory_not_empty(error: pebble.Error, msg: str) -> None:
|
|
27
|
+
if (
|
|
28
|
+
isinstance(error, pebble.PathError)
|
|
29
|
+
and error.kind == 'generic-file-error'
|
|
30
|
+
and 'directory not empty' in error.message
|
|
31
|
+
):
|
|
32
|
+
raise OSError(errno.ENOTEMPTY, os.strerror(errno.ENOTEMPTY), msg) from error
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def raise_file_exists(msg: str, from_: BaseException | None = None) -> NoReturn:
|
|
36
|
+
e = FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), msg)
|
|
37
|
+
raise e from from_
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def raise_if_matches_file_exists(error: pebble.Error, msg: str) -> None:
|
|
41
|
+
if (
|
|
42
|
+
isinstance(error, pebble.PathError)
|
|
43
|
+
and error.kind == 'generic-file-error'
|
|
44
|
+
and 'file exists' in error.message
|
|
45
|
+
):
|
|
46
|
+
raise_file_exists(msg, from_=error)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def raise_file_not_found(msg: str, from_: BaseException | None = None) -> NoReturn:
|
|
50
|
+
# pebble will return this error when trying to read_{text,bytes} a socket
|
|
51
|
+
# pathlib raises OSError(errno.ENXIO, os.strerror(errno.ENXIO), path) in this case
|
|
52
|
+
# displaying as "OSError: [Errno 6] No such device or address: '/path'"
|
|
53
|
+
# since FileNotFoundError is a subtype of OSError, and this case should be rare
|
|
54
|
+
# it seems sensible to just raise FileNotFoundError here, without checking
|
|
55
|
+
# if the file in question is a socket
|
|
56
|
+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), msg) from from_
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def raise_if_matches_file_not_found(error: pebble.Error, msg: str) -> None:
|
|
60
|
+
if (isinstance(error, pebble.APIError) and error.code == 404) or (
|
|
61
|
+
isinstance(error, pebble.PathError) and error.kind == 'not-found'
|
|
62
|
+
):
|
|
63
|
+
raise_file_not_found(msg, from_=error)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def raise_is_a_directory(msg: str, from_: BaseException | None = None) -> None:
|
|
67
|
+
raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), msg) from from_
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def raise_if_matches_is_a_directory(error: pebble.Error, msg: str) -> None:
|
|
71
|
+
if (
|
|
72
|
+
isinstance(error, pebble.PathError)
|
|
73
|
+
and error.kind == 'generic-file-error'
|
|
74
|
+
and 'can only read a regular file' in error.message
|
|
75
|
+
):
|
|
76
|
+
raise_is_a_directory(msg, from_=error)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def raise_if_matches_lookup(error: pebble.Error, msg: str) -> None:
|
|
80
|
+
if (
|
|
81
|
+
isinstance(error, pebble.PathError)
|
|
82
|
+
and error.kind == 'generic-file-error'
|
|
83
|
+
and 'cannot look up user and group' in error.message
|
|
84
|
+
):
|
|
85
|
+
raise LookupError(msg) from error
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def matches_not_a_directory(error: pebble.Error) -> bool:
|
|
89
|
+
return (
|
|
90
|
+
isinstance(error, pebble.APIError)
|
|
91
|
+
and error.code == 400
|
|
92
|
+
and 'not a directory' in error.message
|
|
93
|
+
) or (
|
|
94
|
+
isinstance(error, pebble.PathError)
|
|
95
|
+
and error.kind == 'generic-file-error'
|
|
96
|
+
and 'not a directory' in error.message
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def raise_not_a_directory(msg: str, from_: BaseException | None = None) -> NoReturn:
|
|
101
|
+
raise NotADirectoryError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), msg) from from_
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def raise_if_matches_not_a_directory(error: pebble.Error, msg: str) -> None:
|
|
105
|
+
if matches_not_a_directory(error):
|
|
106
|
+
raise_not_a_directory(msg, from_=error)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def raise_if_matches_permission(error: pebble.Error, msg: str) -> None:
|
|
110
|
+
if isinstance(error, pebble.PathError) and error.kind == 'permission-denied':
|
|
111
|
+
raise PermissionError(errno.EPERM, os.strerror(errno.EPERM), msg) from error
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def raise_if_matches_too_many_levels_of_symlinks(error: pebble.Error, msg: str) -> None:
|
|
115
|
+
if (
|
|
116
|
+
isinstance(error, pebble.APIError)
|
|
117
|
+
and error.code == 400
|
|
118
|
+
and 'too many levels of symbolic links' in error.message
|
|
119
|
+
):
|
|
120
|
+
raise OSError(errno.ELOOP, os.strerror(errno.ELOOP), msg) from error
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Copyright 2024 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Utilities for creating and working with :class:`ops.pebble.FileInfo` objects."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import datetime
|
|
20
|
+
import grp
|
|
21
|
+
import pwd
|
|
22
|
+
import stat
|
|
23
|
+
import typing
|
|
24
|
+
|
|
25
|
+
from ops import pebble
|
|
26
|
+
|
|
27
|
+
from . import _errors
|
|
28
|
+
|
|
29
|
+
if typing.TYPE_CHECKING:
|
|
30
|
+
import pathlib
|
|
31
|
+
|
|
32
|
+
from ._container_path import ContainerPath
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_FT_MAP: dict[int, pebble.FileType] = {
|
|
36
|
+
stat.S_IFREG: pebble.FileType.FILE,
|
|
37
|
+
stat.S_IFDIR: pebble.FileType.DIRECTORY,
|
|
38
|
+
stat.S_IFLNK: pebble.FileType.SYMLINK,
|
|
39
|
+
stat.S_IFSOCK: pebble.FileType.SOCKET,
|
|
40
|
+
stat.S_IFIFO: pebble.FileType.NAMED_PIPE,
|
|
41
|
+
stat.S_IFBLK: pebble.FileType.DEVICE, # block device
|
|
42
|
+
stat.S_IFCHR: pebble.FileType.DEVICE, # character device
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def from_container_path(path: ContainerPath, follow_symlinks: bool = True) -> pebble.FileInfo:
|
|
47
|
+
if follow_symlinks:
|
|
48
|
+
return _get_fileinfo_directly(path)
|
|
49
|
+
return _get_fileinfo_from_parent(path)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_fileinfo_directly(path: ContainerPath) -> pebble.FileInfo:
|
|
53
|
+
try:
|
|
54
|
+
info_list = path._container.list_files(path._path, itself=True)
|
|
55
|
+
except (pebble.APIError, pebble.PathError) as e:
|
|
56
|
+
msg = repr(path)
|
|
57
|
+
_errors.raise_if_matches_file_not_found(e, msg=msg)
|
|
58
|
+
_errors.raise_if_matches_not_a_directory(e, msg=msg)
|
|
59
|
+
_errors.raise_if_matches_permission(e, msg=msg)
|
|
60
|
+
_errors.raise_if_matches_too_many_levels_of_symlinks(e, msg=msg)
|
|
61
|
+
raise
|
|
62
|
+
assert len(info_list) == 1, 'ops.Container.list_files with itself=True returns 1 item'
|
|
63
|
+
return info_list[0]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_fileinfo_from_parent(path: ContainerPath) -> pebble.FileInfo:
|
|
67
|
+
try:
|
|
68
|
+
info_list = path._container.list_files(path._path.parent, pattern=path.name)
|
|
69
|
+
except (pebble.APIError, pebble.PathError) as e:
|
|
70
|
+
msg = repr(path)
|
|
71
|
+
_errors.raise_if_matches_file_not_found(e, msg=msg)
|
|
72
|
+
_errors.raise_if_matches_permission(e, msg=msg)
|
|
73
|
+
raise
|
|
74
|
+
if not info_list:
|
|
75
|
+
_errors.raise_file_not_found(repr(path))
|
|
76
|
+
assert len(info_list) == 1, 'ops.Container.list_files with non-* pattern should return 1 item'
|
|
77
|
+
return info_list[0]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def from_pathlib_path(path: pathlib.Path, follow_symlinks: bool = True) -> pebble.FileInfo:
|
|
81
|
+
stat_result = path.stat() if follow_symlinks else path.lstat()
|
|
82
|
+
utcoffset = datetime.datetime.now().astimezone().utcoffset()
|
|
83
|
+
timezone = datetime.timezone(utcoffset) if utcoffset is not None else datetime.timezone.utc
|
|
84
|
+
filetype = _FT_MAP.get(stat.S_IFMT(stat_result.st_mode), pebble.FileType.UNKNOWN)
|
|
85
|
+
size = stat_result.st_size if filetype is pebble.FileType.FILE else None
|
|
86
|
+
return pebble.FileInfo(
|
|
87
|
+
path=str(path),
|
|
88
|
+
name=path.name,
|
|
89
|
+
type=filetype,
|
|
90
|
+
size=size,
|
|
91
|
+
permissions=stat.S_IMODE(stat_result.st_mode),
|
|
92
|
+
last_modified=datetime.datetime.fromtimestamp(int(stat_result.st_mtime), tz=timezone),
|
|
93
|
+
user_id=stat_result.st_uid,
|
|
94
|
+
user=pwd.getpwuid(stat_result.st_uid).pw_name,
|
|
95
|
+
group_id=stat_result.st_gid,
|
|
96
|
+
group=grp.getgrgid(stat_result.st_gid).gr_name,
|
|
97
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Copyright 2024 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Public helper functions exported by this package."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import pathlib
|
|
20
|
+
import typing
|
|
21
|
+
|
|
22
|
+
from . import _constants, _fileinfo
|
|
23
|
+
from ._container_path import ContainerPath
|
|
24
|
+
from ._local_path import LocalPath
|
|
25
|
+
|
|
26
|
+
if typing.TYPE_CHECKING:
|
|
27
|
+
import os
|
|
28
|
+
from typing import BinaryIO, TextIO
|
|
29
|
+
|
|
30
|
+
from ops import pebble
|
|
31
|
+
from typing_extensions import TypeIs
|
|
32
|
+
|
|
33
|
+
from ._types import PathProtocol
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ensure_contents(
|
|
37
|
+
path: str | os.PathLike[str] | PathProtocol,
|
|
38
|
+
source: bytes | str | BinaryIO | TextIO,
|
|
39
|
+
*,
|
|
40
|
+
mode: int = _constants.DEFAULT_WRITE_MODE,
|
|
41
|
+
user: str | None = None,
|
|
42
|
+
group: str | None = None,
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Ensure ``source`` can be read from ``path``. Return True if any changes were made.
|
|
45
|
+
|
|
46
|
+
Ensure that ``path`` exists, contains ``source``, has the correct permissions (``mode``),
|
|
47
|
+
and has the correct file ownership (``user`` and ``group``).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: A local or remote filesystem path.
|
|
51
|
+
source: The desired contents in ``str`` or ``bytes`` form, or an object with a ``.read()``
|
|
52
|
+
method which returns a ``str`` or ``bytes`` object.
|
|
53
|
+
mode: The desired file permissions.
|
|
54
|
+
user: The desired file owner, or ``None`` to not change the owner.
|
|
55
|
+
group: The desired group, or ``None`` to not change the group.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
``True`` if any changes were made, including permissions or ownership, otherwise ``False``.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
LookupError: if the user or group is unknown.
|
|
62
|
+
NotADirectoryError: if the parent exists as a non-directory file.
|
|
63
|
+
PermissionError: if the user does not have permissions for the operation.
|
|
64
|
+
:class:`PebbleConnectionError`: if the remote Pebble client cannot be reached.
|
|
65
|
+
"""
|
|
66
|
+
if _is_str_pathlike(path):
|
|
67
|
+
path = LocalPath(path)
|
|
68
|
+
source = _as_bytes(source)
|
|
69
|
+
try:
|
|
70
|
+
info = _get_fileinfo(path)
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
pass # file doesn't exist, so writing is required
|
|
73
|
+
else: # check if metadata and contents already match
|
|
74
|
+
if (
|
|
75
|
+
(info.permissions == mode)
|
|
76
|
+
and (user is None or info.user == user)
|
|
77
|
+
and (group is None or info.group == group)
|
|
78
|
+
and (path.read_bytes() == source)
|
|
79
|
+
):
|
|
80
|
+
return False # everything matches, so writing is not required
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
path.write_bytes(source, mode=mode, user=user, group=group)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_str_pathlike(obj: object) -> TypeIs[str | os.PathLike[str]]:
|
|
87
|
+
return isinstance(obj, str) or hasattr(obj, '__fspath__')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_fileinfo(
|
|
91
|
+
path: str | os.PathLike[str] | PathProtocol, follow_symlinks: bool = True
|
|
92
|
+
) -> pebble.FileInfo:
|
|
93
|
+
if isinstance(path, ContainerPath):
|
|
94
|
+
return _fileinfo.from_container_path(path, follow_symlinks=follow_symlinks)
|
|
95
|
+
assert _is_str_pathlike(path)
|
|
96
|
+
return _fileinfo.from_pathlib_path(pathlib.Path(path), follow_symlinks=follow_symlinks)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _as_bytes(source: bytes | str | BinaryIO | TextIO) -> bytes:
|
|
100
|
+
if isinstance(source, bytes):
|
|
101
|
+
return source
|
|
102
|
+
if isinstance(source, str):
|
|
103
|
+
return source.encode()
|
|
104
|
+
return _as_bytes(source.read())
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Copyright 2024 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Implementation of LocalPath class."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import grp
|
|
20
|
+
import pathlib
|
|
21
|
+
import pwd
|
|
22
|
+
import shutil
|
|
23
|
+
import typing
|
|
24
|
+
|
|
25
|
+
from . import _constants
|
|
26
|
+
|
|
27
|
+
if typing.TYPE_CHECKING:
|
|
28
|
+
from typing_extensions import Buffer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LocalPath(pathlib.PosixPath):
|
|
32
|
+
r""":class:`pathlib.PosixPath` subclass with extended file-creation method arguments.
|
|
33
|
+
|
|
34
|
+
.. note::
|
|
35
|
+
The :meth:`write_bytes`, :meth:`write_text`, and :meth:`mkdir` methods are extended with
|
|
36
|
+
file permission and ownership arguments, for compatibility with :class:`PathProtocol`.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
\*parts: :class:`str` or :class:`os.PathLike`. ``LocalPath`` takes no keyword arguments.
|
|
40
|
+
|
|
41
|
+
::
|
|
42
|
+
|
|
43
|
+
LocalPath(pathlib.Path('/foo'))
|
|
44
|
+
LocalPath('/', 'foo')
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def write_bytes(
|
|
48
|
+
self,
|
|
49
|
+
data: Buffer,
|
|
50
|
+
*,
|
|
51
|
+
mode: int | None = None,
|
|
52
|
+
user: str | None = None,
|
|
53
|
+
group: str | None = None,
|
|
54
|
+
) -> int:
|
|
55
|
+
"""Write the provided data to the corresponding local filesystem path.
|
|
56
|
+
|
|
57
|
+
Compared to :meth:`pathlib.Path.write_bytes`, this method adds ``mode``, ``user``
|
|
58
|
+
and ``group`` args. These are used to set the permissions and ownership of the file.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data: The bytes to write, typically a :class:`bytes` object, but may also be a
|
|
62
|
+
:class:`bytearray` or :class:`memoryview`.
|
|
63
|
+
mode: The permissions to set on the file. Defaults to 0o644 (-rw-r--r--) for new files.
|
|
64
|
+
If the file already exists, its permissions will be changed, using
|
|
65
|
+
:meth:`pathlib.PosixPath.chmod`, unless ``mode`` is ``None`` (default).
|
|
66
|
+
user: The name of the user to set for the file using :func:`shutil.chown`.
|
|
67
|
+
Validated to be an existing user before writing.
|
|
68
|
+
If the file already exists, its user and group will be changed,
|
|
69
|
+
unless ``user`` is ``None`` (default).
|
|
70
|
+
group: The name of the group to set for the file using :func:`shutil.chown`.
|
|
71
|
+
Validated to be an existing group before writing.
|
|
72
|
+
If the file already exists, its group will be changed,
|
|
73
|
+
unless ``user`` and ``group`` are ``None`` (default).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The number of bytes written.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
FileNotFoundError: if the parent directory does not exist.
|
|
80
|
+
LookupError: if the user or group is unknown.
|
|
81
|
+
NotADirectoryError: if the parent exists as a non-directory file.
|
|
82
|
+
PermissionError: if the local user does not have permissions for the operation.
|
|
83
|
+
"""
|
|
84
|
+
_validate_user_and_group(user=user, group=group)
|
|
85
|
+
if mode is None:
|
|
86
|
+
# create the file with Pebble's default write mode if it doesn't exist
|
|
87
|
+
# doesn't change the mode if the file already exists
|
|
88
|
+
self.touch(mode=_constants.DEFAULT_WRITE_MODE)
|
|
89
|
+
bytes_written = super().write_bytes(data)
|
|
90
|
+
_chown_if_needed(self, user=user, group=group)
|
|
91
|
+
if mode is not None:
|
|
92
|
+
# explicitly set the mode if the user requested it
|
|
93
|
+
self.chmod(mode)
|
|
94
|
+
return bytes_written
|
|
95
|
+
|
|
96
|
+
def write_text(
|
|
97
|
+
self,
|
|
98
|
+
data: str,
|
|
99
|
+
encoding: str | None = None,
|
|
100
|
+
errors: str | None = None,
|
|
101
|
+
newline: str | None = None,
|
|
102
|
+
*,
|
|
103
|
+
mode: int | None = None,
|
|
104
|
+
user: str | None = None,
|
|
105
|
+
group: str | None = None,
|
|
106
|
+
) -> int:
|
|
107
|
+
r"""Write the provided string to the corresponding local filesystem path.
|
|
108
|
+
|
|
109
|
+
Compared to :meth:`pathlib.Path.write_bytes`, this method adds ``mode``, ``user``
|
|
110
|
+
and ``group`` args. These are used to set the permissions and ownership of the file.
|
|
111
|
+
|
|
112
|
+
.. warning::
|
|
113
|
+
:class:`ContainerPath` and :class:`PathProtocol` do not support the ``encoding``,
|
|
114
|
+
``errors``, and ``newline`` arguments of :meth:`pathlib.Path.write_text`.
|
|
115
|
+
For :class:`ContainerPath` compatible code, do not use these arguments.
|
|
116
|
+
They are provided to allow :class:`LocalPath` to be used as a drop-in
|
|
117
|
+
replacement for :class:`pathlib.Path` if needed.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
data: The string to write. Newlines are not modified on writing.
|
|
121
|
+
encoding: The encoding to use when writing the data, defaults to 'UTF-8'.
|
|
122
|
+
errors: 'strict' to raise any encoding errors, 'ignore' to ignore them.
|
|
123
|
+
Defaults to 'strict'.
|
|
124
|
+
newline: If ``None``, ``''``, or ``'\n'``, then '\n' will be written as is.
|
|
125
|
+
This is the default behaviour. If ``newline`` is ``'\r'`` or ``'\r\n'``,
|
|
126
|
+
then ``'\n'`` will be replaced with ``newline`` in memory before writing.
|
|
127
|
+
mode: The permissions to set on the file. Defaults to 0o644 (-rw-r--r--) for new files.
|
|
128
|
+
If the file already exists, its permissions will be changed, using
|
|
129
|
+
:meth:`pathlib.PosixPath.chmod`, unless ``mode`` is ``None`` (default).
|
|
130
|
+
user: The name of the user to set for the file using :func:`shutil.chown`.
|
|
131
|
+
Validated to be an existing user before writing.
|
|
132
|
+
If the file already exists, its user and group will be changed,
|
|
133
|
+
unless ``user`` is ``None`` (default).
|
|
134
|
+
group: The name of the group to set for the file using :func:`shutil.chown`.
|
|
135
|
+
Validated to be an existing group before writing.
|
|
136
|
+
If the file already exists, its group will be changed,
|
|
137
|
+
unless ``user`` and ``group`` are ``None`` (default).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The number of bytes written.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
FileNotFoundError: if the parent directory does not exist.
|
|
144
|
+
LookupError: if the user or group is unknown.
|
|
145
|
+
NotADirectoryError: if the parent exists as a non-directory file.
|
|
146
|
+
PermissionError: if the local user does not have permissions for the operation.
|
|
147
|
+
ValueError: if ``newline`` is any value other than those documented above.
|
|
148
|
+
"""
|
|
149
|
+
_validate_user_and_group(user=user, group=group)
|
|
150
|
+
if newline in ('\r', '\r\n'):
|
|
151
|
+
data = data.replace('\n', newline)
|
|
152
|
+
elif newline not in ('', '\n', None):
|
|
153
|
+
raise ValueError(f'illegal newline value: {newline!r}')
|
|
154
|
+
if mode is None:
|
|
155
|
+
# create the file with Pebble's default write mode
|
|
156
|
+
self.touch(mode=_constants.DEFAULT_WRITE_MODE)
|
|
157
|
+
bytes_written = super().write_text(data, encoding=encoding, errors=errors)
|
|
158
|
+
_chown_if_needed(self, user=user, group=group)
|
|
159
|
+
if mode is not None:
|
|
160
|
+
# explicitly set the mode if the user requested it
|
|
161
|
+
self.chmod(mode)
|
|
162
|
+
return bytes_written
|
|
163
|
+
|
|
164
|
+
def mkdir(
|
|
165
|
+
self,
|
|
166
|
+
mode: int = _constants.DEFAULT_MKDIR_MODE,
|
|
167
|
+
parents: bool = False,
|
|
168
|
+
exist_ok: bool = False,
|
|
169
|
+
*,
|
|
170
|
+
user: str | None = None,
|
|
171
|
+
group: str | None = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Create a new directory at the corresponding local filesystem path.
|
|
174
|
+
|
|
175
|
+
Compared to :meth:`pathlib.Path.mkdir`, this method adds ``user`` and ``group`` args.
|
|
176
|
+
These are used to set the ownership of the created directory. Any created parents
|
|
177
|
+
will not have their ownership set.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
mode: The permissions to set on the created directory. Any parents created will have
|
|
181
|
+
their permissions set to the default value of 0o755 (drwxr-xr-x).
|
|
182
|
+
The permissions are not changed if the directory already exists.
|
|
183
|
+
parents: Whether to create any missing parent directories as well. If ``False``
|
|
184
|
+
(default) and a parent directory does not exist, a :class:`FileNotFound` error will
|
|
185
|
+
be raised.
|
|
186
|
+
exist_ok: Whether to raise an error if the directory already exists.
|
|
187
|
+
If ``False`` (default) and the directory already exists,
|
|
188
|
+
a :class:`FileExistsError` will be raised.
|
|
189
|
+
user: The name of the user to set for the directory using :func:`shutil.chown`.
|
|
190
|
+
Validated to be an existing user before writing.
|
|
191
|
+
The user and group are not changed if the directory already exists.
|
|
192
|
+
group: The name of the group to set for the directory using :func:`shutil.chown`.
|
|
193
|
+
Validated to be an existing group before writing.
|
|
194
|
+
The user and group are not changed if the directory already exists.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
FileExistsError: if the directory already exists and ``exist_ok`` is ``False``.
|
|
198
|
+
FileNotFoundError: if the parent directory does not exist and ``parents`` is ``False``.
|
|
199
|
+
LookupError: if the user or group is unknown.
|
|
200
|
+
NotADirectoryError: if the parent exists as a non-directory file.
|
|
201
|
+
PermissionError: if the local user does not have permissions for the operation.
|
|
202
|
+
"""
|
|
203
|
+
_validate_user_and_group(user=user, group=group)
|
|
204
|
+
already_exists = self.exists()
|
|
205
|
+
super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok)
|
|
206
|
+
if not already_exists:
|
|
207
|
+
_chown_if_needed(self, user=user, group=group)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _validate_user_and_group(user: str | None, group: str | None):
|
|
211
|
+
if user is not None:
|
|
212
|
+
pwd.getpwnam(user)
|
|
213
|
+
if group is not None:
|
|
214
|
+
grp.getgrnam(group)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _chown_if_needed(path: pathlib.Path, user: str | int | None, group: str | int | None) -> None:
|
|
218
|
+
if user is not None:
|
|
219
|
+
if group is None: # use the user's group, following Pebble
|
|
220
|
+
info = pwd.getpwnam(user) if isinstance(user, str) else pwd.getpwuid(user)
|
|
221
|
+
group = info.pw_gid
|
|
222
|
+
shutil.chown(path, user=user, group=group)
|
|
223
|
+
elif group is not None:
|
|
224
|
+
shutil.chown(path, group=group)
|