1- from os import path , mkdir , remove
2- from mimetypes import guess_type
3- from sh import convert
4- from glob import glob
5- from urllib import pathname2url , quote
61from collections import defaultdict , OrderedDict
72from functools import wraps
8- import exifread , json , hmac , time
3+ from glob import glob
4+ from mimetypes import guess_type
5+ from os import path , mkdir , remove
6+ from urllib .parse import quote
7+ from urllib .request import pathname2url
98
9+ import exifread
10+ import hmac
11+ import json
12+ import time
13+ from sh import convert
14+
15+ import settings
1016from bottle import (
1117 Response , request , response , static_file , template , abort ,
12- HTTPError , HTTPResponse , route , hook )
18+ HTTPResponse , route )
1319
14- import settings
1520
1621def log (msg ):
1722 if settings .DEBUG :
18- print msg
23+ print (msg )
24+
1925
2026def get_rel_path (coll , thumb_p ):
2127 """Return originals or thumbnails subdirectory of the main
@@ -33,6 +39,7 @@ def get_rel_path(coll, thumb_p):
3339
3440 return path .join (coll_dir , type_dir )
3541
42+
3643def generate_token (timestamp , filename ):
3744 """Generate the auth token for the given filename and timestamp.
3845 This is for comparing to the client submited token.
@@ -41,16 +48,19 @@ def generate_token(timestamp, filename):
4148 mac = hmac .new (settings .KEY , timestamp + filename )
4249 return ':' .join ((mac .hexdigest (), timestamp ))
4350
51+
4452class TokenException (Exception ):
4553 """Raised when an auth token is invalid for some reason."""
4654 pass
4755
56+
4857def get_timestamp ():
4958 """Return an integer timestamp with one second resolution for
5059 the current moment.
5160 """
5261 return int (time .time ())
5362
63+
5464def validate_token (token_in , filename ):
5565 """Validate the input token for given filename using the secret key
5666 in settings. Checks that the token is within the time tolerance and
@@ -77,6 +87,7 @@ def validate_token(token_in, filename):
7787 if token_in != generate_token (timestamp , filename ):
7888 raise TokenException ("Auth token is invalid." )
7989
90+
8091def require_token (filename_param , always = False ):
8192 """Decorate a view function to require an auth token to be present for access.
8293
@@ -89,6 +100,7 @@ def require_token(filename_param, always=False):
89100 Automatically adds the X-Timestamp header to responses to help clients stay
90101 syncronized.
91102 """
103+
92104 def decorator (func ):
93105 @include_timestamp
94106 @wraps (func )
@@ -102,23 +114,30 @@ def wrapper(*args, **kwargs):
102114 response .status = 403
103115 return e
104116 return func (* args , ** kwargs )
117+
105118 return wrapper
119+
106120 return decorator
107121
122+
108123def include_timestamp (func ):
109124 """Decorate a view function to include the X-Timestamp header to help clients
110125 maintain time syncronization.
111126 """
127+
112128 @wraps (func )
113129 def wrapper (* args , ** kwargs ):
114130 result = func (* args , ** kwargs )
115131 (result if isinstance (result , Response ) else response ) \
116- .set_header ('X-Timestamp' , str (get_timestamp ()))
132+ .set_header ('X-Timestamp' , str (get_timestamp ()))
117133 return result
134+
118135 return wrapper
119136
137+
120138def allow_cross_origin (func ):
121139 """Decorate a view function to allow cross domain access."""
140+
122141 @wraps (func )
123142 def wrapper (* args , ** kwargs ):
124143 try :
@@ -128,10 +147,12 @@ def wrapper(*args, **kwargs):
128147 raise
129148
130149 (result if isinstance (result , Response ) else response ) \
131- .set_header ('Access-Control-Allow-Origin' , '*' )
150+ .set_header ('Access-Control-Allow-Origin' , '*' )
132151 return result
152+
133153 return wrapper
134154
155+
135156def resolve_file ():
136157 """Inspect the request object to determine the file being requested.
137158 If the request is for a thumbnail and it has not been generated, do
@@ -179,21 +200,23 @@ def resolve_file():
179200 input_spec = orig_path
180201 convert_args = ('-resize' , "%dx%d>" % (scale , scale ))
181202 if mimetype == 'application/pdf' :
182- input_spec += '[0]' # only thumbnail first page of PDF
203+ input_spec += '[0]' # only thumbnail first page of PDF
183204 convert_args += ('-background' , 'white' , '-flatten' ) # add white background to PDFs
184205
185206 log ("Scaling thumbnail to %d" % scale )
186207 convert (input_spec , * (convert_args + (scaled_pathname ,)))
187208
188209 return path .join (relpath , scaled_name )
189210
211+
190212@route ('/static/<path:path>' )
191213def static (path ):
192214 """Serve static files to the client. Primarily for Web Portal."""
193215 if not settings .ALLOW_STATIC_FILE_ACCESS :
194216 abort (404 )
195217 return static_file (path , root = settings .BASE_DIR )
196218
219+
197220@route ('/getfileref' )
198221@allow_cross_origin
199222def getfileref ():
@@ -203,6 +226,8 @@ def getfileref():
203226 response .content_type = 'text/plain; charset=utf-8'
204227 return "http://%s:%d/static/%s" % (settings .HOST , settings .PORT ,
205228 pathname2url (resolve_file ()))
229+
230+
206231@route ('/fileget' )
207232@require_token ('filename' )
208233def fileget ():
@@ -214,12 +239,14 @@ def fileget():
214239 r .set_header ('Content-Disposition' , "inline; filename*=utf-8''%s" % download_name )
215240 return r
216241
242+
217243@route ('/fileupload' , method = 'OPTIONS' )
218244@allow_cross_origin
219245def fileupload_options ():
220246 response .content_type = "text/plain; charset=utf-8"
221247 return ''
222248
249+
223250@route ('/fileupload' , method = 'POST' )
224251@allow_cross_origin
225252@require_token ('store' )
@@ -238,12 +265,13 @@ def fileupload():
238265 if not path .exists (basepath ):
239266 mkdir (basepath )
240267
241- upload = request .files .values ()[0 ]
268+ upload = list ( request .files .values () )[0 ]
242269 upload .save (pathname , overwrite = True )
243270
244271 response .content_type = 'text/plain; charset=utf-8'
245272 return 'Ok.'
246273
274+
247275@route ('/filedelete' , method = 'POST' )
248276@require_token ('filename' )
249277def filedelete ():
@@ -271,6 +299,7 @@ def filedelete():
271299 response .content_type = 'text/plain; charset=utf-8'
272300 return 'Ok.'
273301
302+
274303@route ('/getmetadata' )
275304@require_token ('filename' )
276305def getmetadata ():
@@ -297,7 +326,7 @@ def getmetadata():
297326 abort (404 , 'DateTime not found in EXIF' )
298327
299328 data = defaultdict (dict )
300- for key , value in tags .items ():
329+ for key , value in list ( tags .items () ):
301330 parts = key .split ()
302331 if len (parts ) < 2 : continue
303332 try :
@@ -308,28 +337,32 @@ def getmetadata():
308337 data [parts [0 ]][parts [1 ]] = str (v )
309338
310339 response .content_type = 'application/json'
311- data = [OrderedDict ( (('Name' , key ), ('Fields' , value )) )
312- for key ,value in data .items ()]
340+ data = [OrderedDict ((('Name' , key ), ('Fields' , value )))
341+ for key , value in list ( data .items () )]
313342
314343 return json .dumps (data , indent = 4 )
315344
345+
316346@route ('/testkey' )
317347@require_token ('random' , always = True )
318348def testkey ():
319349 """If access to this resource succeeds, clients can conclude
320350 that they have a valid access key.
321351 """
322- response .content_type = 'text/plain; charset=utf-8'
352+ response .content_type = 'text/plain; charset=utf-8'
323353 return 'Ok.'
324354
355+
325356@route ('/web_asset_store.xml' )
326357@include_timestamp
327358def web_asset_store ():
328359 """Serve an XML description of the URLs available here."""
329360 response .content_type = 'text/xml; charset=utf-8'
330361 return template ('web_asset_store.xml' , host = "%s:%d" % (settings .HOST , settings .PORT ))
331362
363+
332364if __name__ == '__main__' :
333365 from bottle import run
366+
334367 run (host = '0.0.0.0' , port = settings .PORT , server = settings .SERVER ,
335368 debug = settings .DEBUG , reloader = settings .DEBUG )
0 commit comments