darkskysync 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ # Copyright (C) 2013 ETH Zurich, Institute of Astronomy
2
+
3
+ """
4
+ Created on Sep 24, 2013
5
+
6
+ @author: J. Akeret
7
+ """
8
+
9
+
10
+ import os
11
+ from abc import ABCMeta, abstractmethod
12
+
13
+ from darkskysync.Exceptions import FileSystemError
14
+
15
+ """
16
+ Created on Sep 30, 2013
17
+
18
+ @author: J. Akeret
19
+ """
20
+
21
+
22
+ class AbstractRemoteSyncAdapter:
23
+ """
24
+ Abstract class for all synchronization adapters
25
+ """
26
+
27
+ __metaclass__ = ABCMeta
28
+
29
+ @abstractmethod
30
+ def loadFiles(self, files):
31
+ """
32
+ Loads files from the remote location
33
+ """
34
+ pass
35
+
36
+ def _prepareDestination(self, path):
37
+ if not os.path.isdir(path):
38
+ # We do not create *all* the parents, but we do create the
39
+ # directory if we can.
40
+ try:
41
+ os.makedirs(path)
42
+ except OSError as ex:
43
+ raise FileSystemError(
44
+ "Unable to create storage directory: " "%s" % (str(ex))
45
+ )
@@ -0,0 +1,251 @@
1
+ # Copyright (C) 2013 ETH Zurich, Institute of Astronomy
2
+
3
+ """
4
+ Created on Sep 24, 2013
5
+
6
+ @author: J. Akeret
7
+ """
8
+
9
+
10
+ import importlib.resources
11
+ import os
12
+ import shutil
13
+ import sys
14
+
15
+ import darkskysync
16
+
17
+ from .DataSourceFactory import DataSourceFactory
18
+ from .Exceptions import (
19
+ ConfigurationError,
20
+ ConnectionError,
21
+ FileSystemError,
22
+ IllegalArgumentException,
23
+ )
24
+ from .LocalSyncAdapter import LocalSyncAdapter
25
+ from .RsyncWrapper import RsyncWrapper
26
+ from .SSHClientAdapter import SSHClientAdapter
27
+
28
+
29
+ class DarkSkySync:
30
+ """
31
+ Helps to access and synchronize with a remote data storage. Features are:
32
+ - lists the available file structure on the remote host
33
+ - lists the file structure in the local cache
34
+ - synchronizes the local cache with the remote file structure
35
+ - allows for removing files from the local file system
36
+
37
+ It delegates the remote access to the according wrappers (currently either scp over
38
+ ssh or rsync over ssh) and manages the local file structure
39
+
40
+ :param template: [optional] alternate name for the config template to use
41
+ :param configFile: [optional] alternate config file to use
42
+ :param verbose: [optional] flag to enable verbose mode
43
+
44
+ :raises ConfigurationError: error while reading the config file
45
+ """
46
+
47
+ REMOTE_WRAPPER_TYPE_MAP = {"rsync": SSHClientAdapter, "ssh": SSHClientAdapter}
48
+
49
+ REMOTE_SYNC_WRAPPER_TYPE_MAP = {
50
+ "rsync": RsyncWrapper,
51
+ "ssh": SSHClientAdapter,
52
+ "local": LocalSyncAdapter,
53
+ }
54
+
55
+ DEFAULT_TEMPLATE = "master"
56
+
57
+ def __init__(self, template=DEFAULT_TEMPLATE, configFile=None, verbose=False):
58
+ """
59
+ Constructor
60
+ """
61
+ self.configFile = configFile
62
+
63
+ if template is None:
64
+ template = DarkSkySync.DEFAULT_TEMPLATE
65
+
66
+ self._verifyConfigFile()
67
+
68
+ self.verbose = verbose
69
+ if self.verbose:
70
+ darkskysync.logger.info("Using config file: %s" % self.configFile)
71
+
72
+ dsFactory = DataSourceFactory.fromConfig(self.configFile)
73
+ self.dataSourceConfig = dsFactory.createDataSource(template)
74
+
75
+ def avail(self, path=None):
76
+ """
77
+ Lists the available file structure on the remote host
78
+
79
+ :param path: [optional] sub path to directory structure to check
80
+
81
+ :return: list of file available on remote host
82
+
83
+ :raises ConnectionError: remote host could not be accessed
84
+ """
85
+
86
+ try:
87
+ RemoteWrapper = DarkSkySync.REMOTE_WRAPPER_TYPE_MAP[
88
+ self.dataSourceConfig.remote.type
89
+ ]
90
+
91
+ remoteWrapper = RemoteWrapper(self.dataSourceConfig)
92
+ fileList = remoteWrapper.getRemoteFilesList(path)
93
+ return self._filterFileList(fileList)
94
+ except ConnectionError as ex:
95
+ darkskysync.logger.error(str(ex))
96
+ raise ex
97
+
98
+ def list(self, path=None, recursive=False):
99
+ """
100
+ Lists the available files in the local cache
101
+
102
+ :param path: [optional] sub path to directory structure to check
103
+ :param recursive: [optional] flag indicating if the cache should be browsed
104
+ recursively
105
+
106
+ :return: list of files in the local cache
107
+
108
+ """
109
+
110
+ filePath = self.dataSourceConfig.local.filePath
111
+ if path is not None:
112
+ filePath = os.path.join(filePath, path)
113
+
114
+ fileList = []
115
+
116
+ if not os.path.exists(filePath):
117
+ return fileList
118
+
119
+ if recursive:
120
+ for dirpath, dirnames, filenames in os.walk(filePath):
121
+ for fileName in filenames:
122
+ fileList.append(os.path.join(dirpath, fileName))
123
+ else:
124
+ pathContent = os.listdir(filePath)
125
+ fileList = [os.path.join(filePath, file) for file in pathContent]
126
+
127
+ return fileList
128
+
129
+ def load(self, names, dry_run=False, force=False):
130
+ """
131
+ Loads the given file names. Checks first if the requested files are available on
132
+ the file system. If not, they will be downloaded from the remote host.
133
+
134
+ :param names: a list of names which should be loaded
135
+ :param dry_run: [optional] flag indicating a dry run. No files will be
136
+ downloaded
137
+ :param force: [optional] flag indicating that the files should be downloaded
138
+ even if they already exist on the local file system
139
+
140
+ :retun: list of files loaded from the remote host
141
+
142
+ :raises ConnectionError: remote host could not be accessed
143
+ :raises FileSystemError: error while accesing the local file system
144
+
145
+ """
146
+
147
+ try:
148
+ RemoteWrapper = DarkSkySync.REMOTE_SYNC_WRAPPER_TYPE_MAP[
149
+ self.dataSourceConfig.remote.type
150
+ ]
151
+
152
+ remoteWrapper = RemoteWrapper(
153
+ self.dataSourceConfig,
154
+ dry_run=dry_run,
155
+ force=force,
156
+ verbose=self.verbose,
157
+ )
158
+ loadedFiles = remoteWrapper.loadFiles(names)
159
+ return self._postProcessFileList(loadedFiles)
160
+ except ConnectionError as ex:
161
+ darkskysync.logger.error(str(ex))
162
+ raise ex
163
+ except FileSystemError as ex:
164
+ darkskysync.logger.error(str(ex))
165
+ raise ex
166
+
167
+ def remove(self, files=None, allFiles=False):
168
+ """
169
+ Removes files and filetrees from the local cache
170
+
171
+ :param files: [optional] list of files or directories to delete
172
+ :param allFiles: [optional] flag indicating that all files shoudl be removed
173
+ from the cache
174
+
175
+ :raises FileSystemError: error while accessing the local file system
176
+ :raises IllegalArgumentException: Illegal parameter combination
177
+
178
+ """
179
+ if files is None and not allFiles:
180
+ darkskysync.logger.warning("At least a file has to be given")
181
+ raise IllegalArgumentException("At least a file has to be given")
182
+
183
+ if files is not None:
184
+ if isinstance(files, str):
185
+ files = [files]
186
+ for file in files:
187
+ path = os.path.join(self.dataSourceConfig.local.filePath, file)
188
+ if self.verbose:
189
+ darkskysync.logger.info("Removing: %s" % path)
190
+
191
+ if os.path.exists(path):
192
+ if os.path.isfile(path):
193
+ os.remove(path)
194
+ else:
195
+ shutil.rmtree(path)
196
+
197
+ if allFiles:
198
+ path = self.dataSourceConfig.local.filePath
199
+ if self.verbose:
200
+ darkskysync.logger.info("Removing content of: %s" % path)
201
+ for file in os.listdir(path):
202
+ filePath = os.path.join(path, file)
203
+ try:
204
+ if os.path.isfile(filePath):
205
+ os.unlink(filePath)
206
+ else:
207
+ shutil.rmtree(filePath)
208
+ except Exception as e:
209
+ darkskysync.logger.warn(e)
210
+ raise FileSystemError(e)
211
+
212
+ def _verifyConfigFile(self):
213
+ if self.configFile is None:
214
+ self.configFile = DataSourceFactory.DEFAULT_CONFIGURATION_FILE
215
+ if not os.path.isfile(self.configFile):
216
+ if not os.path.exists(os.path.dirname(self.configFile)):
217
+ os.mkdir(os.path.dirname(self.configFile))
218
+
219
+ with importlib.resources.as_file(
220
+ importlib.resources.files(__package__).joinpath(
221
+ "data/config.template"
222
+ )
223
+ ) as template:
224
+ darkskysync.logger.warning(
225
+ "Deploying default configuration file to %s.", self.configFile
226
+ )
227
+ shutil.copyfile(template, self.configFile)
228
+
229
+ else:
230
+ # Exit if supplied configuration file does not exists.
231
+ if not os.path.isfile(self.configFile):
232
+ sys.stderr.write(
233
+ "Unable to read configuration file `%s`.\n" % self.configFile
234
+ )
235
+ raise ConfigurationError(
236
+ "Unable to read configuration file `%s`.\n" % self.configFile
237
+ )
238
+
239
+ def _filterFileList(self, fileList):
240
+ fileList = [f for f in fileList if not f.startswith(".")]
241
+ fileList.sort()
242
+ return fileList
243
+
244
+ def _postProcessFileList(self, fileList):
245
+ files = []
246
+ for filePath in fileList:
247
+ if os.path.exists(filePath):
248
+ if not os.path.isfile(filePath):
249
+ filePath = filePath + "/"
250
+ files.append(filePath)
251
+ return files
@@ -0,0 +1,108 @@
1
+ #! /usr/bin/env python
2
+ # Copyright (C) 2013 ETH Zurich, Institute of Astronomy
3
+
4
+ """
5
+ This is the DarkSkySync command line interface
6
+
7
+ Created on Sep 23, 2013
8
+
9
+ @author: J. Akeret
10
+
11
+
12
+ Usage:
13
+ DarkSkySync avail [<path>] [--config=<file>] [--template=<template>] [-v|--verbose]
14
+ DarkSkySync list [<path>] [-r|--recursive] [--config=<file>] [--template=<template>] [-v|--verbose]
15
+ DarkSkySync load <name>... [--dry_run] [-f|--force] [--config=<file>] [--template=<template>] [-v|--verbose]
16
+ DarkSkySync remove (<name>...|--all) [--config=<file>] [--template=<template>] [-v|--verbose]
17
+ DarkSkySync -h | --help
18
+ DarkSkySync --version
19
+
20
+ Options
21
+ -h --help Show this screen.
22
+ --version Show version.
23
+ --config=<file> The configfile to use.
24
+ --template=<template> The template to use [default:master]
25
+ --all All files
26
+ -v --verbose More output
27
+ -f --force Force the download
28
+ -r --recursive List the directories in a recursive way
29
+ --dry_run Dry run - Not loading any files
30
+ """
31
+
32
+ import sys
33
+
34
+ from . import __version__
35
+ from .DarkSkySync import DarkSkySync
36
+ from .docopt import docopt
37
+ from .Exceptions import ConfigurationError
38
+
39
+
40
+ class DarkSkySyncCLI:
41
+ """
42
+ Command line interface for the darkskysync
43
+ """
44
+
45
+ def __init__(self):
46
+ pass
47
+
48
+ def launch(self):
49
+ """
50
+ Starts the darkskysync by parsing the args and the config
51
+ """
52
+
53
+ args = docopt(__doc__, version="DarkSkySync CLI {}".format(__version__))
54
+
55
+ # print args
56
+ try:
57
+ dam = DarkSkySync(
58
+ template=args["--template"],
59
+ configFile=args["--config"],
60
+ verbose=args["--verbose"],
61
+ )
62
+ self.dispatch(dam, args)
63
+ except ConfigurationError:
64
+ sys.exit(1)
65
+
66
+ def dispatch(self, dam, args):
67
+ """
68
+ Dispatches the sub command to the given darkskysync instance
69
+
70
+ :param dam: the instance of the darkskysync to us
71
+ :param args: the arguments passed by the used
72
+ """
73
+ try:
74
+ if args["avail"]:
75
+ filesList = dam.avail(path=args["<path>"])
76
+ self._printFilesList("Available files:", filesList)
77
+
78
+ elif args["list"]:
79
+ filesList = dam.list(path=args["<path>"], recursive=args["--recursive"])
80
+ self._printFilesList("Available files:", filesList)
81
+
82
+ elif args["load"]:
83
+ filesList = dam.load(args["<name>"], args["--dry_run"], args["--force"])
84
+ self._printFilesList("Loaded files:", filesList)
85
+
86
+ elif args["remove"]:
87
+ dam.remove(args["<name>"], args["--all"])
88
+
89
+ except Exception:
90
+ sys.exit(1)
91
+
92
+ def _printFilesList(self, message, filesList):
93
+ print(message)
94
+ if len(filesList) > 0:
95
+ for fileName in filesList:
96
+ print(fileName)
97
+
98
+ else:
99
+ print("-")
100
+
101
+
102
+ def main():
103
+ cli = DarkSkySyncCLI()
104
+ cli.launch()
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -0,0 +1,268 @@
1
+ # Copyright (C) 2013 ETH Zurich, Institute of Astronomy
2
+
3
+ """
4
+ Created on Sep 23, 2013
5
+
6
+ @author: J. Akeret
7
+ """
8
+
9
+ import os
10
+ import re
11
+
12
+ from dataclasses import dataclass
13
+ from configobj import ConfigObj
14
+ from voluptuous import All, Invalid, Length, Schema, Url, message
15
+
16
+ from .Exceptions import ConfigurationError
17
+
18
+ # from .LocalFileSystem import LocalFileSystem
19
+ from .RemoteFactory import SSHRemoteLocationFactory
20
+
21
+
22
+ @dataclass
23
+ class DataSource:
24
+ name: str
25
+ local: str
26
+ remote: str
27
+
28
+
29
+ @dataclass
30
+ class LocalFileSystem:
31
+ name: str
32
+ filePath: str
33
+
34
+
35
+ class DataSourceFactory:
36
+ """
37
+ A factory creating DataSource configurations
38
+ """
39
+
40
+ DEFAULT_CONFIGURATION_FILE = os.path.expanduser("~/.darkskysync/config")
41
+
42
+ REMOTE_FACTORY_TYPE_MAP = {
43
+ "rsync": SSHRemoteLocationFactory,
44
+ "ssh": SSHRemoteLocationFactory,
45
+ "local": SSHRemoteLocationFactory,
46
+ }
47
+
48
+ def __init__(self, config):
49
+ """
50
+ Constructor
51
+ """
52
+ self.config = config
53
+ validator = ConfigValidator(self.config)
54
+ validator.validate()
55
+
56
+ @classmethod
57
+ def fromConfig(cls, configfile):
58
+ """
59
+ Helper method to initialize DataSourceFactory from an ini file.
60
+ :param configfile: path to the ini file
61
+ :return: configurator object
62
+ """
63
+ config_reader = ConfigReader(configfile)
64
+ conf = config_reader.read_config()
65
+ return DataSourceFactory(conf)
66
+
67
+ def createDataSource(self, template, name=None):
68
+ """
69
+ Creates a data source by inspecting the configuration properties of the
70
+ given data source template.
71
+ :param template: name of the data source template
72
+
73
+ :param name: name of the data source. If not defined, the data source
74
+ will be named after the template.
75
+
76
+ :return: :py:class:`DataSource` instance
77
+
78
+ :raises ConfigurationError: data source template not found in config
79
+ """
80
+ if not name:
81
+ name = template
82
+
83
+ if template not in self.config:
84
+ raise ConfigurationError(
85
+ "Invalid configuration for data source `%s`: %s" "" % (template, name)
86
+ )
87
+
88
+ conf = self.config[template]
89
+
90
+ extra = conf["datasource"].copy()
91
+ extra.pop("local")
92
+ extra.pop("remote")
93
+
94
+ return DataSource(
95
+ template, self.createLocal(template), self.createRemote(template)
96
+ )
97
+
98
+ def createLocal(self, template):
99
+ conf = self.config[template]
100
+
101
+ return LocalFileSystem(conf["datasource"]["local"], conf["local"]["path"])
102
+
103
+ def createRemote(self, template):
104
+ conf = self.config[template]
105
+ remoteType = conf["remote"]["type"]
106
+
107
+ RemoteFactory = DataSourceFactory.REMOTE_FACTORY_TYPE_MAP[remoteType]
108
+
109
+ factory = RemoteFactory()
110
+
111
+ return factory.create(conf["datasource"]["remote"], conf)
112
+
113
+
114
+ class ConfigValidator:
115
+ def __init__(self, config):
116
+ self.config = config
117
+
118
+ def _pre_validate(self):
119
+ """
120
+ Handles all pre validation phase functionality, such as:
121
+ - reading environment variables
122
+ - interpolating configuraiton options
123
+ """
124
+
125
+ def _post_validate(self):
126
+ """
127
+ Handles all post validation phase functionality, such as:
128
+ - expanding file paths
129
+ """
130
+ # expand all paths
131
+ for dataSource, values in self.config.items():
132
+ conf = self.config[dataSource]
133
+
134
+ privkey = os.path.expanduser(values["login"]["known_hosts"])
135
+ conf["login"]["known_hosts"] = privkey
136
+
137
+ privkey = os.path.expanduser(values["login"]["user_key_private"])
138
+ conf["login"]["user_key_private"] = privkey
139
+
140
+ pubkey = os.path.expanduser(values["login"]["user_key_public"])
141
+ conf["login"]["user_key_public"] = pubkey
142
+
143
+ path = os.path.expanduser(values["local"]["path"])
144
+ conf["local"]["path"] = path
145
+
146
+ def validate(self):
147
+ """
148
+ Validates the given configuration :py:attr:`self.config` to comply.
149
+ As well all types are converted to the expected
150
+ format if possible.
151
+ """
152
+ self._pre_validate()
153
+
154
+ # custom validators
155
+ @message("file could not be found")
156
+ def check_file(v):
157
+ f = os.path.expanduser(os.path.expanduser(v))
158
+ if os.path.exists(f):
159
+ return f
160
+ else:
161
+ raise Invalid("file could not be found `%s`" % v)
162
+
163
+ # schema to validate all dataSource properties
164
+ schema = {
165
+ "datasource": {
166
+ "local": All(str, Length(min=1)),
167
+ "remote": All(str, Length(min=1)),
168
+ },
169
+ "local": {
170
+ "path": All(str, Length(min=1)), # check_file(),
171
+ },
172
+ "login": {
173
+ "name": All(str, Length(min=1)),
174
+ "known_hosts": check_file(),
175
+ "user_key_private": check_file(),
176
+ "user_key_public": check_file(),
177
+ },
178
+ }
179
+
180
+ remote_schema_ssh = {
181
+ "type": "ssh",
182
+ "host": Url(str),
183
+ "port": All(str, Length(min=1)),
184
+ "path": All(str, Length(min=0)),
185
+ "login": All(str, Length(min=1)),
186
+ }
187
+
188
+ # validation
189
+ validator = Schema(schema, required=True, extra=True)
190
+ ssh_validator = Schema(remote_schema_ssh, required=True, extra=False)
191
+
192
+ if not self.config:
193
+ raise Invalid("No data sources found in configuration.")
194
+
195
+ for dataSource, properties in self.config.items():
196
+ validator(properties)
197
+
198
+ if properties["remote"]["type"] == "ssh":
199
+ ssh_validator(properties["remote"])
200
+
201
+ self._post_validate()
202
+
203
+
204
+ class ConfigReader:
205
+ """
206
+ Reads the configuration properties from a ini file.
207
+ """
208
+
209
+ local_section = "local"
210
+ remote_section = "remote"
211
+ login_section = "login"
212
+ datasource_section = "datasource"
213
+
214
+ def __init__(self, configfile):
215
+ """
216
+ :param configfile: path to configfile
217
+ """
218
+ self.configfile = configfile
219
+ self.conf = ConfigObj(self.configfile, interpolation=False)
220
+
221
+ def read_config(self):
222
+ """
223
+ Reads the configuration properties from the ini file and links the
224
+ section to comply with the datasource config dictionary format.
225
+ :return: dictionary containing all configuration properties from the
226
+ ini file in compliance to the datasource config format
227
+ :raises MultipleInvalid: not all sections present or broken links
228
+ between secitons
229
+ """
230
+ datasources = {
231
+ key: value
232
+ for key, value in self.conf.items()
233
+ if (
234
+ re.search(ConfigReader.datasource_section + "/(.*)", key)
235
+ and key.count("/") == 1
236
+ )
237
+ }
238
+
239
+ conf_values = dict()
240
+
241
+ for datasource in datasources:
242
+ name = re.search(
243
+ ConfigReader.datasource_section + "/(.*)", datasource
244
+ ).groups()[0]
245
+ try:
246
+ datasource_conf = dict(self.conf[datasource])
247
+ local_name = ConfigReader.local_section + "/" + datasource_conf["local"]
248
+ remote_name = (
249
+ ConfigReader.remote_section + "/" + datasource_conf["remote"]
250
+ )
251
+
252
+ values = dict()
253
+ values["datasource"] = datasource_conf
254
+ values["local"] = dict(self.conf[local_name])
255
+ values["remote"] = remote_conf = dict(self.conf[remote_name])
256
+
257
+ login_name = ConfigReader.login_section + "/" + remote_conf["login"]
258
+
259
+ values["login"] = dict(self.conf[login_name])
260
+
261
+ conf_values[name] = values
262
+ except KeyError:
263
+ raise Exception(
264
+ "could not find all sections required `datasource`, "
265
+ "`setup`, `login`, `cloud` for datasource `%s`" % name
266
+ )
267
+
268
+ return conf_values
@@ -0,0 +1,27 @@
1
+ # Copyright (C) 2013 ETH Zurich, Institute of Astronomy
2
+
3
+ """
4
+ Created on Sep 23, 2013
5
+
6
+ @author: J. Akeret
7
+ """
8
+
9
+
10
+ class ConfigurationError(Exception):
11
+ pass
12
+
13
+
14
+ class ConnectionError(Exception):
15
+ pass
16
+
17
+
18
+ class FileSystemError(Exception):
19
+ pass
20
+
21
+
22
+ class IllegalArgumentException(Exception):
23
+ pass
24
+
25
+
26
+ class FileNotFoundError(Exception):
27
+ pass