sidex 1.5.1__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.
sidex/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ ''' Simple Data Exchange Server
4
+
5
+ This module provides a minimal framework to launch a file server
6
+ running on the middleware `Flask`. You are allowed to fetch, put,
7
+ and delete files over HTTP.
8
+
9
+ You can launch a server on the command line as follows:
10
+
11
+ python -m sidex.server --host localhost --port 8000 TARGET_DIR
12
+
13
+ A file in the TARGET_DIR can be fetched by `wget`:
14
+
15
+ wget --post-data="method=get" http://localhost:8000/FILENAME
16
+
17
+ or by using `sidex.client`:
18
+
19
+ python -m sidex.client http://localhost:8080/FILENAME
20
+
21
+ The `POST` method is required since the `method` data is mandatory.
22
+ The `get` method can be protected by setting a token.
23
+
24
+ The `put` and `delete` methods are disabled by default. They can be
25
+ enabled by setting tokens. Note that the token is just a passphrase
26
+ and not ciphered at all. The transaction is never protected. Do not
27
+ use the SIDEX in case that the network is untrastrul.
28
+
29
+ The SIDEX provides a way to customize the functions. The default
30
+ behaviors of any methods can be overridden.
31
+ '''
32
+
33
+ from .setup import setup
34
+ from .request import request
35
+
36
+ __version__ = '1.5.1'
37
+
38
+ __all__ = [
39
+ '__version__',
40
+ 'setup',
41
+ 'request',
42
+ ]
sidex/client.py ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ ''' A command-line SIDEX client.
4
+
5
+ This file provides a command-line SIDEX client.
6
+ A detailed usage is available by typing the following command:
7
+
8
+ python -m sidex.client -h
9
+ '''
10
+
11
+ import os, sys, re, requests
12
+
13
+
14
+ if __name__ == '__main__':
15
+ from argparse import ArgumentParser as ap
16
+ parser = ap(prog='client', description='SIDEX minimal client')
17
+ parser.add_argument(
18
+ 'filename', type=str, nargs='?',
19
+ help='filename to be uploaded (only requred in put mode)')
20
+ parser.add_argument(
21
+ 'target', type=str,
22
+ help='address to SIDEX server')
23
+ parser.add_argument(
24
+ '-d', '--delete', action='store_true',
25
+ help='delete file')
26
+ parser.add_argument(
27
+ '-p', '--ping', action='store_true',
28
+ help='send ping message')
29
+ parser.add_argument(
30
+ '--token', metavar='token', type=str,
31
+ help='set token')
32
+
33
+ args = parser.parse_args()
34
+ # Leading "http://" can be omitted.
35
+ if not re.match('^https?://', args.target):
36
+ args.target = 'http://' + args.target
37
+ eprint = lambda s: print('error: '+s, file=sys.stderr)
38
+
39
+ if args.delete is True and args.filename is not None:
40
+ eprint('option conflicted.')
41
+ exit(1)
42
+
43
+ method = 'get'
44
+ files = None
45
+ filename = os.path.basename(args.target)
46
+
47
+ if args.ping is True:
48
+ method = 'ping'
49
+ if args.delete is True:
50
+ method = 'delete'
51
+ if args.filename is not None:
52
+ method = 'put'
53
+ with open(args.filename, 'rb') as f:
54
+ files = {'payload': f.read()}
55
+
56
+ if method == 'get' and os.path.exists(filename):
57
+ eprint('file "{}" already exists.'.format(filename))
58
+ exit(1)
59
+
60
+ try:
61
+ data = {'method': method, 'token': args.token}
62
+ req = requests.post(args.target, data=data, files=files)
63
+ if req.ok is False:
64
+ eprint(req.text.strip())
65
+ req.raise_for_status()
66
+
67
+ if method == 'get':
68
+ with open(filename, 'wb') as f:
69
+ f.write(req.content)
70
+ elif method == 'ping':
71
+ pass
72
+ else:
73
+ print(req.text.strip())
74
+ except Exception as e:
75
+ eprint(str(e))
76
+ exit(1)
sidex/dump.py ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ ''' A command-line SIDEX client for dump.
4
+
5
+ This file provides a command-line SIDEX client.
6
+ A detailed usage is available by typing the following command:
7
+
8
+ python -m sidex.dump -h
9
+ '''
10
+
11
+ import os, sys, re, requests
12
+ import io, gzip, bz2, lzma, tarfile
13
+
14
+
15
+ if __name__ == '__main__':
16
+ from argparse import ArgumentParser as ap
17
+ parser = ap(prog='dump', description='SIDEX dump client')
18
+ parser.add_argument('target', type=str,
19
+ help='address to SIDEX server')
20
+ parser.add_argument('filename', type=str, nargs='+',
21
+ help='requested filename')
22
+ parser.add_argument(
23
+ '-f', '--overwrite', dest='overwrite', action='store_true',
24
+ help='overwrite files even if exists')
25
+ parser.add_argument(
26
+ '--tar', dest='tarball', type=str, action='store',
27
+ help='grab files as a tarball archive')
28
+ parser.add_argument(
29
+ '--token', dest='token', metavar='token', type=str,
30
+ help='set token')
31
+
32
+ args = parser.parse_args()
33
+ # Leading "http://" can be omitted.
34
+ if not re.match('^https?://', args.target):
35
+ args.target = 'http://' + args.target
36
+ eprint = lambda s: print('error: '+s, file=sys.stderr)
37
+
38
+ method = 'dump'
39
+
40
+ if not args.overwrite:
41
+ for f in args.filename:
42
+ if os.path.exists(f):
43
+ eprint('file "{}" already exists.'.format(f))
44
+ exit(1)
45
+
46
+ try:
47
+ data = {
48
+ 'method': 'dump',
49
+ 'token': args.token,
50
+ 'filename': args.filename,
51
+ }
52
+ with requests.post(args.target, data=data, stream=True) as req:
53
+ if req.ok is False:
54
+ eprint(req.text.strip())
55
+ req.raise_for_status()
56
+
57
+ if args.tarball:
58
+ dummy, ext = os.path.splitext(args.tarball)
59
+ print(ext)
60
+ if ext == '.gz':
61
+ with gzip.open(args.tarball, 'wb') as arv:
62
+ for chunk in req.iter_content(65535): arv.write(chunk)
63
+ elif ext == '.bz2':
64
+ with bz2.open(args.tarball, 'wb') as arv:
65
+ for chunk in req.iter_content(65535): arv.write(chunk)
66
+ elif ext == '.xz':
67
+ with lzma.open(args.tarball, 'wb') as arv:
68
+ for chunk in req.iter_content(65535): arv.write(chunk)
69
+ else:
70
+ with open(args.tarball, 'wb') as arv:
71
+ for chunk in req.iter_content(65535): arv.write(chunk)
72
+ else:
73
+ buf = io.BytesIO(req.content)
74
+ with tarfile.open(fileobj=buf, mode='r:') as arv:
75
+ arv.extractall()
76
+ except Exception as e:
77
+ eprint(str(e))
78
+ exit(1)
sidex/request.py ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ ''' A function for a quick access to a sidex server.
4
+
5
+ This module provides a helper function `sidex_request` to make
6
+ a query to a sidex server.
7
+ '''
8
+ import os, requests
9
+
10
+
11
+ def request(url, method, filename=None, token=None):
12
+ ''' Make a request to a sidex server.
13
+
14
+ Arguments:
15
+ url (str):
16
+ The url, which defines a query to a sidex server.
17
+ method (str):
18
+ The name of the requested method. The available methods
19
+ are `get`, `put`, and `delete`.
20
+ filename (str, optional):
21
+ A filename to be uploaded to a sidex server. This
22
+ argument is only required in the 'put' method.
23
+ token (str, optional):
24
+ A token string passed to a sidex server. This argument will
25
+ be ignored when the server is not protecte by token.
26
+
27
+ Returns:
28
+ requests.Response:
29
+ A response from a sidex server.
30
+ '''
31
+ method = 'get'
32
+
33
+ if method not in ('get', 'put', 'delete'):
34
+ raise RuntimeError('invalid method.')
35
+ if method == 'put' and filename is None:
36
+ raise RuntimeError('upload file is not specified.')
37
+ data = {'method': method, 'token': token}
38
+
39
+ if method == 'get':
40
+ if os.path.exists(filename):
41
+ raise RuntimeError('file "{}" already exists'.format(filename))
42
+ return requests.post(url, data=data)
43
+ elif method == 'put':
44
+ with open(filename, 'rb') as f:
45
+ files = {
46
+ 'payload': f.read(),
47
+ }
48
+ return requests.post(url, data=data, files=files)
49
+ elif method == 'delete':
50
+ return requests.post(url, data=data)
sidex/server.py ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ ''' A command-line SIDEX server.
4
+
5
+ This file provides a command-line SIDEX server.
6
+ A detailed usage is available by typing the following command:
7
+
8
+ python -m sidex.server -h
9
+ '''
10
+
11
+ from . setup import setup
12
+
13
+
14
+ if __name__ == '__main__':
15
+ from argparse import ArgumentParser as ap
16
+ import logging
17
+ parser = ap(prog='server', description='sidex server process')
18
+ parser.add_argument(
19
+ 'target', type=str,
20
+ help='target directory')
21
+ parser.add_argument(
22
+ '--host', metavar='host', type=str, default='0.0.0.0',
23
+ help='set server hostname')
24
+ parser.add_argument(
25
+ '--port', metavar='port', type=int, default=8080,
26
+ help='set server port number')
27
+ parser.add_argument(
28
+ '--get-token', metavar='token', type=str,
29
+ help='limit get function by setting token')
30
+ parser.add_argument(
31
+ '--put-token', metavar='token', type=str,
32
+ help='enable put function by setting token')
33
+ parser.add_argument(
34
+ '--delete-token', metavar='token', type=str,
35
+ help='enable delete function by setting token.')
36
+ parser.add_argument(
37
+ '--subdir', metavar='subdir', type=str,
38
+ help='set subdirectory')
39
+ parser.add_argument(
40
+ '--debug', action='store_true',
41
+ help='enable debug messages')
42
+
43
+ args = parser.parse_args()
44
+
45
+ log_level = 'DEBUG' if args.debug else 'INFO'
46
+ log_handler = logging.StreamHandler()
47
+ log_handler.setFormatter(logging.Formatter(
48
+ fmt='[%(asctime)s] %(levelname)s:%(name)s:%(message)s',
49
+ datefmt='%Y-%m-%d %H:%M:%S'))
50
+
51
+ server = setup(
52
+ args.target, subdir=args.subdir,
53
+ get_token=args.get_token, put_token=args.put_token,
54
+ delete_token=args.delete_token,
55
+ log_handler=log_handler, log_level=log_level)
56
+ server.run(host=args.host, port=args.port, threaded=True, debug=args.debug)
sidex/setup.py ADDED
@@ -0,0 +1,674 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ ''' SIDEX miscellaneous functions.
4
+
5
+ This module provides miscellaneous functions to setup the SIDEX server.
6
+ The behaviours of the `get`, `put`, and `delete` methods are defined.
7
+
8
+ Use the `setup` function to launch a customized SIDEX server.
9
+ '''
10
+ from flask import Flask, Response
11
+ from flask import request, url_for
12
+ import os, io, tarfile, logging
13
+
14
+ werkzeug = logging.getLogger('werkzeug')
15
+ werkzeug.setLevel('ERROR')
16
+
17
+ app = Flask(__name__)
18
+
19
+
20
+ @app.context_processor
21
+ def override_url_for():
22
+ ''' A helper function to construct a url with `subdir` option.
23
+
24
+ This does nothing when the `subdir` option is not defined.
25
+ The url is modified in case that the `subdir` is given. The modified
26
+ `url_for` function accepts the following arguments.
27
+
28
+ Arguments:
29
+ endpoint (str):
30
+ The relative path to the requested resource.
31
+ *Arguments:
32
+ Variable length argument list.
33
+ **options:
34
+ Arbitrary option arguments.
35
+
36
+ Returns:
37
+ str: A constructed url to the requeste resource.
38
+ '''
39
+ def url_for_subdir(endpoint, *args, **options):
40
+ subdir = '/{}'.format(app.subdir) if app.subdir else ''
41
+ return subdir+url_for(endpoint, *args, **options)
42
+ return dict(url_for=url_for_subdir)
43
+
44
+
45
+ def eprint(message, exc_info=None):
46
+ ''' Print an error message in the log file.
47
+
48
+ Arguments:
49
+ message (str):
50
+ The body of the error message.
51
+ exc_info (Exception,optional):
52
+ The Exception instance to print traceback information.
53
+ '''
54
+ remote = request.remote_addr
55
+ app.logger.error(remote+':{}'.format(message), exc_info=exc_info)
56
+
57
+
58
+ def iprint(message):
59
+ ''' Print an info message in the log file.
60
+
61
+ Arguments:
62
+ message (str):
63
+ The body of the information message.
64
+ '''
65
+ remote = request.remote_addr
66
+ app.logger.info(remote+':{}'.format(message))
67
+
68
+
69
+ def dprint(message):
70
+ ''' Print a debug message in the log file.
71
+
72
+ Arguments:
73
+ message (str):
74
+ The body of the debug message.
75
+ '''
76
+ remote = request.remote_addr
77
+ app.logger.debug(remote+':{}'.format(message))
78
+
79
+
80
+ def invalid_path(path):
81
+ ''' Check whether the <path> looks invalid.
82
+
83
+ Arguments:
84
+ path (str):
85
+ The path to the file to be checked.
86
+
87
+ Returns:
88
+ bool:
89
+ `true` when the <path> contains any invalid sequences.
90
+ '''
91
+ return '../' in path
92
+
93
+
94
+ def default_delete_function(req, local_path, **options):
95
+ ''' Define the default behavior of the `delete` method.
96
+
97
+ This function removes the file located at <local_path>.
98
+
99
+ Arguments:
100
+ req (flask.request):
101
+ The request instance given by Flask.
102
+ local_path (str):
103
+ The absolute path to the requested resource.
104
+ **options:
105
+ Arbitrary option arguments.
106
+ Currently no option is passed to this function.
107
+
108
+ Returns:
109
+ flask.Response:
110
+ A response message.
111
+ '''
112
+ filename = os.path.basename(local_path)
113
+ os.unlink(local_path)
114
+ return Response('"{}" successfully deleted.\n'.format(filename))
115
+
116
+
117
+ def delete(req, target, **options):
118
+ ''' Provide the `delete` method.
119
+
120
+ This function is called when the `delete` method is selected.
121
+ The <app.delete_function> is called internally. When the customized
122
+ `delete_function` is specified, the behavior of the `delete` method
123
+ is overridden.
124
+
125
+ This method is disabled by default. To enable the `delete` method,
126
+ the application should be setup with `delete_token`. The token is
127
+ reffered to as <app.delete_token>.
128
+
129
+ The request must contain the `token` field. When the `token` is not
130
+ available, this always returns an error message.
131
+ In case that the `token` does not equal to <app.delete_token>,
132
+ an error message will be returned.
133
+
134
+ Arguments:
135
+ req (flask.request):
136
+ A request instance generated by Flask.
137
+ target (str):
138
+ The path to the requested resource.
139
+ **options:
140
+ Arbitrary option arguments.
141
+ Currently no option is passed to this function.
142
+
143
+ Returns:
144
+ flask.Response:
145
+ A response message.
146
+ '''
147
+ local_path = '{}/{}'.format(app.workdir, target)
148
+ filename = os.path.basename(local_path)
149
+ dirname = os.path.dirname(local_path)
150
+ emsg = lambda s: 'cannot delete "{}": {{}}.\n'.format(target).format(s)
151
+ # check if a valid token is given.
152
+ if app.delete_token is None:
153
+ eprint('disabled function "delete" called.')
154
+ return Response(emsg('function disabled'), status=400)
155
+ token = req.form.get('token')
156
+ dprint('token = "{}"'.format(token))
157
+ if app.delete_token is not None and token != app.delete_token:
158
+ return Response(emsg('invalid token'), status=400)
159
+ # assert path seems valid.
160
+ if invalid_path(target):
161
+ eprint('invalid path: {}'.format(target))
162
+ return Response(emsg('invalid path'), status=400)
163
+ # delete a file.
164
+ try:
165
+ return app.delete_function(req, local_path, **options)
166
+ except FileNotFoundError as e:
167
+ eprint(str(e))
168
+ return Response(emsg('file not found'), status=404)
169
+ except Exception as e:
170
+ eprint(str(e))
171
+ errmsg = emsg(f'unexpected error: {str(e)} ({e.__class__.__name__})')
172
+ return Response(errmsg, status=500)
173
+
174
+
175
+ def default_put_function(req, local_path, **options):
176
+ ''' Define the default behavior of the `put` method.
177
+
178
+ This function create a file stored in <req> at <local_path>.
179
+ This fails when the local directory does not exist. Note that
180
+ any existing file cannot be overwritten.
181
+
182
+ Arguments:
183
+ req (flask.request):
184
+ The request instance given by Flask.
185
+ local_path (str):
186
+ The absolute path to the requested resource.
187
+ **options:
188
+ Arbitrary option arguments.
189
+ Currently no option is passed to this function.
190
+
191
+ Returns:
192
+ flask.Response:
193
+ A response message.
194
+ '''
195
+ filename = os.path.basename(local_path)
196
+ dirname = os.path.dirname(local_path)
197
+ req.files['payload'].save(local_path)
198
+ return Response('successfully uploaded to "{}".\n'.format(filename))
199
+
200
+
201
+ def put(req, target, **options):
202
+ ''' Provide the `put` method.
203
+
204
+ This function is called when the `put` method is selected.
205
+ The <app.put_function> is called internally. When the customized
206
+ `put_function` is specified, the behavior of the `put` method
207
+ is overridden.
208
+
209
+ This method is disabled by default. To enable the `put` method,
210
+ the application should be setup with `put_token`. The token is
211
+ reffered to as <app.put_token>.
212
+
213
+ The request must contain the `token` field. When the `token` is not
214
+ available, this always returns an error message.
215
+ In case that the `token` does not equal to <app.put_token>,
216
+ an error message will be returned.
217
+
218
+ Arguments:
219
+ req (flask.request):
220
+ A request instance generated by Flask.
221
+ target (str):
222
+ The path to the requested resource.
223
+ **options:
224
+ Arbitrary option arguments.
225
+ Currently no option is passed to this function.
226
+
227
+ Returns:
228
+ flask.Response:
229
+ A response message.
230
+ '''
231
+ local_path = '{}/{}'.format(app.workdir, target)
232
+ filename = os.path.basename(local_path)
233
+ emsg = lambda s: 'cannot create "{}": {{}}.\n'.format(target).format(s)
234
+ # check if a valid token is given.
235
+ if app.put_token is None:
236
+ eprint('disabled function "put" called.')
237
+ return Response(emsg('function disabled'), status=400)
238
+ token = req.form.get('token')
239
+ dprint('token = "{}"'.format(token))
240
+ if app.put_token is not None and token != app.put_token:
241
+ return Response(emsg('invalid token'), status=400)
242
+ # assert path seems valid.
243
+ if invalid_path(target):
244
+ eprint('invalid path: {}'.format(target))
245
+ return Response(emsg('invalid path'), status=400)
246
+ # overwrite is not allowed.
247
+ if os.path.exists(local_path):
248
+ eprint('file "{}" already exists.'.format(local_path))
249
+ return Response(emsg('cannot overwrite files'), status=400)
250
+ # create a file.
251
+ if 'payload' not in req.files:
252
+ return Response(emsg('"payload" is required'), status=400)
253
+ try:
254
+ return app.put_function(req, local_path, **options)
255
+ except FileNotFoundError as e:
256
+ eprint(str(e))
257
+ return Response(emsg('file not found'), status=404)
258
+ except Exception as e:
259
+ eprint(str(e))
260
+ errmsg = emsg(f'unexpected error: {str(e)} ({e.__class__.__name__})')
261
+ return Response(errmsg, status=500)
262
+
263
+
264
+ def default_get_function(req, local_path, **options):
265
+ ''' Define the default behavior of the `get` method.
266
+
267
+ Returns the file content located at <local_path>.
268
+
269
+ Arguments:
270
+ req (flask.request):
271
+ The request instance given by Flask.
272
+ local_path (str):
273
+ The absolute path to the requested resource.
274
+ **options:
275
+ Arbitrary option arguments.
276
+ Currently no option is passed to this function.
277
+
278
+ Returns:
279
+ flask.Response:
280
+ A response message.
281
+ '''
282
+ with open(local_path, 'rb') as f:
283
+ return Response(f.read(), mimetype='application/octet-stream')
284
+
285
+
286
+ def streaming_get_function(req, local_path, **options):
287
+ ''' Define the streaming version of the `get` method.
288
+
289
+ Returns the file content located at <local_path>.
290
+
291
+ Arguments:
292
+ req (flask.request):
293
+ The request instance given by Flask.
294
+ local_path (str):
295
+ The absolute path to the requested resource.
296
+ **options:
297
+ Arbitrary option arguments.
298
+ Currently no option is passed to this function.
299
+
300
+ Returns:
301
+ flask.Response:
302
+ A response message.
303
+ '''
304
+ bufsize = options.get('bufsize', 65535)
305
+
306
+ def is_active(FileStream):
307
+ b = FileStream.read(1)
308
+ FileStream.seek(-1, 1)
309
+ return bool(b)
310
+
311
+ def streaming():
312
+ with open(local_path, 'rb') as f:
313
+ while is_active(f):
314
+ yield f.read(bufsize)
315
+
316
+ return Response(streaming(), mimetype='application/octet-stream')
317
+
318
+
319
+ def get(req, target, **options):
320
+ ''' Provide the `get` method.
321
+
322
+ This function is called when the `get` method is selected.
323
+ The <app.get_function> is called internally. When the customized
324
+ `get_function` is specified, the behavior of the `get` method
325
+ is overridden.
326
+
327
+ This method can be protected by setting `get_token`. The token is
328
+ reffered to as <app.get_token>. When <app.get_token> is defined,
329
+ the request must contain the `token` field.
330
+ In case that the `token` does not equal to <app.get_token>,
331
+ an error message will be returned.
332
+
333
+ Arguments:
334
+ req (flask.request):
335
+ A request instance generated by Flask.
336
+ target (str):
337
+ The path to the requested resource.
338
+ **options:
339
+ Arbitrary option arguments.
340
+ Currently no option is passed to this function.
341
+
342
+ Returns:
343
+ flask.Response:
344
+ A response message.
345
+ '''
346
+ local_path = '{}/{}'.format(app.workdir, target)
347
+ filename = os.path.basename(local_path)
348
+ emsg = lambda s: 'cannot access "{}": {{}}.\n'.format(target).format(s)
349
+ # check if a valid token is given.
350
+ if app.get_token is not None:
351
+ dprint('"get" function requres a token.')
352
+ token = req.form.get('token')
353
+ dprint('token = "{}"'.format(token))
354
+ if token != app.get_token:
355
+ return Response(emsg('invalid token'), status=400)
356
+ # assert path seems valid.
357
+ if invalid_path(target):
358
+ eprint('invalid path: {}'.format(target))
359
+ return Response(emsg('invalid path'), status=500)
360
+ # access to file.
361
+ try:
362
+ return app.get_function(req, local_path, **options)
363
+ except FileNotFoundError as e:
364
+ eprint(str(e))
365
+ return Response(emsg('file not found'), status=404)
366
+ except IsADirectoryError as e:
367
+ eprint(str(e))
368
+ return Response(emsg('cannot obtain directory'), status=400)
369
+ except Exception as e:
370
+ eprint(str(e))
371
+ errmsg = emsg(f'unexpected error: {str(e)} ({e.__class__.__name__})')
372
+ return Response(errmsg, status=500)
373
+
374
+
375
+ def default_dump_function(req, local_paths, **options):
376
+ ''' Define the default behavior of the `dump` method.
377
+
378
+ Returns the file content located at <local_path>.
379
+
380
+ Arguments:
381
+ req (flask.request):
382
+ The request instance given by Flask.
383
+ local_paths (list):
384
+ The list of the absolute paths to the requested resources.
385
+ **options:
386
+ Arbitrary option arguments.
387
+ Currently no option is passed to this function.
388
+
389
+ Returns:
390
+ flask.Response:
391
+ A response message.
392
+ '''
393
+ def streaming():
394
+ buf = io.BytesIO()
395
+ with tarfile.open(fileobj=buf, mode='w') as arv:
396
+ for filename in local_paths:
397
+ pos = buf.tell()
398
+ basename = os.path.basename(filename)
399
+ arv.add(filename, arcname=basename)
400
+ buf.seek(pos)
401
+ yield buf.read()
402
+ # the stream position should be set something but zero.
403
+ # an init process is called when the stream position is zero.
404
+ buf.seek(1)
405
+ buf.truncate(1)
406
+ return Response(streaming(), mimetype='application/octet-stream')
407
+
408
+
409
+ def dump(req, filelist, **options):
410
+ ''' Provide the `dump` method.
411
+
412
+ This function is called when the `dump` method is selected.
413
+ The <app.dump_function> is called internally. When the customized
414
+ `dump_function` is specified, the behavior of the `dump` method
415
+ is overridden.
416
+
417
+ This method can be protected by setting `get_token`. The token is
418
+ reffered to as <app.get_token>. When <app.get_token> is defined,
419
+ the request must contain the `token` field.
420
+ In case that the `token` does not equal to <app.get_token>,
421
+ an error message will be returned.
422
+
423
+ Arguments:
424
+ req (flask.request):
425
+ A request instance generated by Flask.
426
+ filelist (list):
427
+ The list of the paths to the requested resources.
428
+ **options:
429
+ Arbitrary option arguments.
430
+ Currently no option is passed to this function.
431
+
432
+ Returns:
433
+ flask.Response:
434
+ A response message.
435
+ '''
436
+ local_paths = ['{}/{}'.format(app.workdir, f) for f in filelist]
437
+ filenames = [os.path.basename(p) for p in local_paths]
438
+ emsg = lambda s: 'cannot access resources: {}.\n'.format(s)
439
+ # check if a valid token is given.
440
+ if app.get_token is not None:
441
+ dprint('"get" function requres a token.')
442
+ token = req.form.get('token')
443
+ dprint('token = "{}"'.format(token))
444
+ if token != app.get_token:
445
+ return Response(emsg('invalid token'), status=400)
446
+ # assert path seems valid.
447
+ for target in filelist:
448
+ if invalid_path(target):
449
+ eprint('invalid path: {}'.format(target))
450
+ return Response(emsg('invalid path'), status=500)
451
+ # access to file.
452
+ try:
453
+ return app.dump_function(req, local_paths, **options)
454
+ except FileNotFoundError as e:
455
+ eprint(str(e))
456
+ return Response(emsg('file not found'), status=404)
457
+ except IsADirectoryError as e:
458
+ eprint(str(e))
459
+ return Response(emsg('cannot obtain directory'), status=400)
460
+ except Exception as e:
461
+ eprint(str(e))
462
+ errmsg = emsg(f'unexpected error: {str(e)} ({e.__class__.__name__})')
463
+ return Response(errmsg, status=500)
464
+
465
+
466
+ def default_ping_function(req, local_path, **options):
467
+ ''' Define the default behavior of the `ping` method.
468
+
469
+ Returns True if the the requested file exists at <local_path>.
470
+
471
+ Arguments:
472
+ req (flask.request):
473
+ The request instance given by Flask.
474
+ local_path (str):
475
+ The absolute paths to the requested resource.
476
+ **options:
477
+ Arbitrary option arguments.
478
+ Currently no option is passed to this function.
479
+
480
+ Returns:
481
+ flask.Response:
482
+ A response message.
483
+ '''
484
+ if os.path.exists(local_path):
485
+ return Response('', 200)
486
+ else:
487
+ raise FileNotFoundError(local_path)
488
+
489
+
490
+ def ping(req, target, **options):
491
+ ''' Provide the `ping` method.
492
+
493
+ This function is called when the `ping` method is selected.
494
+ The <app.ping_function> is called internally. When the customized
495
+ `ping_function` is specified, the behavior of the `ping` method
496
+ is overridden.
497
+
498
+ This method can be protected by setting `get_token`. The token is
499
+ reffered to as <app.get_token>. When <app.get_token> is defined,
500
+ the request must contain the `token` field.
501
+ In case that the `token` does not equal to <app.get_token>,
502
+ an error message will be returned.
503
+
504
+ Arguments:
505
+ req (flask.request):
506
+ A request instance generated by Flask.
507
+ target (str):
508
+ The path to the requested resource.
509
+ **options:
510
+ Arbitrary option arguments.
511
+ Currently no option is passed to this function.
512
+
513
+ Returns:
514
+ flask.Response:
515
+ A response message.
516
+ '''
517
+ local_path = '{}/{}'.format(app.workdir, target)
518
+ filename = os.path.basename(local_path)
519
+ emsg = lambda s: 'cannot access "{}": {{}}.\n'.format(target).format(s)
520
+ # check if a valid token is given.
521
+ if app.get_token is not None:
522
+ dprint('"get" function requres a token.')
523
+ token = req.form.get('token')
524
+ dprint('token = "{}"'.format(token))
525
+ if token != app.get_token:
526
+ return Response(emsg('invalid token'), status=400)
527
+ # assert path seems valid.
528
+ if invalid_path(target):
529
+ eprint('invalid path: {}'.format(target))
530
+ return Response(emsg('invalid path'), status=500)
531
+ # access to file.
532
+ try:
533
+ return app.ping_function(req, local_path, **options)
534
+ except FileNotFoundError as e:
535
+ eprint(str(e))
536
+ return Response(emsg('file/directory not found'), status=404)
537
+ except Exception as e:
538
+ eprint(str(e))
539
+ errmsg = emsg(f'unexpected error: {str(e)} ({e.__class__.__name__})')
540
+ return Response(errmsg, status=500)
541
+
542
+
543
+ @app.route('/<path:target>', methods=['GET', ])
544
+ def access_by_get(target):
545
+ ''' The access point of the SIDEX server.
546
+
547
+ This function handles the GET request to the SIDEX server.
548
+ This works only if the `token` is not specified.
549
+
550
+ Arguments:
551
+ target (str):
552
+ The path to the requested resource.
553
+
554
+ Returns:
555
+ flask.Response:
556
+ The response instance from the requested method.
557
+ '''
558
+ return get(request, target)
559
+
560
+
561
+ @app.route('/<path:target>', methods=['POST', ])
562
+ def access_get_by_post(target):
563
+ ''' The access point of the SIDEX server.
564
+
565
+ This function handles the POST request to the SIDEX server.
566
+ The `method` field is mandatory. The value should be one of `get`,
567
+ `put` and `delete`. The corresponding function will be called.
568
+
569
+ The `token` field is required in the `put` and `delete` methods.
570
+
571
+ Arguments:
572
+ target (str):
573
+ The path to the requested resource.
574
+
575
+ Returns:
576
+ flask.Response:
577
+ The response instance from the requested method.
578
+ '''
579
+ method = request.form.get('method', 'none').lower()
580
+ if method == 'get':
581
+ return get(request, target)
582
+ elif method == 'put':
583
+ return put(request, target)
584
+ elif method == 'delete':
585
+ return delete(request, target)
586
+ elif method == 'ping':
587
+ return ping(request, target)
588
+ else:
589
+ return Response('invalid method: {}.\n'.format(method), status=400)
590
+
591
+
592
+ @app.route('/', methods=['GET', 'POST'])
593
+ def root():
594
+ if request.method == 'GET':
595
+ return Response('It works! The SIDEX server is running.\n')
596
+ else:
597
+ method = request.form.get('method', 'none').lower()
598
+ if method == 'dump':
599
+ filenames = request.form.getlist('filename')
600
+ return dump(request, filenames)
601
+ else:
602
+ return Response('invalid method: {}.\n'.format(method), status=400)
603
+
604
+
605
+ def setup(
606
+ workdir, subdir=None,
607
+ get_function=default_get_function,
608
+ put_function=default_put_function,
609
+ delete_function=default_delete_function,
610
+ dump_function=default_dump_function,
611
+ ping_function=default_ping_function,
612
+ get_token=None, put_token=None, delete_token=None,
613
+ log_handler=None, log_level='INFO'):
614
+ ''' Setup a customized SIDEX server.
615
+
616
+ Arguments:
617
+ workdir (str):
618
+ The relative path to the working directory.
619
+ subdir (str,optional):
620
+ The name of the subdirectory.
621
+ get_function (function,optional):
622
+ The function instance called in the `get` method.
623
+ The following arguments are required:
624
+ - req (flask.request): The flask request instance.
625
+ - local_path (str): The local path to the resource.
626
+ put_function (function,optional):
627
+ The function instance called in the `put` method.
628
+ The following arguments are required:
629
+ - req (flask.request): The flask request instance.
630
+ - local_path (str): The local path to the resource.
631
+ delete_function (function,optional):
632
+ The function instance called in the `delete` method.
633
+ The following arguments are required:
634
+ - req (flask.request): The flask request instance.
635
+ - local_path (str): The local path to the resource.
636
+ dump_function (function,optional):
637
+ The function instance called in the `dump` method.
638
+ The following arguments are required:
639
+ - req (flask.request): The flask request instance.
640
+ - local_paths (list): The path list to the resources.
641
+ ping_function (function,optional):
642
+ The function instance called in the `ping` method.
643
+ The following arguments are required:
644
+ - req (flask,request): The flask request instance.
645
+ - local_path (str): The local path to the resource.
646
+ get_token (str,optional):
647
+ The secret token for the `get` method.
648
+ put_token (str,optional):
649
+ The secret token for the `put` method.
650
+ delete_token (str,optional):
651
+ The secret token for the `delete` method.
652
+ log_handler (logger.Logger,optional):
653
+ The user-defined log handler to override the default handler.
654
+ log_level (str,optional):
655
+ The log_level. `INFO` is given by default.
656
+ '''
657
+ if get_token is not None: assert len(get_token) > 0
658
+ if put_token is not None: assert len(put_token) > 0
659
+ if delete_token is not None: assert len(delete_token) > 0
660
+ app.workdir = workdir
661
+ app.get_function = get_function
662
+ app.put_function = put_function
663
+ app.delete_function = delete_function
664
+ app.dump_function = dump_function
665
+ app.ping_function = ping_function
666
+ app.get_token = get_token
667
+ app.put_token = put_token
668
+ app.delete_token = delete_token
669
+ app.subdir = subdir
670
+ app.logger.setLevel(log_level)
671
+ if log_handler is not None:
672
+ werkzeug.handlers = []
673
+ app.logger.handlers = [log_handler, ]
674
+ return app
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: sidex
3
+ Version: 1.5.1
4
+ Summary: SIDEX: Simple Data Exchange server over HTTP
5
+ Author-email: Ryou Ohsawa <ryou.ohsawa@nao.ac.jp>
6
+ Maintainer-email: Ryou Ohsawa <ryou.ohsawa@nao.ac.jp>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/astronasutarou/sidex
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: flask>=2.0
21
+ Requires-Dist: requests>=2.27
22
+
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
24
+ [![CodeQL](https://github.com/astronasutarou/sidex/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/astronasutarou/sidex/actions/workflows/github-code-scanning/codeql)
25
+ [![Documentation Status](https://readthedocs.org/projects/sidex/badge/?version=latest)](https://sidex.readthedocs.io/en/latest/?badge=latest)
26
+
27
+ # Simple Data Exchange server over HTTP
28
+
29
+ ## Overview
30
+ This package provides a function to launch a simple file server. Getting, putting, and deleting files on the server via the HTTP POST methods are available. The function `setup_sidex()` returns a `flask` instance. You can launch a simple file server by `run()`.
31
+
32
+ ``` python
33
+ from sidex import setup_sidex
34
+
35
+ target = '/path/to/directory'
36
+ app = setup_sidex(target)
37
+ app.run()
38
+ ```
39
+
40
+ Otherwise, you can directly call `sidex.server`.
41
+
42
+ ``` sh
43
+ $ python -m sidex.server /path/to/directory
44
+ ```
45
+
46
+ By default, only retrieving files (`get`) is available. The `put` and `delete` methods are enabled by setting a 'token' for each method. Of course, the `get` function can be restricted by a `token`.
47
+
48
+ The HTTP POST method is available to submit a request. Any request should contain the `method` field, which should be one of `get`, `put`, and `delete`. The `token` field may be required in some cases. The followings are samples with `curl`.
49
+
50
+ ``` sh
51
+ $ curl http://0.0.0.0:8080/path/to/file -F 'method=get'
52
+ $ curl http://0.0.0.0:8080/path/to/upload -F 'method=put' -F 'payload=@filename' -F 'token=foo'
53
+ $ curl http://0.0.0.0:8080/path/to/delete -F 'method=delete' -F 'token=bar'
54
+ ```
55
+
56
+ The package provides a function, `sidex_request()`, which is a wrapper function of `requests.post()`. You can directly execute `sidex.client`.
57
+
58
+ ``` sh
59
+ $ python -m sidex.client http://0.0.0.0:8080/path/to/file
60
+ $ python -m sidex.client http://0.0.0.0:8080/path/to/file --ping
61
+ $ python -m sidex.client http://0.0.0.0:8080/path/to/upload -f upload_file
62
+ $ python -m sidex.client http://0.0.0.0:8080/path/to/delete -d
63
+ ```
64
+
65
+
66
+ ## Dependencies
67
+ The library is developed on Python 3.9.9. The following packages are required:
68
+
69
+ ``` python
70
+ flask>=2.0
71
+ requests>=2.27
72
+ ```
@@ -0,0 +1,10 @@
1
+ sidex/__init__.py,sha256=FHqXfZ3IwQBfmTzcI_3NdVUzPgQJJ8smrsDRUmHvTQc,1205
2
+ sidex/client.py,sha256=aM34SdBhXUf7e7glkuaFpJj8LlD29O1Iu5oLhJm9Y84,2228
3
+ sidex/dump.py,sha256=cHT5-ghfrImOU6ZM1U5m002OQIFl7_6CUDQJVYceENM,2782
4
+ sidex/request.py,sha256=6RXQg3ldZZqOOzyS4agbtI0sOcz2fAvBMnYoBj-staU,1680
5
+ sidex/server.py,sha256=IoG8oHS7xGRCRlZ6McAC7GWxeFTAGo3Uyii7VU1vqYk,1916
6
+ sidex/setup.py,sha256=SEDinX8t3W2PZjF7tdMZ9kkqeRRhtb8jLlk-uu7R9Z4,23511
7
+ sidex-1.5.1.dist-info/METADATA,sha256=2xOF5oGCf3qOYqyRMT1AFtEEjPEmC4X2qELhCIMkzr0,3059
8
+ sidex-1.5.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ sidex-1.5.1.dist-info/top_level.txt,sha256=9gw4MnFmjW8UkjL-AZPLbc-9jAuNPFxn9bc_guBVPwE,17
10
+ sidex-1.5.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ build
2
+ docs
3
+ sidex