|
5 | 5 | import re |
6 | 6 | import stat |
7 | 7 | from datetime import datetime |
| 8 | +from typing import List, Tuple |
8 | 9 |
|
9 | 10 | from pyinfra.api.command import QuoteString, make_formatted_string_command |
10 | 11 | from pyinfra.api.facts import FactBase |
@@ -204,91 +205,57 @@ class Socket(File): |
204 | 205 | type = "socket" |
205 | 206 |
|
206 | 207 |
|
207 | | -class Sha1File(FactBase): |
208 | | - """ |
209 | | - Returns a SHA1 hash of a file. Works with both sha1sum and sha1. Returns |
210 | | - ``None`` if the file doest not exist. |
211 | | - """ |
| 208 | +class HashFileFactBase(FactBase): |
| 209 | + _raw_cmd: str |
| 210 | + _regexes: Tuple[str, str] |
| 211 | + |
| 212 | + def __init_subclass__(cls, digits: int, cmds: List[str], **kwargs) -> None: |
| 213 | + super().__init_subclass__(**kwargs) |
212 | 214 |
|
213 | | - _regexes = [ |
214 | | - r"^([a-zA-Z0-9]{40})\s+%s$", |
215 | | - r"^SHA1\s+\(%s\)\s+=\s+([a-zA-Z0-9]{40})$", |
216 | | - ] |
| 215 | + raw_hash_cmds = ["%s {0} 2> /dev/null" % cmd for cmd in cmds] |
| 216 | + raw_hash_cmd = " || ".join(raw_hash_cmds) |
| 217 | + cls._raw_cmd = "test -e {0} && ( %s ) || true" % raw_hash_cmd |
| 218 | + |
| 219 | + assert cls.__name__.endswith("File") |
| 220 | + hash_name = cls.__name__[:-4].upper() |
| 221 | + cls._regexes = ( |
| 222 | + # GNU coreutils style: |
| 223 | + r"^([a-fA-F0-9]{%d})\s+%%s$" % digits, |
| 224 | + # BSD style: |
| 225 | + r"^%s\s+\(%%s\)\s+=\s+([a-fA-F0-9]{%d})$" % (hash_name, digits), |
| 226 | + ) |
217 | 227 |
|
218 | 228 | def command(self, path): |
219 | 229 | self.path = path |
220 | | - return make_formatted_string_command( |
221 | | - ( |
222 | | - "test -e {0} && ( " |
223 | | - "sha1sum {0} 2> /dev/null || shasum {0} 2> /dev/null || sha1 {0} " |
224 | | - ") || true" |
225 | | - ), |
226 | | - QuoteString(path), |
227 | | - ) |
| 230 | + return make_formatted_string_command(self._raw_cmd, QuoteString(path)) |
228 | 231 |
|
229 | 232 | def process(self, output): |
| 233 | + output = output[0] |
| 234 | + escaped_path = re.escape(self.path) |
230 | 235 | for regex in self._regexes: |
231 | | - regex = regex % re.escape(self.path) |
232 | | - matches = re.match(regex, output[0]) |
| 236 | + matches = re.match(regex % escaped_path, output) |
233 | 237 | if matches: |
234 | 238 | return matches.group(1) |
235 | 239 |
|
236 | 240 |
|
237 | | -class Sha256File(FactBase): |
| 241 | +class Sha1File(HashFileFactBase, digits=40, cmds=["sha1sum", "shasum", "sha1"]): |
238 | 242 | """ |
239 | | - Returns a SHA256 hash of a file, or ``None`` if the file does not exist. |
| 243 | + Returns a SHA1 hash of a file. Works with both sha1sum and sha1. Returns |
| 244 | + ``None`` if the file doest not exist. |
240 | 245 | """ |
241 | 246 |
|
242 | | - _regexes = [ |
243 | | - r"^([a-zA-Z0-9]{64})\s+%s$", |
244 | | - r"^SHA256\s+\(%s\)\s+=\s+([a-zA-Z0-9]{64})$", |
245 | | - ] |
246 | | - |
247 | | - def command(self, path): |
248 | | - self.path = path |
249 | | - return make_formatted_string_command( |
250 | | - ( |
251 | | - "test -e {0} && ( " |
252 | | - "sha256sum {0} 2> /dev/null " |
253 | | - "|| shasum -a 256 {0} 2> /dev/null " |
254 | | - "|| sha256 {0} " |
255 | | - ") || true" |
256 | | - ), |
257 | | - QuoteString(path), |
258 | | - ) |
259 | 247 |
|
260 | | - def process(self, output): |
261 | | - for regex in self._regexes: |
262 | | - regex = regex % re.escape(self.path) |
263 | | - matches = re.match(regex, output[0]) |
264 | | - if matches: |
265 | | - return matches.group(1) |
| 248 | +class Sha256File(HashFileFactBase, digits=64, cmds=["sha256sum", "shasum -a 256", "sha256"]): |
| 249 | + """ |
| 250 | + Returns a SHA256 hash of a file, or ``None`` if the file does not exist. |
| 251 | + """ |
266 | 252 |
|
267 | 253 |
|
268 | | -class Md5File(FactBase): |
| 254 | +class Md5File(HashFileFactBase, digits=32, cmds=["md5sum", "md5"]): |
269 | 255 | """ |
270 | 256 | Returns an MD5 hash of a file, or ``None`` if the file does not exist. |
271 | 257 | """ |
272 | 258 |
|
273 | | - _regexes = [ |
274 | | - r"^([a-zA-Z0-9]{32})\s+%s$", |
275 | | - r"^MD5\s+\(%s\)\s+=\s+([a-zA-Z0-9]{32})$", |
276 | | - ] |
277 | | - |
278 | | - def command(self, path): |
279 | | - self.path = path |
280 | | - return make_formatted_string_command( |
281 | | - "test -e {0} && ( md5sum {0} 2> /dev/null || md5 {0} ) || true", |
282 | | - QuoteString(path), |
283 | | - ) |
284 | | - |
285 | | - def process(self, output): |
286 | | - for regex in self._regexes: |
287 | | - regex = regex % re.escape(self.path) |
288 | | - matches = re.match(regex, output[0]) |
289 | | - if matches: |
290 | | - return matches.group(1) |
291 | | - |
292 | 259 |
|
293 | 260 | class FindInFile(FactBase): |
294 | 261 | """ |
|
0 commit comments