#!/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())