diff options
-rw-r--r-- | README.md | 141 | ||||
-rw-r--r-- | syncthingmanager/__init__.py | 91 |
2 files changed, 200 insertions, 32 deletions
@@ -2,25 +2,126 @@ A command line tool for the Syncthing API. Designed to make setting up remote servers easier. (and for users who prefer the cli) -## Features -- Adding and removing devices -- Adding and removing folders -- Sharing folders -- More to come... +## Installation and configuration +###Requirements +- Python 3.4 or later +- setuptools and pip +- Syncthing 0.14.19 or later + +Make sure you have setuptools installed, clone the repository, and run +`python3 setup.py install` + +The configuration must be initialized with the Syncthing API key. +Usually this can be done automatically: +`stman configure`. If that doesn't work, get the API key from the GUI +or config.xml (in Syncthing's config directory), then run `stman configure apikey`. + +### Configuration syntax +If your Syncthing GUI/API is on a non-standard port, or not on localhost, +you will need to configure it manually. By default, `stman` will look for +settings at `~/.config/syncthingmanager/syncthingmanager.conf`. +A sample syncthingmanager.conf follows: + +``` +[DEFAULT] +name = localhost + +[localhost] +apikey = MafkDvpagX5J6oMzxm9HwDSXJPSQKPFS +hostname = localhost +port = 8384 + +[remote-device] +apikey = h9mifaKwDq3SSPPmgUuDjsrivFg3dtkK +hostname = some-host +port = 9001 +``` + +In this example, my default device is the one at localhost:8384. If I wanted +to send a command to the one at some-host:9001, it would look like +`stman --device remote-device ...` ## Usage -The first time you use `stman`, you must give it the Syncthing API key. -This can be found in the GUI or in the file `~/.config/syncthing/config.xml`. -Then run `stman configure APIKEY`. - -All commands are documented in `stman -h`. - -## TODO -- Untested on non-Linux platforms -- More commands should be implemented, for changing the settings of existing -folders and devices. -- Should be able to find the API key automatically in most cases -- Output of the device and folder listings could be prettier and more complete. -- Tests are ugly and minimal, make them more systematic and complete. -- You tell me! Open an issue and/or PR if you think a new command would be -useful, or something unexpected happens. +``` +$ stman device list +$HOME/.config/syncthingmanager/syncthingmanager.conf doesn't appear to be a valid path. Exiting. +# Autoconfiguration +$ stman configure +# List configured devices +$ stman device list +syncthingmanager-test This Device + ID: LYAB7ZG-XDVMAVM-OUZ7EAB-5N3UVWY-DXTFRJ4-U2MTHGQ-7TIBRJE-PC56BQ6 + +another-device Connected + At: # Address removed + Folders: dotest + ID: H2AJWNR-5VYNWKM-PS2L2EE-QJYBG2U-3IFN5XM-EKSIIKF-NVLAG2E-KIQE4AE +# List configured folders +$ stman folder list +Default Folder + Shared With: + Folder ID: default + Folder Path: /home/syncthing/Sync/ + +do-test + Shared With: another-device + Folder ID: dotest + Folder Path: /home/syncthing/stman-test/ +# Adding a device +$ stman device add MFZWI3D-BONSGYC-YLTMRWG-C43ENR5-QXGZDMM-FZWI3DP-BONSGYY-LTMRWAD -n yet-another-device -i + +$ stman device list +syncthingmanager-test This Device + ID: LYAB7ZG-XDVMAVM-OUZ7EAB-5N3UVWY-DXTFRJ4-U2MTHGQ-7TIBRJE-PC56BQ6 + +$ stman device add MFZWI3D-BONSGYC-YLTMRWG-C43ENR5-QXGZDMM-FZWI3DP-BONSGYY-LTMRWAD -n yet-another-device -i + +$ stman device list +syncthingmanager-test This Device + ID: LYAB7ZG-XDVMAVM-OUZ7EAB-5N3UVWY-DXTFRJ4-U2MTHGQ-7TIBRJE-PC56BQ6 + +sam-thinker Connected + At: 104.32.133.79:60249 + Folders: dotest + ID: H2AJWNR-5VYNWKM-PS2L2EE-QJYBG2U-3IFN5XM-EKSIIKF-NVLAG2E-KIQE4AE + +yet-another-device Not Connected + Folders: + ID: MFZWI3D-BONSGYC-YLTMRWG-C43ENR5-QXGZDMM-FZWI3DP-BONSGYY-LTMRWAD +# Share a folder with a device +$ stman folder share dotest yet-another-device +$ stman folder list +Default Folder + Shared With: + Folder ID: default + Folder Path: /home/syncthing/Sync/ + +do-test + Shared With: another-device, yet-another-device + Folder ID: dotest + Folder Path: /home/syncthing/stman-test/ +# Configure and view advanced options +$ stman folder versioning dotest simple --versions 15 +$ stman folder edit dotest -r 70 +$ stman folder info dotest +do-test + Shared With: another-device, yet-another-device + Folder ID: dotest + Folder Path: /home/syncthing/stman-test/ + Rescan Interval: 70 + File Pull Order: alphabetic + Versioning: simple + Keep Versions: 15 +``` + +Other commands are documented in `stman -h`, `stman command -h`, and so on. + + +## Notes +- On Windows, cmd.exe will print funny characters in place of colors. +PowerShell works fine. +- Some information shown in the GUI requires use of the Events API, which +isn't part of python-syncthing. I plan on creating Python bindings for it +and using the results, but haven't started yet. +- I chose to have the device list output be online first instead of +alphabetical. diff --git a/syncthingmanager/__init__.py b/syncthingmanager/__init__.py index 1ea5d94..65c9abb 100644 --- a/syncthingmanager/__init__.py +++ b/syncthingmanager/__init__.py @@ -56,11 +56,15 @@ class SyncthingManager(Syncthing): Args: devicestr (str): the string that may be a deviceID or configured device name. + Returns: dict: + id: The deviceID in modern format, or None if not recogized. + index: the index of the device in config['devices'] in the current configuration, or None if not configured. + folders: a list of folder IDs associated with the device.""" try: @@ -81,6 +85,7 @@ class SyncthingManager(Syncthing): for d in folder['devices']: if d['deviceID'] == device_id: folders.append(folder['id']) + break else: for index, device in enumerate(config['devices']): if device_id == device['deviceID']: @@ -90,6 +95,7 @@ class SyncthingManager(Syncthing): for d in folder['devices']: if device['deviceID'] == d['deviceID']: folders.append(folder['id']) + break return {'id': device_id, 'index': deviceindex, 'folders': folders, 'name': device_name} @@ -98,14 +104,23 @@ class SyncthingManager(Syncthing): returns some useful info about it. Looks for a matching ID first, only considers labels if none is found. Further, duplicate labels are not reported. The first matching label in the config is used. + Args: + folderstr (str): the folder ID or label + returns: + dict: + id: (str) the folder ID + index: (str) the index of the folder in the active configuration + label: (str) the folder label + devices: (list) the deviceIDs associated with the folder + None if no matching folder found """ config = self.system.config() for index, folder in enumerate(config['folders']): @@ -129,14 +144,21 @@ class SyncthingManager(Syncthing): def add_device(self, device_id, name='', address='', dynamic=False, introducer=False): """ Adds a device to the configuration and sets the configuration. + Args: + device_id (str): The device ID to be added. + name (str): The name of the device. default: ``''`` + dynamic (bool): Add the ``dynamic`` entry to the addresses. No effect if ``addresses`` is not specified. default: ``False`` + introducer (bool): Give the device the introducer flag. default: ``False`` + Returns: + None """ config = self.system.config() info = self.device_info(device_id) @@ -155,10 +177,15 @@ class SyncthingManager(Syncthing): def remove_device(self, devicestr): """Removes a device from the configuration and sets it. + Args: + devicestr (str): The device ID or name. + Returns: + None + Raises: ``SyncthingManagerError``: when the given device is not configured. """ config = self.system.config() @@ -171,13 +198,20 @@ class SyncthingManager(Syncthing): def edit_device(self, devicestr, prop, value): """Changes properties of a device's configuration. + Args: + devicestr (str): The device ID or name. + prop (str): the property as in the REST config documentaion + value: the new value of the property. Needs to be in a serializable format accepted by the API. + Returns: + None + Raises: ``SyncthingManagerError``: when the given device is not configured.""" config = self.system.config() info = self.device_info(devicestr) @@ -189,15 +223,21 @@ class SyncthingManager(Syncthing): def device_change_name(self, devicestr, name): """Set or change the name of a configured device. + Args: + devicestr (str): the device ID or current name. + name (str): the new device name.""" self.edit_device(devicestr, 'name', name) def device_add_address(self, devicestr, address): """Add an address to the device's list of addresses. + Args: + devicestr(str): the device ID or name. + address(str): a tcp://address to add. """ info = self.device_info(devicestr) @@ -224,19 +264,30 @@ class SyncthingManager(Syncthing): def add_folder(self, path, folderid, label='', foldertype='readwrite', rescan=60): """Adds a folder to the configuration and sets it. + Args: + path (str): a path to the folder to be configured, either absolute or relative to the cwd. + folderid (str): the string to identify the folder (must be same on every device) + label (str): the label used as an alternate, local name for the folder. + foldertype (str): see syncthing documentation... + rescan (int): the interval for scanning in seconds. + Returns: + None + Raises: + ``SyncthingManagerError``: when the path is invalid + ``SyncthingManagerError``: when a folder with identical label is already configured. """ config = self.system.config() @@ -253,7 +304,7 @@ class SyncthingManager(Syncthing): raise SyncthingManagerError("There was a problem with the path " "entered: " + path) folder = dict({'id': folderid, 'label': label, 'path': str(path), - 'type': foldertype, 'rescanIntervalS': rescan, 'fsync': True, + 'type': foldertype, 'rescanIntervalS': int(rescan), 'fsync': True, 'autoNormalize': True, 'maxConflicts': 10, 'pullerSleepS': 0, 'minDiskFreePct': 1}) config['folders'].append(folder) @@ -261,10 +312,14 @@ class SyncthingManager(Syncthing): def remove_folder(self, folderstr): """Removes a folder from the configuration and sets it. + Args: + folderstr (str): an item from user input that may be the folder ID or label. + Returns: + None""" info = self.folder_info(folderstr) if not info: @@ -277,12 +332,17 @@ class SyncthingManager(Syncthing): def share_folder(self, folderstr, devicestr): """ Adds a device to a folder's list of devices and sets the configuration. + Args: + folderstr (str): an item from user input that may be the folder ID or label. + devicestr (str): an item from user input that may be the device ID or name. + Returns: + None """ info = self.folder_info(folderstr) if not info: @@ -304,12 +364,17 @@ class SyncthingManager(Syncthing): def unshare_folder(self, folderstr, devicestr): """ Removes a device from a folder's list of devices and sets the configuration. + Args: + folderstr (str): an item from user input that may be the folder ID or label. + devicestr (str): an item from user input that may be the device ID or name. + Returns: + None """ info = self.folder_info(folderstr) if not info: @@ -540,7 +605,7 @@ def arguments(): configuration_parser.add_argument('--hostname', '-a', default='localhost', help="the hostname to use. default localhost.") configuration_parser.add_argument('--port', '-p', default='8384', - help="the port to use. Default 8384") + help="the port to use. Default 8384", type=int) configuration_parser.add_argument('--name', '-n', help="what to call this device. Defaults to the hostname.") configuration_parser.add_argument('--default', action='store_true', @@ -584,7 +649,7 @@ def arguments(): edit_device_parser.add_argument('-r', '--remove-address', metavar='ADDRESS', help='remove ADDRESS from the list of hosts') edit_device_parser.add_argument('-c', '--compression', metavar='SETTING', - help='the level of compression to use (always, metadata, or never)') + help='the level of compression to use', choices=['always', 'metadata', 'never']) edit_device_parser.add_argument('-i', '--introducer', action='store_true', help='set the device as an introducer') edit_device_parser.add_argument('-io', '--introducer-off', action='store_true', @@ -602,8 +667,8 @@ def arguments(): help="the folder ID. Must match the one used on all cluster devices.") add_folder_parser.add_argument('--label', '-l', help="a local name for the folder") add_folder_parser.add_argument('--foldertype', '-t', default='readwrite', - help="'readwrite' or 'readonly'. Default readwrite") - add_folder_parser.add_argument('--rescan-interval', '-r', default=60, + help="'readwrite' or 'readonly'. Default readwrite", choices=['readwrite', 'readonly']) + add_folder_parser.add_argument('--rescan-interval', '-r', default=60, type=int, help='time in seconds between scanning for changes. Default 60.') remove_folder_parser = folder_subparsers.add_parser('remove', @@ -627,13 +692,15 @@ def arguments(): edit_folder_parser.add_argument('--label', '-n', metavar='LABEL', help='the label to be set') edit_folder_parser.add_argument('--rescan', '-r', metavar='INTERVAL', - help="the time (in seconds) between scanning for changes") - edit_folder_parser.add_argument('--minfree', '-m', metavar='PERCENT', + help="the time (in seconds) between scanning for changes", type=int) + edit_folder_parser.add_argument('--minfree', '-m', metavar='PERCENT', type=int, help='percentage of space that should be available on the disk this folder resides') edit_folder_parser.add_argument('--type', '-t', metavar='TYPE', dest='folder_type', - help='readonly or readwrite') + help='readonly or readwrite', choices=['readonly', 'readwrite']) edit_folder_parser.add_argument('--order', '-o', metavar='ORDER', - help='see the Syncthing documentation for all options') + help='see the Syncthing documentation for all options', + choices=['random', 'alphabetic', 'smallestFirst', 'largestFirst', + 'oldestFirst', 'newestFirst']) edit_folder_parser.add_argument('--ignore-permissions', action='store_true', help='ignore file permissions. Normally used on non-Unix filesystems') edit_folder_parser.add_argument('--sync-permissions', action='store_true', @@ -645,12 +712,12 @@ def arguments(): folder_versioning_subparsers = folder_versioning_parser.add_subparsers(dest='versionparser_name', metavar='TYPE') trashcan_parser = folder_versioning_subparsers.add_parser('trashcan', help="move deleted files to .stversions") - trashcan_parser.add_argument('--cleanout', default='0', help="number of days to keep files in trash") + trashcan_parser.add_argument('--cleanout', default='0', help="number of days to keep files in trash", type=int) simple_parser = folder_versioning_subparsers.add_parser('simple', help="keep old versions of files in .stversions") - simple_parser.add_argument('--versions', default='5', help="the number of versions to keep") + simple_parser.add_argument('--versions', default='5', help="the number of versions to keep", type=int) staggered_parser = folder_versioning_subparsers.add_parser('staggered', help="specify a maximum age for versions") staggered_parser.add_argument('--maxage', metavar='MAXAGE', default='365', - help="the maximum time to keep a version, in days, 0=forever") + help="the maximum time to keep a version, in days, 0=forever", type=int) staggered_parser.add_argument('--path', metavar='PATH', default='', help="a custom path for storing versions") external_parser = folder_versioning_subparsers.add_parser('external', help="use a custom command for versioning") external_parser.add_argument('command', metavar='COMMAND', help='the command to run') |