-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDownloadServer.py
More file actions
159 lines (125 loc) · 5.26 KB
/
Copy pathDownloadServer.py
File metadata and controls
159 lines (125 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# encoding=utf-8
import os
import threading
import shutil
import hashlib
from datetime import datetime
from local_settings import *
from JsonExporterForIOS import build_split_archive, SEFARIA_EXPORT_PATH, get_export_path, updated_books_list, new_books_since_last_update, clear_old_bundles, SCHEMA_VERSION, PREV_SCHEMA_VERSION
from flask import Flask, request, Response, jsonify
from werkzeug.datastructures import FileStorage
try:
from local_settings import DEBUG_MODE
except ImportError:
DEBUG_MODE = False
# schema version is now required when requesting an API view that is schema specific
# in the case it's not provided, we will fall back to the last version that allowed it to not be defined (which is buggy behavior)
DEFAULT_API_SCHEMA_VERSION = "6"
app = Flask(__name__)
URL_BASE = 'static/ios-export'
def url_stubs(bundle_path, schema_version):
bundle_name = os.path.basename(bundle_path)
try:
values = [f'{URL_BASE}/{schema_version}/bundles/{bundle_name}/{f}' for f in os.listdir(bundle_path)]
except FileNotFoundError:
values = []
values.sort()
return values
def get_directory_size(dir_path):
total = 0
for f in os.listdir(dir_path): # this is naive but works for the case at hand
total += os.path.getsize(os.path.join(dir_path, f))
return total
def create_zip_bundle(book_list, zip_path, zip_dirname, file_locations):
# thread safety is critical in this method.
def is_recent_dir(dirname): # this method should only return False if the directory in question exists and is "old"
try:
s = os.stat(dirname)
except FileNotFoundError:
return True
diff = datetime.now() - datetime.fromtimestamp(s.st_mtime)
return diff.total_seconds() < 120
try:
os.mkdir('./tmp')
except FileExistsError:
pass
tmp_dir = f'./tmp/{zip_dirname}'
if os.path.exists(tmp_dir):
if is_recent_dir(tmp_dir):
return
build_split_archive(book_list, tmp_dir, file_locations)
try:
shutil.move(tmp_dir, zip_path)
except FileNotFoundError: # can theoretically happen if another thread is active
return
def get_schema_from_request(req):
try:
return int(req.args.get('schema_version'))
except (ValueError, TypeError):
return DEFAULT_API_SCHEMA_VERSION
@app.route('/makeBundle', methods=['POST'])
def make_bundle():
if not request.json or not request.json.get('books'):
return Response(status=400, response='Invalid JSON')
schema_version = get_schema_from_request(request)
export_path = f'{SEFARIA_EXPORT_PATH}/{schema_version}'
book_list = [f'{b}.zip' for b in request.json['books']]
book_list = [b for b in book_list if os.path.exists(os.path.join(export_path, b))]
zip_dirname = get_bundle_filename(book_list)
zip_path = f'{export_path}/bundles/{zip_dirname}'
if os.path.exists(zip_path):
return {
'bundleArray': url_stubs(zip_path, schema_version),
'downloadSize': get_directory_size(zip_path),
}
else:
clear_old_bundles(SCHEMA_VERSION)
clear_old_bundles(PREV_SCHEMA_VERSION)
t = threading.Thread(target=create_zip_bundle, args=(book_list, zip_path, zip_dirname, export_path))
t.start()
return Response(status=202, response='Accepted')
@app.route('/packageData', methods=['GET'])
def get_package_paths():
if not request.args or not request.args.get('package'):
return jsonify([])
package_name = request.args['package']
schema_version = get_schema_from_request(request)
base_path = f'{SEFARIA_EXPORT_PATH}/{schema_version}/bundles'
return jsonify(url_stubs(f'{base_path}/{package_name}', schema_version))
def get_bundle_filename(book_list: list) -> str:
books_hash = hashlib.sha1('|'.join(sorted(book_list)).encode('utf-8')).hexdigest()
return f'{books_hash}'
def password_protect(user_password):
try:
password = os.environ['PASSWORD']
except KeyError:
return False
return user_password == password
@app.route('/update')
def update():
try:
password = os.environ['PASSWORD']
except KeyError:
return Response(status=403, response='Forbidden')
user_password = request.args.get('password')
if user_password != password:
return Response(status=403, response='Forbidden')
action, index = request.args.get('action', default='export_updated'), request.args.get('index', default='')
os.system(f'python JsonExporterForIOS.py {action} {index} &')
return {'status': 'ok'}
@app.route('/booksExport', methods=['GET', 'POST'])
def books_export():
user_password = request.args.get('password')
if not password_protect(user_password) and not DEBUG_MODE:
return Response(status=403, response='Forbidden')
if request.method == 'GET':
return jsonify(updated_books_list() + new_books_since_last_update())
elif request.method == 'POST':
f = request.args.get('filename')
FileStorage(request.stream).save(os.path.join(get_export_path(SCHEMA_VERSION), f))
return Response(status=200, response='ok')
@app.route('/healthz')
def healthcheck():
return Response(status=200, response='Health: Ok')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)