aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md141
-rw-r--r--syncthingmanager/__init__.py91
2 files changed, 200 insertions, 32 deletions
diff --git a/README.md b/README.md
index a075b8d..ef7c83b 100644
--- a/README.md
+++ b/README.md
@@ -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')