Skip to content

multi-directory backup support#200

Open
mlapaglia wants to merge 21 commits into
developfrom
multi-directory
Open

multi-directory backup support#200
mlapaglia wants to merge 21 commits into
developfrom
multi-directory

Conversation

@mlapaglia
Copy link
Copy Markdown
Owner

No description provided.

Copilot AI review requested due to automatic review settings February 22, 2026 01:48
@mlapaglia mlapaglia linked an issue Feb 22, 2026 that may be closed by this pull request
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 87.78626% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.60%. Comparing base (1de5809) to head (656092d).
⚠️ Report is 6 commits behind head on develop.

Files with missing lines Patch % Lines
src/borgitory/models/schemas.py 80.43% 9 Missing ⚠️
src/borgitory/utils/source_paths.py 76.47% 4 Missing ⚠️
src/borgitory/api/schedules.py 94.44% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #200      +/-   ##
===========================================
+ Coverage    79.51%   79.60%   +0.09%     
===========================================
  Files          147      148       +1     
  Lines        13871    13989     +118     
===========================================
+ Hits         11029    11136     +107     
- Misses        2842     2853      +11     
Flag Coverage Δ
e2e 0.00% <0.00%> (ø)
integration 46.16% <80.15%> (+0.30%) ⬆️
unit 70.86% <54.96%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for backing up multiple directories in a single backup operation. Instead of being limited to a single source path, users can now specify multiple source paths through a dynamic UI that allows adding/removing paths. The implementation stores multiple paths as a JSON array string in the existing source_path database field, maintaining backward compatibility with single-path legacy data.

Changes:

  • Added utility functions to parse and serialize source paths as JSON arrays while maintaining backward compatibility with single-path strings
  • Updated UI templates to support dynamic addition/removal of source path fields via HTMX
  • Modified schema validators to convert multi-value form submissions into JSON array strings
  • Updated backup task executor to parse and pass multiple paths to borg commands

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/borgitory/utils/source_paths.py New utility module for parsing/serializing source paths with legacy single-path and new JSON array format support
tests/utils/test_source_paths.py Comprehensive test suite for source path utility functions covering various edge cases
src/borgitory/templates/partials/schedules/source_paths_container.html Container template for dynamically managing multiple source path input fields
src/borgitory/templates/partials/schedules/source_path_field.html Individual source path field template with remove button
src/borgitory/templates/partials/schedules/edit_form.html Updated to use new multi-path UI instead of single path input
src/borgitory/templates/partials/schedules/create_form.html Updated to use new multi-path UI instead of single path input
src/borgitory/templates/partials/backups/manual_form.html Updated to use new multi-path UI instead of single path input
src/borgitory/services/scheduling/schedule_service.py Added logic to handle source_paths array in form data and serialize to JSON
src/borgitory/services/jobs/task_executors/backup_task_executor.py Updated to parse source_path string and pass all paths to borg create command
src/borgitory/models/schemas.py Added merge_source_paths validators to ScheduleCreate, ScheduleUpdate, and BackupRequest; removed ABSOLUTE_PATH_PATTERN validation from Schedule.source_path field
src/borgitory/api/schedules.py Added HTMX endpoints for dynamically adding/removing source path fields; updated edit form to parse stored paths for display

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/utils/source_paths.py Outdated
Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/services/scheduling/schedule_service.py Outdated
Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/api/schedules.py
Comment thread src/borgitory/services/jobs/task_executors/backup_task_executor.py Outdated
Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/templates/partials/backups/manual_form.html Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/borgitory/templates/partials/schedules/create_form.html Outdated
Comment thread src/borgitory/templates/partials/backups/manual_form.html Outdated
Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/services/scheduling/schedule_service.py Outdated
Comment thread src/borgitory/api/schedules.py
Comment thread src/borgitory/api/schedules.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

src/borgitory/services/task_definition_builder.py:46

  • patterns: List[str] = [] uses a mutable default value, which can leak state between calls if the list is ever mutated. Use None as the default and normalize to an empty list inside the function.
        ignore_lock: bool = False,
        patterns: List[str] = [],
    ) -> TaskDefinition:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/borgitory/services/task_definition_builder.py
Comment thread src/borgitory/models/schemas.py Outdated
Comment thread src/borgitory/models/database.py Outdated
Comment thread src/borgitory/utils/source_paths.py
Comment thread src/borgitory/alembic/versions/a1b2c3d4e5f6_rename_source_path_to_source_paths.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +107
Accepts a JSON array string, a plain list of strings, None, or empty string.
"""
if v is None or (isinstance(v, str) and not v.strip()):
return []
if isinstance(v, list):
return [p.strip() for p in v if isinstance(p, str) and p.strip()]
if isinstance(v, str):
return parse_source_paths(v)
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_coerce_source_paths_to_list currently treats a non-empty non-JSON string as [] (because it delegates to parse_source_paths, which returns [] unless the string is a JSON array). That means callers sending a legacy single path string (or mistakenly sending source_path instead of source_paths) can silently end up with no source paths and create empty/invalid backup tasks. Consider supporting a plain absolute path string as a single-item list (or explicitly rejecting non-JSON strings) and mapping legacy source_path into source_paths for backward compatibility.

Suggested change
Accepts a JSON array string, a plain list of strings, None, or empty string.
"""
if v is None or (isinstance(v, str) and not v.strip()):
return []
if isinstance(v, list):
return [p.strip() for p in v if isinstance(p, str) and p.strip()]
if isinstance(v, str):
return parse_source_paths(v)
Accepts:
- None or empty string: returns []
- Plain list of strings: normalized and filtered
- JSON array string: parsed via parse_source_paths
- Single absolute path string: treated as a one-item list
"""
# Treat None and empty/whitespace-only strings as no source paths
if v is None or (isinstance(v, str) and not v.strip()):
return []
# Already a list: normalize and filter to non-empty strings
if isinstance(v, list):
return [p.strip() for p in v if isinstance(p, str) and p.strip()]
# String input: support legacy single absolute path and JSON array strings
if isinstance(v, str):
s = v.strip()
if not s:
return []
# Legacy behavior: accept a single absolute path string
if re.match(ABSOLUTE_PATH_PATTERN, s):
return [s]
# Otherwise, expect a JSON array string
paths = parse_source_paths(s)
if not paths:
raise ValueError(
"source_paths must be either a JSON array of strings or a single absolute path string."
)
return paths
# Unsupported type: no valid source paths

Copilot uses AI. Check for mistakes.
Comment on lines +422 to +426
source_paths_value = backup_params.get("source_paths") or []
if isinstance(source_paths_value, list):
source_paths = source_paths_value
else:
source_paths = []
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_task_list currently drops backup_params['source_paths'] unless it is already a Python list. If any caller passes a JSON-encoded string (as the UI does in hidden fields) or a legacy single-path string, this will silently produce source_paths = [] and the backup task will run with no paths. Consider accepting a JSON array string (and/or a single string) here and coercing it to a list (e.g., via the shared parse_source_paths helper) to make this path more robust and backward-compatible.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +53
paths = _string_to_paths_list(value)
conn_val = json.dumps(paths) if paths else "[]"
connection.execute(
sa.text("UPDATE schedules SET source_paths_json = :val WHERE id = :id"),
{"val": conn_val, "id": row_id},
)
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration stores conn_val as a JSON-encoded string (via json.dumps) into a sa.JSON() column. That will persist a JSON string instead of a JSON array in some DBs/drivers, causing schedule.source_paths to come back as a string (breaking code that expects a list, e.g. list(schedule.source_paths) in the edit form context). Write the Python list directly to the JSON column (bind param should be the list), not a pre-dumped string.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Multi-directory backup

3 participants