Source code for pyutil.rclone

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Backup files using :ref:`rclone`.

======
rclone
======

.. rubric:: Requires

`rclone`_, a Golang package.


* Set up a simple single use case backup.
    * Realistically this should be more of the focus.
* However if it's not, then we could make a :class:`collections.defaultdict`
  that holds default values for each option.
  * Actually wouldn't :class:`configparser.ConfigParser` make more sense?
* In addition we could use :class:`collections.ChainMap()` to set
  precedence of `backupdir`.
* Expand :mod:`argparse` usage with :func:`argparse.fromfile_prefix_chars()`
  to emulate rsync's file input.

.. _`rclone`: https://rclone.org

"""
import argparse
import logging
import os
import shlex
import shutil
import subprocess
import sys

from pyutil.__about__ import __version__

LOG_LEVEL = "logging.WARNING"


[docs]def _parse_arguments(cwd=None, **kwargs): """Parse user-given arguments.""" if cwd is None: cwd = os.getcwd() parser = argparse.ArgumentParser( description="Automate usage of rclone for " "simple backup creation.") parser.add_argument(action='store', dest='src', default=cwd, metavar='source_dir', help="The source directory. Defaults to the cwd.") parser.add_argument( "dst", action='store', metavar='dest_directory', help="The folder that the files should be backed up to." "Can be a remote instance as well. See rclone.org for " "all accepted values for this parameter") # config = parser.add_subparsers( # help="Configure rclone. Additional options can't be specified;" # "however, :mod:`pyutil.rclone` will halt execution as rclone is configured." # ) parser.add_argument( '-ll', '--log_level', dest='log_level', metavar='log_level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Set the logging level') parser.add_argument('-f', '--follow', action='store_true', default=False, help="Follow symlinks.") parser.add_argument('-V', '--version', action='version', version='%(prog)s' + __version__) if len(sys.argv) == 1: parser.print_help() sys.exit() else: return parser.parse_args()
[docs]def _set_debugging(): """Enable debug logging.""" root = logging.getLogger(name=__name__) root.setLevel(logging.DEBUG) ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(message)s') ch.setFormatter(formatter) root.addHandler(ch)
[docs]def run(cmd, **kwargs): """Execute the required command in a subshell. First the command is splited used typical shell grammer. A new process is created, and from the resulting subprocess object, the :func:`subprocess.Popen().wait()`. This function returns the return code of split `cmd`, so any non-zero value will lead to a ``SystemExit`` with a passed value of ``returncode``. Parameters ---------- cmd : str The command to be called Returns ------- process.returncode : int The returncode from the process. """ cmd = shlex.split(cmd) logging.debug("Cmd is: " + str(cmd)) process = subprocess.Popen(cmd, kwargs) if process.wait(): raise SystemExit(process.returncode) else: return process.returncode
[docs]def _dir_checker(dir_): """Check that necessary directories exist. If the default `dst` doesn't exist, definitely create it. If the user provided `src` doesn't exist, crash without making one. It's more likely that they typed the src dir incorrectly rather than running the script aware of the fact that it is nonexistent. """ if os.path.isdir(dir_): return True else: sys.exit(str(dir_) + 'does not exist. Exiting.')
[docs]def rclone_base_case(src, dst): """Base case that all other functions build off of. This function shouldn't be executed directly; however, it serves as a good template detailing a function and useful command with parameters that rclone uses. For example, ``--follow`` is a flag that has conditionals associated it with it. There are situations in which one wants to follow symlinks and others that they don't. This command assumes a use case and configures it rclone for it properly. .. todo:: rclone takes an argument for user-agent Parameters ---------- src : str directory to clone files from dst : str destination to send files to. Can be configured as a local directory, a dropbox directory, a google drive folder or a google cloud storage bucket among many other things. """ cmd = ['rclone', 'copy', '--update', '--track-renames', src, dst] run(cmd)
[docs]def rclone_follow(dst, src): """Follow symlinks. Parameters ---------- src : str directory to clone files from dst : str destination to send files to. Can be configured as a local directory, a dropbox directory, a google drive folder or a google cloud storage bucket among many other things. .. See Also .. -------- .. :ref:`pyutil.rclone.rclone_base_case()` for a more detailed explanation """ cmd = [ 'rclone', 'copy', '--update', '--track-renames' '--copy-links', src, dst ] run(cmd)
# uhhh how do we implememt this
[docs]class CloudProvider(): """Emulate the provider rclone is syncing to.""" @property def url(self): logging.debug("URL was: " + self.url)
[docs] @url.setter def url(self): logging.debug("Old URL was: " + self.url) # set it logging.debug("New URL is: " + self.url)
[docs]def main(): """Receive user arguments and begin executing module appropriately.""" cwd = os.getcwd() args = _parse_arguments(cwd) try: log_level = args.log_level except AttributeError: # IndexError? logging.basicConfig(level=LOG_LEVEL) else: logging.basicConfig(level=log_level) if args.src: src = args.src else: src = cwd dst = args.dst if args.follow: rclone_follow(dst, src)
if __name__ == "__main__": # This feels like a necessary stop-gap if not shutil.which('rclone'): sys.exit('rclone not in $PATH. Exiting.') sys.exit(main())