1 | #!/usr/bin/env python |
||
2 | # SPDX-License-Identifier: LGPL-3.0-only |
||
3 | 1 | ||
4 | """REST server to display content and reserve item numbers.""" |
||
5 | 1 | ||
6 | 1 | import argparse |
|
7 | 1 | import logging |
|
8 | 1 | import os |
|
9 | 1 | import webbrowser |
|
10 | from collections import defaultdict |
||
11 | 1 | ||
12 | 1 | import bottle |
|
13 | from bottle import get, hook, post, request, response, template |
||
14 | 1 | ||
15 | 1 | from doorstop import build, common, publisher, settings |
|
16 | 1 | from doorstop.common import HelpFormatter |
|
17 | 1 | from doorstop.core import vcs |
|
18 | 1 | from doorstop.server import utilities |
|
19 | |||
20 | 1 | log = common.logger(__name__) |
|
21 | |||
22 | 1 | app = utilities.StripPathMiddleware(bottle.app()) |
|
23 | 1 | tree = None # set in `run`, read in the route functions |
|
24 | 1 | numbers = defaultdict(int) # cache of next document numbers |
|
25 | |||
26 | |||
27 | 1 | def main(args=None): |
|
28 | """Process command-line arguments and run the program.""" |
||
29 | 1 | from doorstop import SERVER, VERSION |
|
30 | |||
31 | # Shared options |
||
32 | 1 | debug = argparse.ArgumentParser(add_help=False) |
|
33 | 1 | debug.add_argument('-V', '--version', action='version', version=VERSION) |
|
34 | 1 | debug.add_argument('--debug', action='store_true', help=argparse.SUPPRESS) |
|
35 | 1 | debug.add_argument('--launch', action='store_true', help=argparse.SUPPRESS) |
|
36 | 1 | shared = {'formatter_class': HelpFormatter, 'parents': [debug]} |
|
37 | |||
38 | # Build main parser |
||
39 | 1 | parser = argparse.ArgumentParser(prog=SERVER, description=__doc__, **shared) |
|
40 | cwd = os.getcwd() |
||
41 | 1 | ||
42 | 1 | parser.add_argument( |
|
43 | '-j', '--project', default=None, help="path to the root of the project" |
||
44 | 1 | ) |
|
45 | parser.add_argument( |
||
46 | 1 | '-P', |
|
47 | '--port', |
||
48 | metavar='NUM', |
||
49 | 1 | type=int, |
|
50 | default=settings.SERVER_PORT, |
||
51 | help="use a custom port for the server", |
||
52 | ) |
||
53 | 1 | parser.add_argument( |
|
54 | '-H', '--host', default='127.0.0.1', help="IP address to listen" |
||
55 | ) |
||
56 | 1 | parser.add_argument( |
|
57 | '-w', '--wsgi', action='store_true', help="Run as a WSGI process" |
||
58 | ) |
||
59 | parser.add_argument( |
||
60 | 1 | '-b', |
|
61 | '--baseurl', |
||
62 | default='', |
||
63 | 1 | help="Base URL this is served at (Usually only necessary for WSGI)", |
|
64 | ) |
||
65 | |||
66 | # Parse arguments |
||
67 | args = parser.parse_args(args=args) |
||
68 | |||
69 | if args.project is None: |
||
70 | args.project = vcs.find_root(cwd) |
||
71 | |||
72 | 1 | # Configure logging |
|
73 | 1 | logging.basicConfig( |
|
74 | 1 | format=settings.VERBOSE_LOGGING_FORMAT, level=settings.VERBOSE_LOGGING_LEVEL |
|
75 | 1 | ) |
|
76 | 1 | ||
77 | 1 | # Run the program |
|
78 | 1 | run(args, os.getcwd(), parser.error) |
|
79 | 1 | ||
80 | |||
81 | def run(args, cwd, _): |
||
82 | """Start the server. |
||
83 | 1 | ||
84 | :param args: Namespace of CLI arguments (from this module or the CLI) |
||
85 | :param cwd: current working directory |
||
86 | :param error: function to call for CLI errors |
||
87 | |||
88 | """ |
||
89 | 1 | global tree # pylint: disable=W0603 |
|
90 | tree = build(cwd=cwd, root=args.project) |
||
91 | tree.load() |
||
92 | 1 | host = args.host |
|
93 | 1 | port = args.port or settings.SERVER_PORT |
|
94 | 1 | bottle.TEMPLATE_PATH.insert( |
|
95 | 0, os.path.join(os.path.dirname(__file__), '..', 'views') |
||
96 | ) |
||
97 | 1 | ||
98 | # If you started without WSGI, the base will be '/'. |
||
99 | if args.baseurl == '' and not args.wsgi: |
||
100 | 1 | args.baseurl = '/' |
|
101 | 1 | ||
102 | 1 | # If you specified a base URL, make sure it ends with '/'. |
|
103 | 1 | if args.baseurl != '' and not args.baseurl.endswith('/'): |
|
104 | args.baseurl += '/' |
||
105 | 1 | ||
106 | bottle.SimpleTemplate.defaults['baseurl'] = args.baseurl |
||
107 | bottle.SimpleTemplate.defaults['navigation'] = True |
||
108 | 1 | ||
109 | if args.launch: |
||
110 | url = utilities.build_url(host=host, port=port) |
||
111 | 1 | webbrowser.open(url) |
|
112 | 1 | if not args.wsgi: |
|
113 | 1 | bottle.run(app=app, host=host, port=port, debug=args.debug, reloader=args.debug) |
|
114 | |||
115 | 1 | ||
116 | 1 | @hook('before_request') |
|
117 | def strip_path(): |
||
118 | request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('/') |
||
119 | 1 | request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('.html') |
|
120 | |||
121 | |||
122 | 1 | @hook('after_request') |
|
123 | 1 | def enable_cors(): |
|
124 | 1 | """Allow a webserver running on the same machine to access data.""" |
|
125 | 1 | response.headers['Access-Control-Allow-Origin'] = '*' |
|
126 | |||
127 | 1 | ||
128 | @get('/') |
||
129 | def index(): |
||
130 | 1 | """Read the tree.""" |
|
131 | yield template('index', tree_code=tree.draw(html_links=True)) |
||
132 | |||
133 | 1 | ||
134 | 1 | @get('/documents') |
|
135 | 1 | def get_documents(): |
|
136 | 1 | """Read the tree's documents.""" |
|
137 | 1 | prefixes = [str(document.prefix) for document in tree] |
|
138 | if utilities.json_response(request): |
||
139 | 1 | data = {'prefixes': prefixes} |
|
140 | return data |
||
141 | else: |
||
142 | 1 | return template('document_list', prefixes=prefixes) |
|
143 | |||
144 | |||
145 | 1 | @get('/documents/all') |
|
146 | 1 | def get_all_documents(): |
|
147 | 1 | """Read the tree's documents.""" |
|
148 | 1 | if utilities.json_response(request): |
|
149 | data = {str(d.prefix): {str(i.uid): i.data for i in d} for d in tree} |
||
150 | 1 | return data |
|
151 | else: |
||
152 | prefixes = [str(document.prefix) for document in tree] |
||
153 | 1 | return template('document_list', prefixes=prefixes) |
|
154 | |||
155 | |||
156 | 1 | @get('/documents/<prefix>') |
|
157 | 1 | def get_document(prefix): |
|
158 | 1 | """Read a tree's document.""" |
|
159 | 1 | document = tree.find_document(prefix) |
|
160 | 1 | if utilities.json_response(request): |
|
161 | 1 | data = {str(item.uid): item.data for item in document} |
|
162 | return data |
||
163 | 1 | else: |
|
164 | return publisher.publish_lines(document, ext='.html', linkify=True) |
||
165 | |||
166 | 1 | ||
167 | @get('/documents/<prefix>/items') |
||
168 | def get_items(prefix): |
||
169 | 1 | """Read a document's items.""" |
|
170 | 1 | document = tree.find_document(prefix) |
|
171 | 1 | uids = [str(item.uid) for item in document] |
|
172 | 1 | if utilities.json_response(request): |
|
173 | 1 | data = {'uids': uids} |
|
174 | 1 | return data |
|
175 | else: |
||
176 | 1 | return template('item_list', prefix=prefix, items=uids) |
|
177 | 1 | ||
178 | 1 | ||
179 | 1 | @get('/documents/<prefix>/items/<uid>') |
|
180 | 1 | def get_item(prefix, uid): |
|
181 | 1 | """Read a document's item.""" |
|
182 | document = tree.find_document(prefix) |
||
183 | item = document.find_item(uid) |
||
184 | 1 | if utilities.json_response(request): |
|
185 | return {'data': item.data} |
||
186 | else: |
||
187 | 1 | return publisher.publish_lines(item, ext='.html') |
|
188 | 1 | ||
189 | 1 | ||
190 | 1 | @get('/documents/<prefix>/items/<uid>/attrs') |
|
191 | 1 | def get_attrs(prefix, uid): |
|
192 | 1 | """Read an item's attributes.""" |
|
193 | document = tree.find_document(prefix) |
||
194 | 1 | item = document.find_item(uid) |
|
195 | attrs = sorted(item.data.keys()) |
||
196 | if utilities.json_response(request): |
||
197 | data = {'attrs': attrs} |
||
198 | return data |
||
199 | else: |
||
200 | return '<br>'.join(attrs) |
||
201 | |||
202 | |||
203 | @get('/documents/<prefix>/items/<uid>/attrs/<name>') |
||
204 | def get_attr(prefix, uid, name): |
||
205 | """Read an item's attribute value.""" |
||
206 | document = tree.find_document(prefix) |
||
207 | item = document.find_item(uid) |
||
208 | value = item.data.get(name, None) |
||
209 | if utilities.json_response(request): |
||
210 | data = {'value': value} |
||
211 | return data |
||
212 | else: |
||
213 | if isinstance(value, str): |
||
214 | return value |
||
215 | try: |
||
216 | return '<br>'.join(str(e) for e in value) |
||
217 | except TypeError: |
||
218 | return str(value) |
||
219 | |||
220 | |||
221 | @get('/assets/doorstop/<filename>') |
||
222 | def get_assets(filename): |
||
223 | """Serve static files. Mainly used to serve CSS files and javascript.""" |
||
224 | public_dir = os.path.join( |
||
225 | os.path.dirname(__file__), '..', 'core', 'files', 'assets', 'doorstop' |
||
226 | ) |
||
227 | return bottle.static_file(filename, root=public_dir) |
||
228 | |||
229 | |||
230 | @post('/documents/<prefix>/numbers') |
||
231 | def post_numbers(prefix): |
||
232 | """Create the next number in a document.""" |
||
233 | document = tree.find_document(prefix) |
||
234 | number = max(document.next_number, numbers[prefix]) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
235 | numbers[prefix] = number + 1 |
||
236 | if utilities.json_response(request): |
||
237 | data = {'next': number} |
||
238 | return data |
||
239 | else: |
||
240 | return str(number) |
||
241 | |||
242 | |||
243 | if __name__ == '__main__': |
||
244 | main() |
||
245 |