Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Build Plugin

@AkazaRenn AkazaRenn Aug 24, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This action builds and creates a prerelease with the tag using a timestamp like 20240824153302 (when the action runs), it would be triggered on every push on the main branch.
You can check my fork as an example https://github.com/AkazaRenn/decky-cloud-save/tags


on:
push:
branches:
- main
pull_request_target:
branches: ['*']

jobs:
build:
name: Build plugin
runs-on: ubuntu-latest

steps:
- name: Create variables
run: echo "TIMESTAMP=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV

- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
submodules: "recursive"
token: ${{ secrets.GITHUB_TOKEN }}

- name: Append hash to plugin version
run: |
echo "::notice::This run was triggered by a commit. Appending the commit hash to the plugin version."

PACKAGE_FILE="package.json"

SHA=$(cut -c1-7 <<< "${{ github.event.pull_request.head.sha || github.sha }}")
VERSION=$(jq -r '.version' $PACKAGE_FILE)
NEW_VERSION="$VERSION-$SHA"

echo "::notice::Going from $VERSION to $NEW_VERSION"

tmp=$(mktemp)
jq --arg newversion "$NEW_VERSION" '.version = $newversion' $PACKAGE_FILE > $tmp
mv $tmp $PACKAGE_FILE

echo "::endgroup::"

- uses: pnpm/action-setup@v3

- name: Update pnpm lockfile
run: |
$(which pnpm) install --lockfile-only

- name: Download Decky CLI
run: |
mkdir -p "$(pwd)"/cli
curl -L -o "$(pwd)"/cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/latest/download/decky-linux-x86_64"
chmod +x "$(pwd)"/cli/decky

- name: Build plugin
run: |
# Run the CLI as root to get around Docker's weird permissions
sudo .vscode/build.sh
sudo chown -R $(whoami) out

- name: Unzip plugin
run: |
for file in out/*.zip; do
echo "Unzipping $file"
unzip -qq "$file" -d out
done
tree out

- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: output
path: out/*/

- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TIMESTAMP }}
prerelease: true
files: out/*.zip
8 changes: 7 additions & 1 deletion assets/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,11 @@
"sync.logs": "Sync logs",
"no.available.logs": "No available logs",
"deleting.locks.sync": "Deleting locks and syncing",
"delete.locks": "Delete locks"
"delete.locks": "Delete locks",
"library.sync": "Library Sync",
"documents": "Documents",
"music": "Music",
"pictures": "Pictures",
"videos": "Videos",
"enabled": "Enabled"
}
7 changes: 4 additions & 3 deletions backend/rcloneLauncher
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash

PLUGIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CONFIG_DIR="$PLUGIN_DIR/../../../settings/decky-cloud-save"
BIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PLUGIN_DIR=$(realpath "$BIN_DIR"/..)
CONFIG_DIR=$(realpath "$PLUGIN_DIR"/../../settings/"$(basename "$PLUGIN_DIR")")

exec "$PLUGIN_DIR/rclone" "--config" "$CONFIG_DIR/rclone.conf" "$@"
exec "$BIN_DIR/rclone" "--config" "$CONFIG_DIR/rclone.conf" "$@"

@AkazaRenn AkazaRenn Aug 24, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure why but my own build uses Decky Cloud Save for the folder name, so I changed it to a more generic solution


1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "decky-cloud-save",
"version": "1.4.2",
"description": "Manage cloud saves for games that do not support it in [current year].",
"packageManager": "pnpm@9.8.0",
"scripts": {
"build": "shx rm -rf dist && rollup -c",
"watch": "rollup -c -w",
Expand Down
122 changes: 89 additions & 33 deletions py_modules/plugin_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from pathlib import Path
import decky_plugin
from settings import SettingsManager as settings_manager

# Plugin directories and files
plugin_dir = Path(decky_plugin.DECKY_PLUGIN_DIR)
Expand All @@ -12,43 +13,48 @@
cfg_syncpath_includes_file = config_dir / "sync_paths.txt"
cfg_syncpath_excludes_file = config_dir / "sync_paths_excludes.txt"
cfg_syncpath_filter_file = config_dir / "sync_paths_filter.txt"
cfg_property_file = config_dir / "plugin.properties"

def get_config():
config = settings_manager(name="config", settings_directory=decky_plugin.DECKY_PLUGIN_SETTINGS_DIR)

def get_cfg_property():
"""
Reads and parses the plugin configuration file.
Reads and parses the plugin.properties.
This is only used for migration from legacy config file and should not be used for any other purpose.

Returns:
list: A list of key-value pairs representing the configuration.
"""
cfg_property_file = config_dir / "plugin.properties"
if not cfg_property_file.is_file():
return []
with open(cfg_property_file) as f:
lines = f.readlines()
lines = list(map(lambda x: x.strip().split('='), lines))
decky_plugin.logger.debug("config %s", lines)
cfg_property_file.unlink()
return lines

def set_config(key: str, value: str):
def get_config():
"""
Retrieves the plugin configuration.

Returns:
dict: The plugin configuration.
"""
config.read()
return config.settings

def set_config(key: str, value):
"""
Sets a configuration key-value pair in the plugin configuration file.

Parameters:
key (str): The key to set.
value (str): The value to set for the key.
"""
with open(cfg_property_file, "r") as f:
lines = f.readlines()
with open(cfg_property_file, "w") as f:
found = False
for line in lines:
if line.startswith(key + '='):
f.write(f"{key}={value}\n")
found = True
else:
f.write(line)
if not found:
f.write(f"{key}={value}\n")

def get_config_item(name: str, default: str = None):
config.setSetting(key, value)

def get_config_item(name: str, default = None):
"""
Retrieves a configuration item by name.

Expand All @@ -59,7 +65,8 @@ def get_config_item(name: str, default: str = None):
Returns:
str: The value of the configuration item.
"""
return next((x[1] for x in get_config() if x[0] == name), default)
config.read()
return config.getSetting(name, default)

def regenerate_filter_file():
"""
Expand All @@ -82,6 +89,42 @@ def regenerate_filter_file():
f.write("\n")
f.write("- **\n")

def get_library_sync_config(key: str = None):
"""
Retrieves the library sync configuration.

Parameters:
key (str, optional): The key of the value to retrieve. If not provided, the entire configuration will be returned.

Returns:
dict: The library sync configuration.
"""
config.read()
if key:
return config.getSetting("library_sync", {}).get(key, {"enabled": False, "bisync": False, "destination": f"deck-libraries/{key}"})
else:
return config.getSetting("library_sync", {})

def set_library_sync_config(key: str, enabled: bool = None, bisync: bool = None, destination: str = None):
"""
Sets the library sync configuration.

Parameters:
key (str): The key to set.
enabled (bool): Whether the key is enabled.
destination (str): The destination of the key.
"""
config.read()
if enabled == None:
enabled = get_library_sync_config(key).get("enabled", False)
if bisync == None:
bisync = get_library_sync_config(key).get(key, {}).get("bisync", False)
if destination == None:
destination = get_library_sync_config(key).get(key, {}).get("destination", f"deck-libraries/{key}")
library_sync_config = get_library_sync_config()
library_sync_config.update({key: {"enabled": enabled, "bisync": bisync, "destination": destination}})
config.setSetting("library_sync", library_sync_config)

def migrate():
"""
Performs migration tasks if necessary, like creating directories and files, and setting default configurations.
Expand All @@ -92,23 +135,36 @@ def migrate():
cfg_syncpath_includes_file.touch()
if not cfg_syncpath_excludes_file.is_file():
cfg_syncpath_excludes_file.touch()
if not cfg_property_file.is_file():
cfg_property_file.touch()
if not cfg_syncpath_filter_file.is_file():
regenerate_filter_file()

config = get_config()
if not any(e[0] == "destination_directory" for e in config):
# Migrate from plugin.properties to config.json
config_pairs_list = get_cfg_property()
for pair in config_pairs_list:
if pair[1] == "true":
pair[1] = True
elif pair[1] == "false":
pair[1] = False
set_config(pair[0], pair[1])

# Set default configurations
current_config = get_config()
if not "destination_directory" in current_config:
set_config("destination_directory", "decky-cloud-save")
if not any(e[0] == "bisync_enabled" for e in config):
set_config("bisync_enabled", "false")
if not any(e[0] == "log_level" for e in config):
if not "bisync_enabled" in current_config:
set_config("bisync_enabled", False)
if not "log_level" in current_config:
set_config("log_level", "INFO")
if not any(e[0] == "sync_on_game_exit" for e in config):
set_config("sync_on_game_exit", "true")
if not any(e[0] == "toast_auto_sync" for e in config):
set_config("toast_auto_sync", "true")
if not any(e[0] == "additional_sync_args" for e in config):
set_config("additional_sync_args", "")
if not any(e[0] == "sync_root" for e in config):
if not "sync_on_game_exit" in current_config:
set_config("sync_on_game_exit", True)
if not "toast_auto_sync" in current_config:
set_config("toast_auto_sync", True)
if not "additional_sync_args" in current_config:
set_config("additional_sync_args", [])
if not "sync_root" in current_config:
set_config("sync_root", "/")
if not "library_sync" in current_config:
set_library_sync_config("Documents")
set_library_sync_config("Music")
set_library_sync_config("Pictures")
set_library_sync_config("Videos")
42 changes: 28 additions & 14 deletions py_modules/rclone_sync_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import os.path
from glob import glob
from pathlib import Path


class RcloneSyncManager:
Expand All @@ -20,41 +21,54 @@ async def delete_lock_files(self):
os.remove(hgx)

async def sync_now(self, winner: str, resync: bool):
for k, v in plugin_config.get_library_sync_config().items():
if v.get("enabled", False):
await self.sync_now_internal(
[str(Path.home() / k), f"backend:{v.get('destination', f'deck-libraries/{k}')}"],
v.get("bisync", False),
winner,
resync
)

sync_root = plugin_config.get_config_item("sync_root", "/")
destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save")
await self.sync_now_internal(
[sync_root, f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file],
plugin_config.get_config_item("bisync_enabled", False),
winner,
resync
)

async def sync_now_internal(self, path_args: list, bisync: bool, winner: str, resync: bool):
"""
Initiates a synchronization process using rclone.

Parameters:
path_args (list[str]): List of arguments for rclone, it contains the destination and filter path
winner (str): The winner of any conflicts during synchronization.
resync (bool): Whether to perform a resynchronization.

"""
args = []

bisync_enabled = plugin_config.get_config_item("bisync_enabled", "false") == "true"
destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save")
sync_root = plugin_config.get_config_item("sync_root", "/")

additional_args = [x for x in plugin_config.get_config_item("additional_sync_args", "").split(' ') if x]

if bisync_enabled:
if bisync:
args.extend(["bisync"])
decky_plugin.logger.debug("Using bisync")
else:
args.extend(["copy"])
decky_plugin.logger.debug("Using copy")

args.extend([sync_root, f"backend:{destination_path}", "--filter-from",
plugin_config.cfg_syncpath_filter_file, "--copy-links"])
if bisync_enabled:
args.extend(path_args)
args.extend(["--copy-links"])
if bisync:
if resync:
args.extend(["--resync-mode", winner, "--resync"])
else:
args.extend(["--conflict-resolve", winner])

args.extend(["--transfers", "8", "--checkers", "16", "--log-file",
decky_plugin.DECKY_PLUGIN_LOG, "--log-format", "none", "-v"])
args.extend(additional_args)

args.extend(plugin_config.get_config_item("additional_sync_args", []))

cmd = [plugin_config.rclone_bin, *args]

Expand All @@ -73,7 +87,7 @@ async def probe(self):
"""
if not self.current_sync:
return 0

if self.current_sync.returncode is not None:
decky_plugin.logger.info("=== FINISHING SYNC ===")

Expand Down
Loading