33# deepzoom_multiserver - Example web application for viewing multiple slides
44#
55# Copyright (c) 2010-2015 Carnegie Mellon University
6- # Copyright (c) 2021-2023 Benjamin Gilbert
6+ # Copyright (c) 2021-2024 Benjamin Gilbert
77#
88# This library is free software; you can redistribute it and/or modify it
99# under the terms of version 2.1 of the GNU Lesser General Public License
2727from collections .abc import Callable
2828from io import BytesIO
2929import os
30+ from pathlib import Path , PurePath
3031from threading import Lock
3132from typing import TYPE_CHECKING , Any , Literal
3233import zlib
8283
8384
8485class DeepZoomMultiServer (Flask ):
85- basedir : str
86+ basedir : Path
8687 cache : _SlideCache
8788
8889
@@ -94,7 +95,7 @@ class AnnotatedDeepZoomGenerator(DeepZoomGenerator):
9495
9596def create_app (
9697 config : dict [str , Any ] | None = None ,
97- config_file : str | None = None ,
98+ config_file : Path | None = None ,
9899) -> Flask :
99100 # Create and configure app
100101 app = DeepZoomMultiServer (__name__ )
@@ -116,7 +117,7 @@ def create_app(
116117 app .config .from_mapping (config )
117118
118119 # Set up cache
119- app .basedir = os . path . abspath (app .config ['SLIDE_DIR' ])
120+ app .basedir = Path (app .config ['SLIDE_DIR' ]). resolve ( strict = True )
120121 config_map = {
121122 'DEEPZOOM_TILE_SIZE' : 'tile_size' ,
122123 'DEEPZOOM_OVERLAP' : 'overlap' ,
@@ -131,16 +132,18 @@ def create_app(
131132 )
132133
133134 # 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
138140 abort (404 )
139- if not os .path .exists (path ):
141+ if path .parts [: len (app .basedir .parts )] != app .basedir .parts :
142+ # Directory traversal
140143 abort (404 )
141144 try :
142145 slide = app .cache .get (path )
143- slide .filename = os . path .basename ( path )
146+ slide .filename = path .name
144147 return slide
145148 except OpenSlideError :
146149 abort (404 )
@@ -152,7 +155,7 @@ def index() -> str:
152155
153156 @app .route ('/<path:path>' )
154157 def slide (path : str ) -> str :
155- slide = get_slide (path )
158+ slide = get_slide (PurePath ( path ) )
156159 slide_url = url_for ('dzi' , path = path )
157160 return render_template (
158161 'slide-fullpage.html' ,
@@ -163,15 +166,15 @@ def slide(path: str) -> str:
163166
164167 @app .route ('/<path:path>.dzi' )
165168 def dzi (path : str ) -> Response :
166- slide = get_slide (path )
169+ slide = get_slide (PurePath ( path ) )
167170 format = app .config ['DEEPZOOM_FORMAT' ]
168171 resp = make_response (slide .get_dzi (format ))
169172 resp .mimetype = 'application/xml'
170173 return resp
171174
172175 @app .route ('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>' )
173176 def tile (path : str , level : int , col : int , row : int , format : str ) -> Response :
174- slide = get_slide (path )
177+ slide = get_slide (PurePath ( path ) )
175178 format = format .lower ()
176179 if format != 'jpeg' and format != 'png' :
177180 # Not supported by Deep Zoom
@@ -208,7 +211,7 @@ def __init__(
208211 self .dz_opts = dz_opts
209212 self .color_mode = color_mode
210213 self ._lock = Lock ()
211- self ._cache : OrderedDict [str , AnnotatedDeepZoomGenerator ] = OrderedDict ()
214+ self ._cache : OrderedDict [Path , AnnotatedDeepZoomGenerator ] = OrderedDict ()
212215 # Share a single tile cache among all slide handles, if supported
213216 try :
214217 self ._tile_cache : OpenSlideCache | None = OpenSlideCache (
@@ -217,7 +220,7 @@ def __init__(
217220 except OpenSlideVersionError :
218221 self ._tile_cache = None
219222
220- def get (self , path : str ) -> AnnotatedDeepZoomGenerator :
223+ def get (self , path : Path ) -> AnnotatedDeepZoomGenerator :
221224 with self ._lock :
222225 if path in self ._cache :
223226 # Move to end of LRU
@@ -286,13 +289,14 @@ def xfrm(img: Image.Image) -> None:
286289
287290
288291class _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
291296 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 ():
296300 cur_dir = _Directory (basedir , cur_relpath )
297301 if cur_dir .children :
298302 self .children .append (cur_dir )
@@ -301,9 +305,9 @@ def __init__(self, basedir: str, relpath: str = ''):
301305
302306
303307class _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 ()
307311
308312
309313if __name__ == '__main__' :
@@ -336,7 +340,7 @@ def __init__(self, relpath: str):
336340 ),
337341 )
338342 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'
340344 )
341345 parser .add_argument (
342346 '-d' ,
@@ -396,6 +400,7 @@ def __init__(self, relpath: str):
396400 parser .add_argument (
397401 'SLIDE_DIR' ,
398402 metavar = 'SLIDE-DIRECTORY' ,
403+ type = Path ,
399404 nargs = '?' ,
400405 help = 'slide directory' ,
401406 )
0 commit comments