python3-cyberfusion-file-support 1.1.1.2__tar.gz → 1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python3-cyberfusion-file-support
3
- Version: 1.1.1.2
3
+ Version: 1.2
4
4
  Summary: Library for idempotent writing to files.
5
5
  Author-email: Cyberfusion <support@cyberfusion.io>
6
6
  Project-URL: Source, https://github.com/CyberfusionIO/python3-cyberfusion-file-support
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python3-cyberfusion-file-support"
7
- version = "1.1.1.2"
7
+ version = "1.2"
8
8
  description = "Library for idempotent writing to files."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,8 +1,9 @@
1
1
  """Classes for files."""
2
2
 
3
3
  import difflib
4
- import filecmp
4
+
5
5
  import os
6
+
6
7
  from typing import List, Optional, Union
7
8
 
8
9
  from cyberfusion.Common import get_tmp_file
@@ -11,13 +12,27 @@ from cyberfusion.QueueSupport.items.command import CommandItem
11
12
  from cyberfusion.QueueSupport.items.copy import CopyItem
12
13
  from cyberfusion.QueueSupport.items.unlink import UnlinkItem
13
14
 
15
+ from cyberfusion.FileSupport.encryption import (
16
+ EncryptionProperties,
17
+ encrypt_file,
18
+ decrypt_file,
19
+ )
20
+ from cyberfusion.FileSupport.exceptions import DecryptionError
21
+
14
22
 
15
23
  class _DestinationFile:
16
24
  """Represents destination file."""
17
25
 
18
- def __init__(self, *, path: str) -> None:
19
- """Set attributes."""
26
+ def __init__(
27
+ self, *, path: str, encryption_properties: Optional[EncryptionProperties] = None
28
+ ) -> None:
29
+ """Set attributes.
30
+
31
+ If 'encryption_properties' is specified, and the destination file already
32
+ exists, it must be encrypted using the same properties (it is decrypted).
33
+ """
20
34
  self.path = path
35
+ self.encryption_properties = encryption_properties
21
36
 
22
37
  @property
23
38
  def exists(self) -> bool:
@@ -27,11 +42,19 @@ class _DestinationFile:
27
42
  @property
28
43
  def contents(self) -> Optional[str]:
29
44
  """Get contents."""
30
- if self.exists:
45
+ if not self.exists:
46
+ return None
47
+
48
+ if not self.encryption_properties:
31
49
  with open(self.path, "r") as f:
32
50
  return f.read()
33
51
 
34
- return None
52
+ try:
53
+ return decrypt_file(self.encryption_properties, self.path)
54
+ except DecryptionError as e:
55
+ raise DecryptionError(
56
+ f"Decrypting the destination file at '{self.path}' failed. Note that the file must already be encrypted using the specified encryption properties."
57
+ ) from e
35
58
 
36
59
 
37
60
  class DestinationFileReplacement:
@@ -41,33 +64,37 @@ class DestinationFileReplacement:
41
64
  self,
42
65
  queue: Queue,
43
66
  *,
44
- contents: Union[str, bytes],
67
+ contents: str,
45
68
  destination_file_path: str,
46
69
  default_comment_character: Optional[str] = None,
47
70
  command: Optional[List[str]] = None,
48
71
  reference: Optional[str] = None,
72
+ encryption_properties: Optional[EncryptionProperties] = None,
49
73
  ) -> None:
50
74
  """Set attributes.
51
75
 
52
76
  'default_comment_character' has no effect when 'contents' is not string.
77
+
78
+ If 'encryption_properties' is specified, and the destination file already
79
+ exists, it must be encrypted using the same properties (it is decrypted).
53
80
  """
54
81
  self.queue = queue
55
82
  self._contents = contents
56
83
  self.default_comment_character = default_comment_character
57
84
  self.command = command
58
85
  self.reference = reference
86
+ self.encryption_properties = encryption_properties
59
87
 
60
88
  self.tmp_path = get_tmp_file()
61
- self.destination_file = _DestinationFile(path=destination_file_path)
89
+ self.destination_file = _DestinationFile(
90
+ path=destination_file_path, encryption_properties=encryption_properties
91
+ )
62
92
 
63
- self._write_to_tmp_file()
93
+ self.write_to_file(self.tmp_path)
64
94
 
65
95
  @property
66
- def contents(self) -> Union[str, bytes]:
96
+ def contents(self) -> str:
67
97
  """Get contents."""
68
- if not isinstance(self._contents, str):
69
- return self._contents
70
-
71
98
  if self._contents != "" and not self._contents.endswith(
72
99
  "\n"
73
100
  ): # Some programs require newline to consider last line completed
@@ -84,22 +111,32 @@ class DestinationFileReplacement:
84
111
 
85
112
  return default_comment + self._contents
86
113
 
87
- def _write_to_tmp_file(self) -> None:
88
- """Write contents to tmp file."""
89
- if isinstance(self.contents, bytes):
114
+ def write_to_file(self, path: str) -> None:
115
+ """Write contents to file."""
116
+ contents: Union[str, bytes]
117
+
118
+ if self.encryption_properties:
90
119
  open_mode = "wb"
120
+
121
+ contents = encrypt_file(
122
+ self.encryption_properties,
123
+ self.contents,
124
+ )
91
125
  else:
92
126
  open_mode = "w"
93
127
 
94
- with open(self.tmp_path, open_mode) as f:
95
- f.write(self.contents)
128
+ contents = self.contents
129
+
130
+ with open(path, open_mode) as f:
131
+ f.write(contents)
96
132
 
97
133
  @property
98
134
  def changed(self) -> bool:
99
135
  """Get if destination file will change."""
100
- return not self.destination_file.exists or not filecmp.cmp(
101
- self.tmp_path, self.destination_file.path
102
- )
136
+ if not self.destination_file.exists:
137
+ return True
138
+
139
+ return self.destination_file.contents != self.contents
103
140
 
104
141
  @property
105
142
  def differences(self) -> List[str]:
@@ -107,9 +144,6 @@ class DestinationFileReplacement:
107
144
 
108
145
  No differences are returned when contents is not string.
109
146
  """
110
- if not isinstance(self._contents, str):
111
- return []
112
-
113
147
  results = []
114
148
 
115
149
  for line in difflib.unified_diff(
@@ -118,7 +152,7 @@ class DestinationFileReplacement:
118
152
  if self.destination_file.contents
119
153
  else []
120
154
  ),
121
- self.contents.splitlines(), # type: ignore[arg-type]
155
+ self.contents.splitlines(),
122
156
  fromfile=self.tmp_path,
123
157
  tofile=self.destination_file.path,
124
158
  lineterm="",
@@ -0,0 +1,65 @@
1
+ """Utilities for file encryption."""
2
+
3
+ import subprocess
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+ from cyberfusion.FileSupport.exceptions import EncryptionError, DecryptionError
8
+
9
+
10
+ class MessageDigestEnum(str, Enum):
11
+ """Message digests supported by OpenSSL."""
12
+
13
+ MD2 = "md2"
14
+ MD5 = "md5"
15
+ SHA = "sha"
16
+ SHA1 = "sha1"
17
+
18
+
19
+ @dataclass
20
+ class EncryptionProperties:
21
+ """Properties to encrypt files, needed by OpenSSL."""
22
+
23
+ cipher_name: str # Get options with `openssl list -cipher-algorithms`
24
+ message_digest: MessageDigestEnum
25
+ password_file_path: str # Create password with `openssl rand -hex 128`
26
+
27
+
28
+ def encrypt_file(encryption_properties: EncryptionProperties, contents: str) -> bytes:
29
+ """Get contents for file to encrypt."""
30
+ try:
31
+ return subprocess.check_output(
32
+ [
33
+ "openssl",
34
+ "enc",
35
+ "-" + encryption_properties.cipher_name,
36
+ "-md",
37
+ encryption_properties.message_digest,
38
+ "-pass",
39
+ "file:" + encryption_properties.password_file_path,
40
+ ],
41
+ input=contents.encode(),
42
+ )
43
+ except subprocess.CalledProcessError as e:
44
+ raise EncryptionError from e
45
+
46
+
47
+ def decrypt_file(encryption_properties: EncryptionProperties, path: str) -> str:
48
+ """Get contents of encrypted file."""
49
+ try:
50
+ return subprocess.check_output(
51
+ [
52
+ "openssl",
53
+ "enc",
54
+ "-d",
55
+ "-" + encryption_properties.cipher_name,
56
+ "-md",
57
+ encryption_properties.message_digest,
58
+ "-pass",
59
+ "file:" + encryption_properties.password_file_path,
60
+ "-in",
61
+ path,
62
+ ]
63
+ ).decode()
64
+ except subprocess.CalledProcessError as e:
65
+ raise DecryptionError from e
@@ -0,0 +1,13 @@
1
+ """Exceptions."""
2
+
3
+
4
+ class EncryptionError(Exception):
5
+ """Encrypting failed."""
6
+
7
+ pass
8
+
9
+
10
+ class DecryptionError(Exception):
11
+ """Decrypting failed."""
12
+
13
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python3-cyberfusion-file-support
3
- Version: 1.1.1.2
3
+ Version: 1.2
4
4
  Summary: Library for idempotent writing to files.
5
5
  Author-email: Cyberfusion <support@cyberfusion.io>
6
6
  Project-URL: Source, https://github.com/CyberfusionIO/python3-cyberfusion-file-support
@@ -2,6 +2,8 @@ README.md
2
2
  pyproject.toml
3
3
  setup.cfg
4
4
  src/cyberfusion/FileSupport/__init__.py
5
+ src/cyberfusion/FileSupport/encryption.py
6
+ src/cyberfusion/FileSupport/exceptions.py
5
7
  src/python3_cyberfusion_file_support.egg-info/PKG-INFO
6
8
  src/python3_cyberfusion_file_support.egg-info/SOURCES.txt
7
9
  src/python3_cyberfusion_file_support.egg-info/dependency_links.txt