#!/bin/env python3 """ Rclone helper script for - uploading files - uploading directories - downloading files - downloading directories better scriptifying of rclone calls for eg CLI IDEs :) """ from typing import Mapping, List, Optional, Any import sys import os import argparse import subprocess import json config_file = "" config_dir = "" def find_config_file() -> str: global config_file global config_dir if config_file != "": return config_file current_dir = os.getcwd() while config_file == "": for entry in os.scandir(current_dir): if entry.path.endswith("rclone.conf") and os.path.isfile(entry.path): config_file = entry.path config_dir = current_dir return config_file # no config file found, fall back to system if current_dir == "/": process = subprocess.run(["rclone", "config", "file"], capture_output=True, text=True) config_file = process.stdout.strip().splitlines()[1] config_dir = os.path.dirname(config_file) return config_file current_dir = os.path.dirname(current_dir) def get_config() -> Mapping[str, Mapping[str, Any]]: """Parse the rclone config""" process = subprocess.run(["rclone", "config", "dump", "--config", find_config_file()], capture_output=True, text=True) config = process.stdout.strip() return json.loads(config) config = get_config() def get_remote_config(remote: str = "") -> Mapping[str, Mapping[str, Any]]: """Get config for specific remote. Fall back to default if unspecified""" global config if remote != "" and remote != "@": return { "remote": remote, "config": config[remote], } else: if len(config.keys()) == 1: remote_name = list(config.keys())[0] return { "remote": remote_name, "config": config[remote_name] } else: for remote_name in config.keys(): remote_config = config[remote_name] if "rclone_ide_default" in remote_config.keys(): return { "remote": remote_name, "config": remote_config, } raise KeyError("No remote set as default") def build_cmd_local_path(config: Mapping[str, Any]) -> str: """Build the local path part for the rclone command""" global config_dir if "rclone_ide_local_path" not in config: raise KeyError("rclone_ide_local_path is not specified") if config["rclone_ide_local_path"] in [".", "./"]: return config_dir local_path = os.path.join(config_dir, config["rclone_ide_local_path"]) return local_path def build_cmd_remote_path(remote: str, config: Mapping[str, Any]) -> str: """Build the remote path part for the rclone command""" if "rclone_ide_remote_path" not in config: raise KeyError("rclone_ide_remote_path is not specified") return "{}:{}".format(remote, config["rclone_ide_remote_path"]) def build_cmd_config() -> str: """Build the config option for the rclone command""" global config_file return f"--config={config_file}" def build_cmd_exclude() -> List[str]: """Build the various exclude option parts for the rclone command.""" return "" def build_cmd_logging(log_level: str = "INFO") -> List[str]: """Build the option parts for logging for the rclone command""" log_dir_path = os.path.expanduser("~/.local/share/rclone-ide") if not os.path.isdir(log_dir_path): os.mkdir(log_dir_path) cmd = [ f"--log-level={log_level}", "--log-file={}".format(os.path.join(log_dir_path, "rclone.log")), ] return cmd def command_set_local_path(options: Mapping[str, Any]) -> None: """Set the config option for the given remote for the local path""" cmd = [ "rclone", "config", "update", options.remote, "rclone_ide_local_path", options.local_path, build_cmd_config(), ] if options.dry_run: print(" ".join(cmd)) else: subprocess.run(cmd) def command_set_remote_path(options: Mapping[str, Any]) -> None: """Set the config option for the given remote for the remote path""" cmd = [ "rclone", "config", "update", options.remote, "rclone_ide_remote_path", options.remote_path, build_cmd_config(), ] if options.dry_run: print(" ".join(cmd)) else: subprocess.run(cmd) def command_set_default_remote(options: Mapping[str, Any]) -> None: """Set the config option for the given remote to mark it as default""" cmd = [ "rclone", "config", "update", options.remote, "rclone_ide_default", "true", build_cmd_config(), ] if options.dry_run: print(" ".join(cmd)) else: with open(config_file, "r+") as file: lines = file.readlines() file.seek(0) for line in lines: if "rclone_ide_default =" not in line: file.write(line) file.truncate() subprocess.run(cmd) def command_copy_file(options: Mapping[str, Any]) -> None: """Copy specific file to remote""" remote_config = get_remote_config(options.remote) local_path = build_cmd_local_path(remote_config["config"]) # build relative path to file local_file_path = os.path.join( os.getcwd().replace(local_path, ""), options.filepath ) cmd = [ "rclone", "copy", os.path.join(local_path, local_file_path), os.path.join( build_cmd_remote_path(remote_config["remote"], remote_config["config"]), os.path.dirname(local_file_path), ), build_cmd_config(), *build_cmd_logging(), ] if options.dry_run: print(" ".join(cmd)) else: subprocess.run(cmd) parser = argparse.ArgumentParser( prog="rclone-ide" ) subparsers = parser.add_subparsers(dest="command") parser.add_argument("--dry-run", action="store_true") # TODO: add to all subparsers? parser_set_local_path = subparsers.add_parser("set-local-path") parser_set_local_path.add_argument("remote", choices=config.keys()) parser_set_local_path.add_argument("local_path") parser_set_remote_path = subparsers.add_parser("set-remote-path") parser_set_remote_path.add_argument("remote", choices=config.keys()) parser_set_remote_path.add_argument("remote_path") parser_set_default_remote = subparsers.add_parser("set-default-remote") parser_set_default_remote.add_argument("remote", choices=config.keys()) parser_copy_file = subparsers.add_parser("copy-file") parser_copy_file.add_argument("remote", help="rclone remote to upload to. @ = default remote", choices=[*config.keys(), "@"]) parser_copy_file.add_argument("filepath", help="cwd-relative path to file") args = parser.parse_args() if args.command: match args.command: case "set-local-path": command_set_local_path(args) case "set-remote-path": command_set_remote_path(args) case "set-default-remote": command_set_default_remote(args) case "copy-file": command_copy_file(args) else: parser.print_help()