Skip to content

Commit

Permalink
Save files to stacks directory, Add GLOB_PATTERNS
Browse files Browse the repository at this point in the history
  • Loading branch information
EthanC committed Nov 9, 2024
1 parent 8140c6f commit 572a123
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 36 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Regardless of your chosen setup method, Salvage is intended for use with a task
- `GITHUB_ACCESS_TOKEN` (Required): [Personal Access Token (Classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic) for GitHub.
- `GITHUB_REPOSITORY` (Required): Name of the private GitHub repository to store backups.
- `DISCORD_WEBHOOK_URL`: [Discord Webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) URL to receive Portainer Stack notifications.
- `GLOB_PATTERNS`: Comma-separated [pathname pattern(s)](https://docs.python.org/3/library/glob.html) to match in local file discovery. Default is `**/compose.yaml`.

### Docker (Recommended)

Expand All @@ -39,6 +40,7 @@ services:
GITHUB_ACCESS_TOKEN: XXXXXXXX
GITHUB_REPOSITORY: XXXXXXXX
DISCORD_WEBHOOK_URL: https://discord.com/api/webhooks/XXXXXXXX/XXXXXXXX
GLOB_PATTERNS: **/compose.yaml,**/config.json
volumes:
- /home/username/stacks:/salvage/stacks:ro
```
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "salvage"
version = "2.1.0"
version = "3.0.0"
description = "Backup Docker Compose files to GitHub and notify about changes."
readme = "README.md"
requires-python = ">=3.13"
Expand Down
95 changes: 60 additions & 35 deletions salvage.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def Start() -> None:

if dotenv.load_dotenv():
logger.success("Loaded environment variables")
logger.trace(environ)
logger.trace(f"{environ=}")

if level := environ.get("LOG_LEVEL"):
logger.remove()
Expand All @@ -44,7 +44,7 @@ def Start() -> None:
)

logger.success(f"Enabled logging to Discord webhook")
logger.trace(url)
logger.trace(f"{url=}")

local: dict[str, dict[str, str]] = GetLocalFiles()

Expand Down Expand Up @@ -74,61 +74,82 @@ def Start() -> None:
def GetLocalFiles() -> dict[str, dict[str, str]]:
"""Return a dictionary containing files from the local stacks directory."""

stacks: dict[str, dict[str, str]] = {}
results: dict[str, dict[str, str]] = {}
directory: Path = Path("./stacks")

if not directory.exists():
logger.error(f"Failed to locate local stacks directory {directory.resolve()}")

return stacks
return results

for file in directory.glob("**/compose.yaml"):
stack: str = file.relative_to("./stacks").parts[0]
# Default pattern if GLOB_PATTERN environment variable is not set
patterns: list[str] = ["**/compose.yaml"]

stacks[stack] = {
"stack": stack,
"filename": file.name,
"filepath": str(file.relative_to("./stacks")).replace("\\", "/"),
"content": file.read_text(),
}
if custom := environ.get("GLOB_PATTERNS"):
patterns = custom.split(",")

logger.trace(f"{patterns=}")

logger.debug(f"Found file {stacks[stack]["filepath"]} in local stacks")
logger.trace(file.resolve())
logger.trace(stacks[stack])
for pattern in patterns:
logger.trace(f"{pattern=}")

logger.info(f"Found {len(stacks):,} files in local stacks")
for file in directory.glob(pattern):
logger.trace(f"{file=}")

return stacks
stack: str = file.relative_to("./stacks").parts[0]
filename: str = file.name

results[f"{stack}/{filename}"] = {
"stack": stack,
"filename": filename,
"filepath": str(file).replace("\\", "/"),
"content": file.read_text(),
}

logger.debug(
f"Found file {results[f"{stack}/{filename}"]["filepath"]} in local stacks"
)
logger.trace(file.resolve())
logger.trace(f"{results[f"{stack}/{filename}"]=}")

logger.info(f"Found {len(results):,} files in local stacks")
logger.trace(f"{results=}")

return results


def GetRemoteFiles(repo: Repository) -> dict[str, dict[str, str]]:
"""Return a dictionary containing files from the remote stacks directory."""

stacks: dict[str, dict[str, str]] = {}
results: dict[str, dict[str, str]] = {}
files: list[ContentFile] = GetFiles(repo)

for file in files:
stack: str = file.path.split("/")[0]
logger.trace(f"{file=}")

stack: str = file.path.split("/")[1]
filename: str = file.name

stacks[stack] = {
results[f"{stack}/{filename}"] = {
"stack": stack,
"filename": file.name,
"filename": filename,
"filepath": file.path,
"content": base64.b64decode(file.content).decode("UTF-8"),
"sha": file.sha,
}

logger.debug(
f"Found file {stacks[stack]["filepath"]} in GitHub repository {repo.full_name} stacks"
f"Found file {results[f"{stack}/{filename}"]["filepath"]} in GitHub repository {repo.full_name} stacks"
)
logger.trace(file.html_url)
logger.trace(stacks[stack])
logger.trace(f"{file.html_url=}")
logger.trace(f"{results[f"{stack}/{filename}"]=}")

logger.info(
f"Found {len(stacks):,} files in GitHub repository {repo.full_name} stacks"
f"Found {len(results):,} files in GitHub repository {repo.full_name} stacks"
)
logger.trace(f"{results=}")

return stacks
return results


def CompareFiles(
Expand All @@ -144,9 +165,11 @@ def CompareFiles(
remote files that are not present locally will be deleted.
"""

for stack in local:
new: dict[str, str] = local[stack]
old: dict[str, str] | None = remote.get(stack)
for file in local:
new: dict[str, str] = local[file]
old: dict[str, str] | None = remote.get(file)

logger.trace(f"{file=} {new=} {old=}")

if not old:
url: str | None = SaveFile(repo, new["filepath"], new["content"])
Expand Down Expand Up @@ -178,19 +201,21 @@ def CompareFiles(
f"Modified file {new["filepath"]} in GitHub repository {repo.full_name}"
)

for stack in remote:
if not local.get(stack):
for file in remote:
logger.trace(f"{file=}")

if not local.get(file):
url: str | None = DeleteFile(
repo, remote[stack]["filepath"], remote[stack]["sha"]
repo, remote[file]["filepath"], remote[file]["sha"]
)

if url:
remote[stack]["url"] = url
remote[file]["url"] = url

Notify(remote[stack], "Deleted")
Notify(remote[file], "Deleted")

logger.success(
f"Deleted file {remote[stack]["filepath"]} in GitHub repository {repo.full_name}"
f"Deleted file {remote[file]["filepath"]} in GitHub repository {repo.full_name}"
)


Expand Down

0 comments on commit 572a123

Please sign in to comment.