3
3
# deepzoom_multiserver - Example web application for viewing multiple slides
4
4
#
5
5
# Copyright (c) 2010-2015 Carnegie Mellon University
6
- # Copyright (c) 2021-2023 Benjamin Gilbert
6
+ # Copyright (c) 2021-2024 Benjamin Gilbert
7
7
#
8
8
# This library is free software; you can redistribute it and/or modify it
9
9
# under the terms of version 2.1 of the GNU Lesser General Public License
27
27
from collections .abc import Callable
28
28
from io import BytesIO
29
29
import os
30
+ from pathlib import Path , PurePath
30
31
from threading import Lock
31
32
from typing import TYPE_CHECKING , Any , Literal
32
33
import zlib
82
83
83
84
84
85
class DeepZoomMultiServer (Flask ):
85
- basedir : str
86
+ basedir : Path
86
87
cache : _SlideCache
87
88
88
89
@@ -94,7 +95,7 @@ class AnnotatedDeepZoomGenerator(DeepZoomGenerator):
94
95
95
96
def create_app (
96
97
config : dict [str , Any ] | None = None ,
97
- config_file : str | None = None ,
98
+ config_file : Path | None = None ,
98
99
) -> Flask :
99
100
# Create and configure app
100
101
app = DeepZoomMultiServer (__name__ )
@@ -116,7 +117,7 @@ def create_app(
116
117
app .config .from_mapping (config )
117
118
118
119
# Set up cache
119
- app .basedir = os . path . abspath (app .config ['SLIDE_DIR' ])
120
+ app .basedir = Path (app .config ['SLIDE_DIR' ]). resolve ( strict = True )
120
121
config_map = {
121
122
'DEEPZOOM_TILE_SIZE' : 'tile_size' ,
122
123
'DEEPZOOM_OVERLAP' : 'overlap' ,
@@ -131,16 +132,18 @@ def create_app(
131
132
)
132
133
133
134
# Helper functions
134
- def get_slide (path : str ) -> AnnotatedDeepZoomGenerator :
135
- path = os .path .abspath (os .path .join (app .basedir , path ))
136
- if not path .startswith (app .basedir + os .path .sep ):
137
- # Directory traversal
135
+ def get_slide (user_path : PurePath ) -> AnnotatedDeepZoomGenerator :
136
+ try :
137
+ path = (app .basedir / user_path ).resolve (strict = True )
138
+ except OSError :
139
+ # Does not exist
138
140
abort (404 )
139
- if not os .path .exists (path ):
141
+ if path .parts [: len (app .basedir .parts )] != app .basedir .parts :
142
+ # Directory traversal
140
143
abort (404 )
141
144
try :
142
145
slide = app .cache .get (path )
143
- slide .filename = os . path .basename ( path )
146
+ slide .filename = path .name
144
147
return slide
145
148
except OpenSlideError :
146
149
abort (404 )
@@ -152,7 +155,7 @@ def index() -> str:
152
155
153
156
@app .route ('/<path:path>' )
154
157
def slide (path : str ) -> str :
155
- slide = get_slide (path )
158
+ slide = get_slide (PurePath ( path ) )
156
159
slide_url = url_for ('dzi' , path = path )
157
160
return render_template (
158
161
'slide-fullpage.html' ,
@@ -163,15 +166,15 @@ def slide(path: str) -> str:
163
166
164
167
@app .route ('/<path:path>.dzi' )
165
168
def dzi (path : str ) -> Response :
166
- slide = get_slide (path )
169
+ slide = get_slide (PurePath ( path ) )
167
170
format = app .config ['DEEPZOOM_FORMAT' ]
168
171
resp = make_response (slide .get_dzi (format ))
169
172
resp .mimetype = 'application/xml'
170
173
return resp
171
174
172
175
@app .route ('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>' )
173
176
def tile (path : str , level : int , col : int , row : int , format : str ) -> Response :
174
- slide = get_slide (path )
177
+ slide = get_slide (PurePath ( path ) )
175
178
format = format .lower ()
176
179
if format != 'jpeg' and format != 'png' :
177
180
# Not supported by Deep Zoom
@@ -208,7 +211,7 @@ def __init__(
208
211
self .dz_opts = dz_opts
209
212
self .color_mode = color_mode
210
213
self ._lock = Lock ()
211
- self ._cache : OrderedDict [str , AnnotatedDeepZoomGenerator ] = OrderedDict ()
214
+ self ._cache : OrderedDict [Path , AnnotatedDeepZoomGenerator ] = OrderedDict ()
212
215
# Share a single tile cache among all slide handles, if supported
213
216
try :
214
217
self ._tile_cache : OpenSlideCache | None = OpenSlideCache (
@@ -217,7 +220,7 @@ def __init__(
217
220
except OpenSlideVersionError :
218
221
self ._tile_cache = None
219
222
220
- def get (self , path : str ) -> AnnotatedDeepZoomGenerator :
223
+ def get (self , path : Path ) -> AnnotatedDeepZoomGenerator :
221
224
with self ._lock :
222
225
if path in self ._cache :
223
226
# Move to end of LRU
@@ -286,13 +289,14 @@ def xfrm(img: Image.Image) -> None:
286
289
287
290
288
291
class _Directory :
289
- def __init__ (self , basedir : str , relpath : str = '' ):
290
- self .name = os .path .basename (relpath )
292
+ _DEFAULT_RELPATH = PurePath ('.' )
293
+
294
+ def __init__ (self , basedir : Path , relpath : PurePath = _DEFAULT_RELPATH ):
295
+ self .name = relpath .name
291
296
self .children : list [_Directory | _SlideFile ] = []
292
- for name in sorted (os .listdir (os .path .join (basedir , relpath ))):
293
- cur_relpath = os .path .join (relpath , name )
294
- cur_path = os .path .join (basedir , cur_relpath )
295
- if os .path .isdir (cur_path ):
297
+ for cur_path in sorted ((basedir / relpath ).iterdir ()):
298
+ cur_relpath = relpath / cur_path .name
299
+ if cur_path .is_dir ():
296
300
cur_dir = _Directory (basedir , cur_relpath )
297
301
if cur_dir .children :
298
302
self .children .append (cur_dir )
@@ -301,9 +305,9 @@ def __init__(self, basedir: str, relpath: str = ''):
301
305
302
306
303
307
class _SlideFile :
304
- def __init__ (self , relpath : str ):
305
- self .name = os . path . basename ( relpath )
306
- self .url_path = relpath
308
+ def __init__ (self , relpath : PurePath ):
309
+ self .name = relpath . name
310
+ self .url_path = relpath . as_posix ()
307
311
308
312
309
313
if __name__ == '__main__' :
@@ -336,7 +340,7 @@ def __init__(self, relpath: str):
336
340
),
337
341
)
338
342
parser .add_argument (
339
- '-c' , '--config' , metavar = 'FILE' , dest = 'config' , help = 'config file'
343
+ '-c' , '--config' , metavar = 'FILE' , type = Path , dest = 'config' , help = 'config file'
340
344
)
341
345
parser .add_argument (
342
346
'-d' ,
@@ -396,6 +400,7 @@ def __init__(self, relpath: str):
396
400
parser .add_argument (
397
401
'SLIDE_DIR' ,
398
402
metavar = 'SLIDE-DIRECTORY' ,
403
+ type = Path ,
399
404
nargs = '?' ,
400
405
help = 'slide directory' ,
401
406
)
0 commit comments