dar-backup 0.8.0__tar.gz → 0.8.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.
Files changed (103) hide show
  1. {dar_backup-0.8.0/src/dar_backup → dar_backup-0.8.2}/Changelog.md +26 -0
  2. {dar_backup-0.8.0 → dar_backup-0.8.2}/PKG-INFO +2 -2
  3. {dar_backup-0.8.0 → dar_backup-0.8.2}/README.md +1 -1
  4. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/badges/badge_clones.json +1 -1
  5. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/clones.json +195 -25
  6. dar_backup-0.8.2/doc/weekly_clones.png +0 -0
  7. {dar_backup-0.8.0 → dar_backup-0.8.2/src/dar_backup}/Changelog.md +26 -0
  8. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/README.md +1 -1
  9. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/__about__.py +1 -1
  10. dar_backup-0.8.2/src/dar_backup/command_runner.py +246 -0
  11. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/dar_backup.py +5 -5
  12. dar_backup-0.8.0/doc/weekly_clones.png +0 -0
  13. dar_backup-0.8.0/src/dar_backup/command_runner.py +0 -133
  14. {dar_backup-0.8.0 → dar_backup-0.8.2}/.gitignore +0 -0
  15. {dar_backup-0.8.0 → dar_backup-0.8.2}/LICENSE +0 -0
  16. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/badges/README.md +0 -0
  17. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/badges/milestone_1000.txt +0 -0
  18. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/badges/milestone_500.txt +0 -0
  19. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/badges/milestone_badge.json +0 -0
  20. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/dev.md +0 -0
  21. {dar_backup-0.8.0 → dar_backup-0.8.2}/doc/doc.md +0 -0
  22. {dar_backup-0.8.0 → dar_backup-0.8.2}/packages/deb/README.md +0 -0
  23. {dar_backup-0.8.0 → dar_backup-0.8.2}/pyproject.toml +0 -0
  24. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/.darrc +0 -0
  25. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/__init__.py +0 -0
  26. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/clean_log.py +0 -0
  27. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/cleanup.py +0 -0
  28. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/config_settings.py +0 -0
  29. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/dar-backup.conf +0 -0
  30. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/dar-backup.conf.j2 +0 -0
  31. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/dar_backup_systemd.py +0 -0
  32. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/demo.py +0 -0
  33. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/demo_backup_def.j2 +0 -0
  34. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/exceptions.py +0 -0
  35. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/installer.py +0 -0
  36. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/manager.py +0 -0
  37. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/rich_progress.py +0 -0
  38. {dar_backup-0.8.0 → dar_backup-0.8.2}/src/dar_backup/util.py +0 -0
  39. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/SecretStorage-3.3.3.dist-info/LICENSE +0 -0
  40. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/LICENSE +0 -0
  41. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/black-25.1.0.dist-info/licenses/LICENSE +0 -0
  42. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/blib2to3/LICENSE +0 -0
  43. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/build-1.2.2.post1.dist-info/LICENSE +0 -0
  44. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/certifi-2025.4.26.dist-info/licenses/LICENSE +0 -0
  45. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/cffi-1.17.1.dist-info/LICENSE +0 -0
  46. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/charset_normalizer-3.4.2.dist-info/licenses/LICENSE +0 -0
  47. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/contourpy-1.3.2.dist-info/LICENSE +0 -0
  48. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/cryptography-45.0.3.dist-info/licenses/LICENSE +0 -0
  49. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/cycler-0.12.1.dist-info/LICENSE +0 -0
  50. {dar_backup-0.8.0/venv/lib/python3.12/site-packages/dar_backup-0.8.0.dist-info → dar_backup-0.8.2/venv/lib/python3.12/site-packages/dar_backup-0.8.2.dist-info}/licenses/LICENSE +0 -0
  51. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/filelock-3.18.0.dist-info/licenses/LICENSE +0 -0
  52. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/fonttools-4.58.2.dist-info/licenses/LICENSE +0 -0
  53. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/hyperlink-21.0.0.dist-info/LICENSE +0 -0
  54. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/id-1.5.0.dist-info/LICENSE +0 -0
  55. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/iniconfig-2.1.0.dist-info/licenses/LICENSE +0 -0
  56. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/jaraco.classes-3.4.0.dist-info/LICENSE +0 -0
  57. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/jaraco.context-6.0.1.dist-info/LICENSE +0 -0
  58. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/jaraco.functools-4.1.0.dist-info/LICENSE +0 -0
  59. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/jeepney-0.9.0.dist-info/licenses/LICENSE +0 -0
  60. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/keyring-25.6.0.dist-info/LICENSE +0 -0
  61. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/kiwisolver-1.4.8.dist-info/LICENSE +0 -0
  62. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/markdown_it_py-3.0.0.dist-info/LICENSE +0 -0
  63. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/matplotlib-3.10.3.dist-info/LICENSE +0 -0
  64. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/mdurl-0.1.2.dist-info/LICENSE +0 -0
  65. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/more_itertools-10.7.0.dist-info/LICENSE +0 -0
  66. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/mypy_extensions-1.1.0.dist-info/licenses/LICENSE +0 -0
  67. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/nh3-0.2.21.dist-info/licenses/LICENSE +0 -0
  68. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/numpy/ma/LICENSE +0 -0
  69. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/packaging-25.0.dist-info/licenses/LICENSE +0 -0
  70. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pandas-2.3.0.dist-info/LICENSE +0 -0
  71. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pathspec-0.12.1.dist-info/LICENSE +0 -0
  72. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pexpect-4.9.0.dist-info/LICENSE +0 -0
  73. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pillow-11.2.1.dist-info/licenses/LICENSE +0 -0
  74. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/platformdirs-4.3.8.dist-info/licenses/LICENSE +0 -0
  75. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE +0 -0
  76. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/LICENSE +0 -0
  77. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/ptyprocess-0.7.0.dist-info/LICENSE +0 -0
  78. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pycparser-2.22.dist-info/LICENSE +0 -0
  79. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pygments-2.19.1.dist-info/licenses/LICENSE +0 -0
  80. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pyparsing-3.2.3.dist-info/LICENSE +0 -0
  81. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pyproject_hooks-1.2.0.dist-info/LICENSE +0 -0
  82. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pytest-8.4.0.dist-info/licenses/LICENSE +0 -0
  83. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pytest_cov-6.1.1.dist-info/licenses/LICENSE +0 -0
  84. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/pytest_timeout-2.4.0.dist-info/licenses/LICENSE +0 -0
  85. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/python_dateutil-2.9.0.post0.dist-info/LICENSE +0 -0
  86. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/readme_renderer-44.0.dist-info/LICENSE +0 -0
  87. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/requests-2.32.3.dist-info/LICENSE +0 -0
  88. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/requests_toolbelt-1.0.0.dist-info/LICENSE +0 -0
  89. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/rfc3986-2.0.0.dist-info/LICENSE +0 -0
  90. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/rich-14.0.0.dist-info/LICENSE +0 -0
  91. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/shellingham-1.5.4.dist-info/LICENSE +0 -0
  92. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/six-1.17.0.dist-info/LICENSE +0 -0
  93. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/sniffio-1.3.1.dist-info/LICENSE +0 -0
  94. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/tomli_w-1.2.0.dist-info/LICENSE +0 -0
  95. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/tomlkit-0.13.2.dist-info/LICENSE +0 -0
  96. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/trove_classifiers-2025.5.9.12.dist-info/licenses/LICENSE +0 -0
  97. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/twine-6.1.0.dist-info/LICENSE +0 -0
  98. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/typing_extensions-4.14.0.dist-info/licenses/LICENSE +0 -0
  99. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/tzdata-2025.2.dist-info/licenses/LICENSE +0 -0
  100. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/virtualenv-20.31.2.dist-info/licenses/LICENSE +0 -0
  101. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE +0 -0
  102. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/zipp-3.22.0.dist-info/licenses/LICENSE +0 -0
  103. {dar_backup-0.8.0 → dar_backup-0.8.2}/venv/lib/python3.12/site-packages/zstandard-0.23.0.dist-info/LICENSE +0 -0
@@ -1,6 +1,32 @@
1
1
  <!-- markdownlint-disable MD024 -->
2
2
  # dar-backup Changelog
3
3
 
4
+ ## v2-beta-0.8.2 - 2025-07-17
5
+
6
+ Github link: [v2-beta-0.8.2](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.2/v2)
7
+
8
+ ### Added
9
+
10
+ - Security hardening: CommandRunner now performs strict command-line sanitization
11
+ - Disallows potentially dangerous characters (e.g. ;, &, |) in command arguments
12
+ - Prevents injection-style misuse when restoring specific files or invoking custom commands
13
+
14
+ - Documentation:
15
+ - New [README section](https://github.com/per2jensen/dar-backup?tab=readme-ov-file#limitations-on-file-names-with-special-characters) explains filename restrictions and safe workarounds (e.g. restoring directly with dar, if needed)
16
+ - Includes a Markdown table listing all disallowed characters
17
+
18
+ - Test suite:
19
+ - Existing test cases updated to comply with the new sanitization rules
20
+ - Additional tests ensure CommandRunner handles large binary output and edge cases cleanly
21
+
22
+ ## v2-beta-0.8.1 - 2025-07-16
23
+
24
+ Github link: [v2-beta-0.8.1](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.1/v2)
25
+
26
+ ### Added
27
+
28
+ - FIX: runner now logs an error and fills more data into the returned CommandResult.
29
+
4
30
  ## v2-beta-0.8.0 - 2025-06-13
5
31
 
6
32
  Github link: [v2-beta-0.8.0](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.0/v2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dar-backup
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
5
5
  Project-URL: GPG Public Key, https://keys.openpgp.org/search?q=dar-backup@pm.me
6
6
  Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
@@ -727,7 +727,7 @@ Description-Content-Type: text/markdown
727
727
  [![PyPI version](https://img.shields.io/pypi/v/dar-backup.svg)](https://pypi.org/project/dar-backup/)
728
728
  [![PyPI downloads](https://img.shields.io/badge/dynamic/json?color=blue&label=PyPI%20downloads&query=total&url=https%3A%2F%2Fraw.githubusercontent.com%2Fper2jensen%2Fdar-backup%2Fmain%2Fdownloads.json)](https://pypi.org/project/dar-backup/)
729
729
  [![# clones](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/badge_clones.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
730
- [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
730
+ [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png) <sub>🎯 Stats powered by [ClonePulse](https://github.com/per2jensen/clonepulse)</sub>
731
731
 
732
732
  The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
733
733
  the heavy lifting, together with the [parchive](https://github.com/Parchive/par2cmdline) suite in these scripts.
@@ -9,7 +9,7 @@
9
9
  [![PyPI version](https://img.shields.io/pypi/v/dar-backup.svg)](https://pypi.org/project/dar-backup/)
10
10
  [![PyPI downloads](https://img.shields.io/badge/dynamic/json?color=blue&label=PyPI%20downloads&query=total&url=https%3A%2F%2Fraw.githubusercontent.com%2Fper2jensen%2Fdar-backup%2Fmain%2Fdownloads.json)](https://pypi.org/project/dar-backup/)
11
11
  [![# clones](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/badge_clones.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
12
- [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
12
+ [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png) <sub>🎯 Stats powered by [ClonePulse](https://github.com/per2jensen/clonepulse)</sub>
13
13
 
14
14
  The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
15
15
  the heavy lifting, together with the [parchive](https://github.com/Parchive/par2cmdline) suite in these scripts.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "label": "# clones",
4
- "message": "1012",
4
+ "message": "1475",
5
5
  "color": "deeppink"
6
6
  }
@@ -13,8 +13,8 @@
13
13
  "label": "Daily max: 124"
14
14
  }
15
15
  ],
16
- "total_clones": 1012,
17
- "unique_clones": 513,
16
+ "total_clones": 1475,
17
+ "unique_clones": 838,
18
18
  "daily": [
19
19
  {
20
20
  "timestamp": "2025-05-04T00:00:00Z",
@@ -153,68 +153,238 @@
153
153
  },
154
154
  {
155
155
  "timestamp": "2025-05-31T00:00:00Z",
156
- "count": 13,
157
- "uniques": 7
156
+ "count": 8,
157
+ "uniques": 5
158
158
  },
159
159
  {
160
160
  "timestamp": "2025-06-01T00:00:00Z",
161
- "count": 31,
162
- "uniques": 17
161
+ "count": 25,
162
+ "uniques": 15
163
163
  },
164
164
  {
165
165
  "timestamp": "2025-06-02T00:00:00Z",
166
- "count": 21,
167
- "uniques": 10
166
+ "count": 15,
167
+ "uniques": 7
168
168
  },
169
169
  {
170
170
  "timestamp": "2025-06-03T00:00:00Z",
171
- "count": 62,
172
- "uniques": 33
171
+ "count": 57,
172
+ "uniques": 31
173
173
  },
174
174
  {
175
175
  "timestamp": "2025-06-04T00:00:00Z",
176
- "count": 45,
177
- "uniques": 26
176
+ "count": 35,
177
+ "uniques": 22
178
178
  },
179
179
  {
180
180
  "timestamp": "2025-06-05T00:00:00Z",
181
- "count": 24,
182
- "uniques": 17
181
+ "count": 15,
182
+ "uniques": 12
183
183
  },
184
184
  {
185
185
  "timestamp": "2025-06-06T00:00:00Z",
186
- "count": 45,
187
- "uniques": 23
186
+ "count": 33,
187
+ "uniques": 18
188
188
  },
189
189
  {
190
190
  "timestamp": "2025-06-07T00:00:00Z",
191
- "count": 60,
192
- "uniques": 25
191
+ "count": 49,
192
+ "uniques": 20
193
193
  },
194
194
  {
195
195
  "timestamp": "2025-06-08T00:00:00Z",
196
- "count": 43,
197
- "uniques": 23
196
+ "count": 33,
197
+ "uniques": 19
198
198
  },
199
199
  {
200
200
  "timestamp": "2025-06-09T00:00:00Z",
201
- "count": 28,
202
- "uniques": 17
201
+ "count": 18,
202
+ "uniques": 12
203
203
  },
204
204
  {
205
205
  "timestamp": "2025-06-10T00:00:00Z",
206
- "count": 34,
207
- "uniques": 21
206
+ "count": 26,
207
+ "uniques": 16
208
208
  },
209
209
  {
210
210
  "timestamp": "2025-06-11T00:00:00Z",
211
+ "count": 9,
212
+ "uniques": 6
213
+ },
214
+ {
215
+ "timestamp": "2025-06-12T00:00:00Z",
216
+ "count": 10,
217
+ "uniques": 8
218
+ },
219
+ {
220
+ "timestamp": "2025-06-13T00:00:00Z",
221
+ "count": 48,
222
+ "uniques": 18
223
+ },
224
+ {
225
+ "timestamp": "2025-06-14T00:00:00Z",
226
+ "count": 11,
227
+ "uniques": 9
228
+ },
229
+ {
230
+ "timestamp": "2025-06-15T00:00:00Z",
231
+ "count": 10,
232
+ "uniques": 8
233
+ },
234
+ {
235
+ "timestamp": "2025-06-16T00:00:00Z",
236
+ "count": 18,
237
+ "uniques": 11
238
+ },
239
+ {
240
+ "timestamp": "2025-06-17T00:00:00Z",
241
+ "count": 11,
242
+ "uniques": 9
243
+ },
244
+ {
245
+ "timestamp": "2025-06-18T00:00:00Z",
246
+ "count": 9,
247
+ "uniques": 7
248
+ },
249
+ {
250
+ "timestamp": "2025-06-19T00:00:00Z",
251
+ "count": 10,
252
+ "uniques": 8
253
+ },
254
+ {
255
+ "timestamp": "2025-06-20T00:00:00Z",
256
+ "count": 25,
257
+ "uniques": 18
258
+ },
259
+ {
260
+ "timestamp": "2025-06-21T00:00:00Z",
261
+ "count": 8,
262
+ "uniques": 6
263
+ },
264
+ {
265
+ "timestamp": "2025-06-22T00:00:00Z",
266
+ "count": 8,
267
+ "uniques": 6
268
+ },
269
+ {
270
+ "timestamp": "2025-06-23T00:00:00Z",
271
+ "count": 19,
272
+ "uniques": 14
273
+ },
274
+ {
275
+ "timestamp": "2025-06-24T00:00:00Z",
276
+ "count": 8,
277
+ "uniques": 6
278
+ },
279
+ {
280
+ "timestamp": "2025-06-25T00:00:00Z",
281
+ "count": 11,
282
+ "uniques": 8
283
+ },
284
+ {
285
+ "timestamp": "2025-06-26T00:00:00Z",
286
+ "count": 6,
287
+ "uniques": 5
288
+ },
289
+ {
290
+ "timestamp": "2025-06-27T00:00:00Z",
291
+ "count": 11,
292
+ "uniques": 7
293
+ },
294
+ {
295
+ "timestamp": "2025-06-28T00:00:00Z",
296
+ "count": 12,
297
+ "uniques": 9
298
+ },
299
+ {
300
+ "timestamp": "2025-06-29T00:00:00Z",
301
+ "count": 10,
302
+ "uniques": 7
303
+ },
304
+ {
305
+ "timestamp": "2025-06-30T00:00:00Z",
211
306
  "count": 20,
212
307
  "uniques": 13
213
308
  },
214
309
  {
215
- "timestamp": "2025-06-12T00:00:00Z",
310
+ "timestamp": "2025-07-01T00:00:00Z",
311
+ "count": 12,
312
+ "uniques": 10
313
+ },
314
+ {
315
+ "timestamp": "2025-07-02T00:00:00Z",
316
+ "count": 12,
317
+ "uniques": 10
318
+ },
319
+ {
320
+ "timestamp": "2025-07-03T00:00:00Z",
321
+ "count": 12,
322
+ "uniques": 10
323
+ },
324
+ {
325
+ "timestamp": "2025-07-04T00:00:00Z",
216
326
  "count": 18,
327
+ "uniques": 9
328
+ },
329
+ {
330
+ "timestamp": "2025-07-05T00:00:00Z",
331
+ "count": 21,
332
+ "uniques": 14
333
+ },
334
+ {
335
+ "timestamp": "2025-07-06T00:00:00Z",
336
+ "count": 20,
337
+ "uniques": 15
338
+ },
339
+ {
340
+ "timestamp": "2025-07-07T00:00:00Z",
341
+ "count": 26,
342
+ "uniques": 17
343
+ },
344
+ {
345
+ "timestamp": "2025-07-08T00:00:00Z",
346
+ "count": 14,
347
+ "uniques": 9
348
+ },
349
+ {
350
+ "timestamp": "2025-07-09T00:00:00Z",
351
+ "count": 19,
352
+ "uniques": 13
353
+ },
354
+ {
355
+ "timestamp": "2025-07-10T00:00:00Z",
356
+ "count": 18,
357
+ "uniques": 13
358
+ },
359
+ {
360
+ "timestamp": "2025-07-11T00:00:00Z",
361
+ "count": 22,
362
+ "uniques": 13
363
+ },
364
+ {
365
+ "timestamp": "2025-07-12T00:00:00Z",
366
+ "count": 19,
217
367
  "uniques": 12
368
+ },
369
+ {
370
+ "timestamp": "2025-07-13T00:00:00Z",
371
+ "count": 20,
372
+ "uniques": 13
373
+ },
374
+ {
375
+ "timestamp": "2025-07-14T00:00:00Z",
376
+ "count": 38,
377
+ "uniques": 21
378
+ },
379
+ {
380
+ "timestamp": "2025-07-15T00:00:00Z",
381
+ "count": 18,
382
+ "uniques": 12
383
+ },
384
+ {
385
+ "timestamp": "2025-07-16T00:00:00Z",
386
+ "count": 30,
387
+ "uniques": 18
218
388
  }
219
389
  ]
220
390
  }
Binary file
@@ -1,6 +1,32 @@
1
1
  <!-- markdownlint-disable MD024 -->
2
2
  # dar-backup Changelog
3
3
 
4
+ ## v2-beta-0.8.2 - 2025-07-17
5
+
6
+ Github link: [v2-beta-0.8.2](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.2/v2)
7
+
8
+ ### Added
9
+
10
+ - Security hardening: CommandRunner now performs strict command-line sanitization
11
+ - Disallows potentially dangerous characters (e.g. ;, &, |) in command arguments
12
+ - Prevents injection-style misuse when restoring specific files or invoking custom commands
13
+
14
+ - Documentation:
15
+ - New [README section](https://github.com/per2jensen/dar-backup?tab=readme-ov-file#limitations-on-file-names-with-special-characters) explains filename restrictions and safe workarounds (e.g. restoring directly with dar, if needed)
16
+ - Includes a Markdown table listing all disallowed characters
17
+
18
+ - Test suite:
19
+ - Existing test cases updated to comply with the new sanitization rules
20
+ - Additional tests ensure CommandRunner handles large binary output and edge cases cleanly
21
+
22
+ ## v2-beta-0.8.1 - 2025-07-16
23
+
24
+ Github link: [v2-beta-0.8.1](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.1/v2)
25
+
26
+ ### Added
27
+
28
+ - FIX: runner now logs an error and fills more data into the returned CommandResult.
29
+
4
30
  ## v2-beta-0.8.0 - 2025-06-13
5
31
 
6
32
  Github link: [v2-beta-0.8.0](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.0/v2)
@@ -9,7 +9,7 @@
9
9
  [![PyPI version](https://img.shields.io/pypi/v/dar-backup.svg)](https://pypi.org/project/dar-backup/)
10
10
  [![PyPI downloads](https://img.shields.io/badge/dynamic/json?color=blue&label=PyPI%20downloads&query=total&url=https%3A%2F%2Fraw.githubusercontent.com%2Fper2jensen%2Fdar-backup%2Fmain%2Fdownloads.json)](https://pypi.org/project/dar-backup/)
11
11
  [![# clones](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/badge_clones.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
12
- [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
12
+ [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png) <sub>🎯 Stats powered by [ClonePulse](https://github.com/per2jensen/clonepulse)</sub>
13
13
 
14
14
  The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
15
15
  the heavy lifting, together with the [parchive](https://github.com/Parchive/par2cmdline) suite in these scripts.
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.0"
1
+ __version__ = "0.8.2"
2
2
 
3
3
  __license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
4
4
  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
@@ -0,0 +1,246 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import subprocess
4
+ import logging
5
+ import traceback
6
+ import threading
7
+ import os
8
+ import re
9
+ import shlex
10
+ import sys
11
+ import tempfile
12
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
13
+ from typing import List, Optional, Union
14
+ from dar_backup.util import get_logger
15
+
16
+
17
+ def is_safe_arg(arg: str) -> bool:
18
+ """
19
+ Check if the argument is safe by rejecting dangerous shell characters.
20
+ """
21
+ return not re.search(r'[;&|><`$\n]', arg)
22
+
23
+
24
+ def sanitize_cmd(cmd: List[str]) -> List[str]:
25
+ """
26
+ Validate and sanitize a list of command-line arguments.
27
+ Ensures all elements are strings and do not contain dangerous shell characters.
28
+ Raises ValueError if any argument is unsafe.
29
+ """
30
+
31
+ if not isinstance(cmd, list):
32
+ raise ValueError("Command must be a list of strings")
33
+ for arg in cmd:
34
+ if not isinstance(arg, str):
35
+ raise ValueError(f"Invalid argument type: {arg} (must be string)")
36
+ if not is_safe_arg(arg):
37
+ raise ValueError(f"Unsafe argument detected: {arg}")
38
+ return cmd
39
+
40
+ def _safe_str(s):
41
+ if isinstance(s, bytes):
42
+ return f"<{len(s)} bytes of binary data>"
43
+ return s
44
+
45
+
46
+ class CommandResult:
47
+ def __init__(
48
+ self,
49
+ returncode: int,
50
+ stdout: Union[str, bytes],
51
+ stderr: Union[str, bytes],
52
+ stack: Optional[str] = None,
53
+ note: Optional[str] = None
54
+ ):
55
+ self.returncode = returncode
56
+ self.stdout = stdout
57
+ self.stderr = stderr
58
+ self.stack = stack
59
+ self.note = note
60
+
61
+
62
+
63
+ def __repr__(self):
64
+ return f"<CommandResult returncode={self.returncode}\nstdout={self.stdout}\nstderr={self.stderr}\nstack={self.stack}>"
65
+
66
+
67
+ def __str__(self):
68
+ return (
69
+ "CommandResult:\n"
70
+ f" Return code: {self.returncode}\n"
71
+ f" Note: {self.note if self.note else '<none>'}\n"
72
+ f" STDOUT: {_safe_str(self.stdout)}\n"
73
+ f" STDERR: {_safe_str(self.stderr)}\n"
74
+ f" Stacktrace: {self.stack if self.stack else '<none>'}"
75
+ )
76
+
77
+
78
+ class CommandRunner:
79
+ def __init__(
80
+ self,
81
+ logger: Optional[logging.Logger] = None,
82
+ command_logger: Optional[logging.Logger] = None,
83
+ default_timeout: int = 30
84
+ ):
85
+ self.logger = logger or get_logger()
86
+ self.command_logger = command_logger or get_logger(command_output_logger=True)
87
+ self.default_timeout = default_timeout
88
+
89
+ if not self.logger or not self.command_logger:
90
+ self.logger_fallback()
91
+
92
+
93
+ def logger_fallback(self):
94
+ """
95
+ Setup temporary log files
96
+ """
97
+ main_log = tempfile.NamedTemporaryFile(delete=False)
98
+ command_log = tempfile.NamedTemporaryFile(delete=False)
99
+
100
+ logger = logging.getLogger("command_runner_fallback_main_logger")
101
+ command_logger = logging.getLogger("command_runner_fallback_command_logger")
102
+ logger.setLevel(logging.DEBUG)
103
+ command_logger.setLevel(logging.DEBUG)
104
+
105
+ main_handler = logging.FileHandler(main_log.name)
106
+ command_handler = logging.FileHandler(command_log.name)
107
+
108
+ logger.addHandler(main_handler)
109
+ command_logger.addHandler(command_handler)
110
+
111
+ self.logger = logger
112
+ self.command_logger = command_logger
113
+ self.default_timeout = 30
114
+ self.logger.info("CommandRunner initialized with fallback loggers")
115
+ self.command_logger.info("CommandRunner initialized with fallback loggers")
116
+
117
+ print(f"[WARN] Using fallback loggers:\n Main log: {main_log.name}\n Command log: {command_log.name}", file=sys.stderr)
118
+
119
+
120
+
121
+ def run(
122
+ self,
123
+ cmd: List[str],
124
+ *,
125
+ timeout: Optional[int] = None,
126
+ check: bool = False,
127
+ capture_output: bool = True,
128
+ text: bool = True
129
+ ) -> CommandResult:
130
+ self._text_mode = text
131
+ timeout = timeout or self.default_timeout
132
+
133
+ cmd_sanitized = None
134
+
135
+ try:
136
+ cmd_sanitized = sanitize_cmd(cmd)
137
+ except ValueError as e:
138
+ stack = traceback.format_exc()
139
+ self.logger.error(f"Command sanitation failed: {e}")
140
+ return CommandResult(
141
+ returncode=-1,
142
+ note=f"Sanitizing failed: command: {' '.join(cmd)}",
143
+ stdout='',
144
+ stderr=str(e),
145
+ stack=stack,
146
+
147
+ )
148
+ finally:
149
+ cmd = cmd_sanitized
150
+
151
+ #command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
152
+ command = f"Executing command: {' '.join(shlex.quote(arg) for arg in cmd)} (timeout={timeout}s)"
153
+
154
+
155
+ self.command_logger.info(command)
156
+ self.logger.debug(command)
157
+
158
+ stdout_lines = []
159
+ stderr_lines = []
160
+
161
+ try:
162
+ process = subprocess.Popen(
163
+ cmd,
164
+ stdout=subprocess.PIPE if capture_output else None,
165
+ stderr=subprocess.PIPE if capture_output else None,
166
+ text=False,
167
+ bufsize=-1
168
+ )
169
+ except Exception as e:
170
+ stack = traceback.format_exc()
171
+ return CommandResult(
172
+ returncode=-1,
173
+ stdout='',
174
+ stderr=str(e),
175
+ stack=stack
176
+ )
177
+
178
+ def stream_output(stream, lines, level):
179
+ try:
180
+ while True:
181
+ chunk = stream.read(1024)
182
+ if not chunk:
183
+ break
184
+ if self._text_mode:
185
+ decoded = chunk.decode('utf-8', errors='replace')
186
+ lines.append(decoded)
187
+ self.command_logger.log(level, decoded.strip())
188
+ else:
189
+ lines.append(chunk)
190
+ # Avoid logging raw binary data to prevent garbled logs
191
+ except Exception as e:
192
+ self.logger.warning(f"stream_output decode error: {e}")
193
+ finally:
194
+ stream.close()
195
+
196
+
197
+
198
+ threads = []
199
+ if capture_output and process.stdout:
200
+ t_out = threading.Thread(target=stream_output, args=(process.stdout, stdout_lines, logging.INFO))
201
+ t_out.start()
202
+ threads.append(t_out)
203
+ if capture_output and process.stderr:
204
+ t_err = threading.Thread(target=stream_output, args=(process.stderr, stderr_lines, logging.ERROR))
205
+ t_err.start()
206
+ threads.append(t_err)
207
+
208
+ try:
209
+ process.wait(timeout=timeout)
210
+ except subprocess.TimeoutExpired:
211
+ process.kill()
212
+ self.logger.error(f"Command timed out: {' '.join(cmd)}")
213
+ return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines))
214
+ except Exception as e:
215
+ stack = traceback.format_exc()
216
+ self.logger.error(f"Command execution failed: {' '.join(cmd)} with error: {e}")
217
+ return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines), stack)
218
+
219
+ for t in threads:
220
+ t.join()
221
+
222
+
223
+
224
+ if self._text_mode:
225
+ stdout_combined = ''.join(stdout_lines)
226
+ stderr_combined = ''.join(stderr_lines)
227
+ else:
228
+ stdout_combined = b''.join(stdout_lines)
229
+ stderr_combined = b''.join(stderr_lines)
230
+
231
+
232
+ if check and process.returncode != 0:
233
+ self.logger.error(f"Command failed with exit code {process.returncode}")
234
+ return CommandResult(
235
+ process.returncode,
236
+ stdout_combined,
237
+ stderr_combined,
238
+ stack=traceback.format_stack()
239
+ )
240
+
241
+ return CommandResult(
242
+ process.returncode,
243
+ stdout_combined,
244
+ stderr_combined
245
+ )
246
+
@@ -248,7 +248,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
248
248
  PermissionError: If a permission error occurs while comparing files.
249
249
  """
250
250
  result = True
251
- command = ['dar', '-t', backup_file, '-Q']
251
+ command = ['dar', '-t', backup_file, '-N', '-Q']
252
252
 
253
253
 
254
254
  log_basename = os.path. dirname(config_settings.logfile_location)
@@ -315,7 +315,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
315
315
  for restored_file_path in random_files:
316
316
  try:
317
317
  args.verbose and logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
318
- command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-Q', '-B', args.darrc, 'restore-options']
318
+ command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '--noconf', '-Q', '-B', args.darrc, 'restore-options']
319
319
  args.verbose and logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
320
320
  process = runner.run(command, timeout = config_settings.command_timeout_secs)
321
321
  if process.returncode != 0:
@@ -347,7 +347,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
347
347
  results: List[tuple] = []
348
348
  try:
349
349
  backup_file = os.path.join(config_settings.backup_dir, backup_name)
350
- command = ['dar', '-x', backup_file, '-Q', '-D']
350
+ command = ['dar', '-x', backup_file, '--noconf', '-Q', '-D']
351
351
  if restore_dir:
352
352
  if not os.path.exists(restore_dir):
353
353
  os.makedirs(restore_dir)
@@ -390,7 +390,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
390
390
  logger.debug(f"Getting backed up files in xml from DAR archive: '{backup_name}'")
391
391
  backup_path = os.path.join(backup_dir, backup_name)
392
392
  try:
393
- command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
393
+ command = ['dar', '-l', backup_path, '--noconf', '-am', '-as', "-Txml" , '-Q']
394
394
  logger.debug(f"Running command: {' '.join(map(shlex.quote, command))}")
395
395
  command_result = runner.run(command)
396
396
  # Parse the XML data
@@ -418,7 +418,7 @@ def list_contents(backup_name, backup_dir, selection=None):
418
418
  backup_path = os.path.join(backup_dir, backup_name)
419
419
 
420
420
  try:
421
- command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
421
+ command = ['dar', '-l', backup_path, '--noconf', '-am', '-as', '-Q']
422
422
  if selection:
423
423
  selection_criteria = shlex.split(selection)
424
424
  command.extend(selection_criteria)
Binary file
@@ -1,133 +0,0 @@
1
- # SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
- import subprocess
4
- import logging
5
- import threading
6
- import os
7
- import sys
8
- import tempfile
9
- sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
10
- from typing import List, Optional
11
- from dar_backup.util import get_logger
12
-
13
-
14
- class CommandResult:
15
- def __init__(self, returncode: int, stdout: str, stderr: str):
16
- self.returncode = returncode
17
- self.stdout = stdout
18
- self.stderr = stderr
19
-
20
- def __repr__(self):
21
- return f"<CommandResult returncode={self.returncode}>"
22
-
23
-
24
- class CommandRunner:
25
- def __init__(
26
- self,
27
- logger: Optional[logging.Logger] = None,
28
- command_logger: Optional[logging.Logger] = None,
29
- default_timeout: int = 30
30
- ):
31
- self.logger = logger or get_logger()
32
- self.command_logger = command_logger or get_logger(command_output_logger=True)
33
- self.default_timeout = default_timeout
34
-
35
- if not self.logger or not self.command_logger:
36
- self.logger_fallback()
37
-
38
- def logger_fallback(self):
39
- """
40
- Setup temporary log files
41
- """
42
- main_log = tempfile.NamedTemporaryFile(delete=False)
43
- command_log = tempfile.NamedTemporaryFile(delete=False)
44
-
45
- logger = logging.getLogger("command_runner_fallback_main_logger")
46
- command_logger = logging.getLogger("command_runner_fallback_command_logger")
47
- logger.setLevel(logging.DEBUG)
48
- command_logger.setLevel(logging.DEBUG)
49
-
50
- main_handler = logging.FileHandler(main_log.name)
51
- command_handler = logging.FileHandler(command_log.name)
52
-
53
- logger.addHandler(main_handler)
54
- command_logger.addHandler(command_handler)
55
-
56
- self.logger = logger
57
- self.command_logger = command_logger
58
- self.default_timeout = 30
59
- self.logger.info("CommandRunner initialized with fallback loggers")
60
- self.command_logger.info("CommandRunner initialized with fallback loggers")
61
-
62
- print(f"[WARN] Using fallback loggers:\n Main log: {main_log.name}\n Command log: {command_log.name}", file=sys.stderr)
63
-
64
-
65
-
66
- def run(
67
- self,
68
- cmd: List[str],
69
- *,
70
- timeout: Optional[int] = None,
71
- check: bool = False,
72
- capture_output: bool = True,
73
- text: bool = True
74
- ) -> CommandResult:
75
- timeout = timeout or self.default_timeout
76
-
77
- #log the command to be executed
78
- command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
79
- self.command_logger.info(command) # log to command logger
80
- self.logger.debug(command) # log to main logger if "--log-level debug"
81
-
82
- process = subprocess.Popen(
83
- cmd,
84
- stdout=subprocess.PIPE if capture_output else None,
85
- stderr=subprocess.PIPE if capture_output else None,
86
- text=False,
87
- bufsize=-1
88
- )
89
-
90
- stdout_lines = []
91
- stderr_lines = []
92
-
93
-
94
- def stream_output(stream, lines, level):
95
- try:
96
- while True:
97
- chunk = stream.read(1024)
98
- if not chunk:
99
- break
100
- decoded = chunk.decode('utf-8', errors='replace')
101
- lines.append(decoded)
102
- self.command_logger.log(level, decoded.strip())
103
- except Exception as e:
104
- self.logger.warning(f"stream_output decode error: {e}")
105
- finally:
106
- stream.close()
107
-
108
-
109
-
110
- threads = []
111
- if capture_output and process.stdout:
112
- t_out = threading.Thread(target=stream_output, args=(process.stdout, stdout_lines, logging.INFO))
113
- t_out.start()
114
- threads.append(t_out)
115
- if capture_output and process.stderr:
116
- t_err = threading.Thread(target=stream_output, args=(process.stderr, stderr_lines, logging.ERROR))
117
- t_err.start()
118
- threads.append(t_err)
119
-
120
- try:
121
- process.wait(timeout=timeout)
122
- except subprocess.TimeoutExpired:
123
- process.kill()
124
- self.logger.error(f"Command timed out: {' '.join(cmd)}")
125
- return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines))
126
-
127
- for t in threads:
128
- t.join()
129
-
130
- if check and process.returncode != 0:
131
- self.logger.error(f"Command failed with exit code {process.returncode}")
132
-
133
- return CommandResult(process.returncode, ''.join(stdout_lines), ''.join(stderr_lines))
File without changes
File without changes
File without changes
File without changes
File without changes