44from pathlib import Path
55from typing import BinaryIO , List , Optional , Set
66
7- from flask import current_app
7+ from flask import abort , current_app
88from gramps .gen .lib import Media
99from gramps .gen .utils .file import expand_media_path
1010
11+ from ..auth import get_tree_usage , set_tree_usage
1112from ..types import FilenameOrPath
1213from ..util import get_extension
1314from .file import FileHandler , LocalFileHandler , upload_file_local
14- from .s3 import ObjectStorageFileHandler , list_object_keys , upload_file_s3
15- from .util import get_db_handle
15+ from .s3 import (
16+ ObjectStorageFileHandler ,
17+ get_object_keys_size ,
18+ upload_file_s3 ,
19+ )
20+ from .util import get_db_handle , get_tree_from_jwt
1621
1722
1823PREFIX_S3 = "s3://"
@@ -60,6 +65,10 @@ def filter_existing_files(self, objects: List[Media]) -> List[Media]:
6065 """Given a list of media objects, return the ones with existing files."""
6166 raise NotImplementedError
6267
68+ def get_media_size (self ) -> int :
69+ """Return the total disk space used by all existing media objects."""
70+ raise NotImplementedError
71+
6372
6473class MediaHandlerLocal (MediaHandlerBase ):
6574 """Handler for local media files."""
@@ -80,7 +89,7 @@ def upload_file(
8089 if Path (path ).is_absolute ():
8190 # Don't allow absolute paths! This will raise
8291 # if path is not relative to base_dir
83- rel_path : FilenameOrPath = Path (path ).relative_to (base_dir )
92+ rel_path : FilenameOrPath = Path (path ).relative_to (self . base_dir )
8493 else :
8594 rel_path = path
8695 upload_file_local (self .base_dir , rel_path , stream )
@@ -94,6 +103,29 @@ def filter_existing_files(self, objects: List[Media]) -> List[Media]:
94103 obj for obj in objects if self .get_file_handler (obj .handle ).file_exists ()
95104 ]
96105
106+ def get_media_size (self ) -> int :
107+ """Return the total disk space used by all existing media objects.
108+
109+ Only works with a request context.
110+ """
111+ if not os .path .isdir (self .base_dir ):
112+ raise ValueError (f"Directory { self .base_dir } does not exist" )
113+ size = 0
114+ paths_seen = set ()
115+ db_handle = get_db_handle ()
116+ for obj in db_handle .iter_media ():
117+ path = obj .path
118+ if os .path .isabs (path ):
119+ if Path (self .base_dir ).resolve () not in Path (path ).resolve ().parents :
120+ continue # file outside base dir - ignore
121+ else :
122+ path = os .path .join (self .base_dir , path )
123+ if Path (path ).is_file () and path not in paths_seen :
124+ file_size = os .path .getsize (path )
125+ size += file_size
126+ paths_seen .add (path )
127+ return size
128+
97129
98130class MediaHandlerS3 (MediaHandlerBase ):
99131 """Generic handler for object storage media files."""
@@ -124,7 +156,9 @@ def prefix(self) -> Optional[str]:
124156
125157 def get_remote_keys (self ) -> Set [str ]:
126158 """Return the set of all object keys that are known to exist on remote."""
127- keys = list_object_keys (self .bucket_name , endpoint_url = self .endpoint_url )
159+ keys = get_object_keys_size (
160+ self .bucket_name , prefix = self .prefix , endpoint_url = self .endpoint_url
161+ )
128162 return set (removeprefix (key , self .prefix or "" ).lstrip ("/" ) for key in keys )
129163
130164 def get_file_handler (self , handle ) -> ObjectStorageFileHandler :
@@ -160,6 +194,17 @@ def filter_existing_files(self, objects: List[Media]) -> List[Media]:
160194 remote_keys = self .get_remote_keys ()
161195 return [obj for obj in objects if obj .checksum in remote_keys ]
162196
197+ def get_media_size (self ) -> int :
198+ """Return the total disk space used by all existing media objects."""
199+ db_handle = get_db_handle ()
200+ keys = set (obj .checksum for obj in db_handle .iter_media ())
201+ keys_size = get_object_keys_size (
202+ bucket_name = self .bucket_name ,
203+ prefix = self .prefix ,
204+ endpoint_url = self .endpoint_url ,
205+ )
206+ return sum (keys_size .get (key , 0 ) for key in keys )
207+
163208
164209def MediaHandler (base_dir : Optional [str ]) -> MediaHandlerBase :
165210 """Return an appropriate media handler."""
@@ -193,3 +238,27 @@ def get_media_handler(tree: Optional[str] = None) -> MediaHandlerBase:
193238 # construct subdirectory using OS dependent path join
194239 base_dir = os .path .join (base_dir , prefix )
195240 return MediaHandler (base_dir )
241+
242+
243+ def update_usage_media () -> int :
244+ """Update the usage of media."""
245+ tree = get_tree_from_jwt ()
246+ media_handler = get_media_handler (tree = tree )
247+ usage_media = media_handler .get_media_size ()
248+ set_tree_usage (tree , usage_media = usage_media )
249+ return usage_media
250+
251+
252+ def check_quota_media (to_add : int ) -> None :
253+ """Check whether the quota allows adding `to_add` bytes and abort if not."""
254+ tree = get_tree_from_jwt ()
255+ usage_dict = get_tree_usage (tree )
256+ if not usage_dict or usage_dict .get ("usage_media" ) is None :
257+ update_usage_media ()
258+ usage_dict = get_tree_usage (tree )
259+ usage = usage_dict ["usage_media" ]
260+ quota = usage_dict .get ("quota_media" )
261+ if quota is None :
262+ return
263+ if usage + to_add > quota :
264+ abort (405 )
0 commit comments