@@ -169,16 +169,17 @@ def get_aips_df(self) -> pd.DataFrame:
169169 for that AIP. To do this, the AIP UUID -- which is minted in Archivematica -- is
170170 extracted from the S3 key.
171171
172- Example s3 key for an AIP:
172+ Example s3 prefix for an AIP:
173173 5b33/1bf3/eb1f/4017/bbe8/c24a/9f60/f4cd/2014_039_002-5b331bf3-eb1f-4017-bbe8-c24a9f60f4cd
174174
175175 Where the UUID can be seen as both part of the leading pairtree path and at the
176176 end of the key:
177177 5b331bf3-eb1f-4017-bbe8-c24a9f60f4cd
178178
179- The following regex is used in this DuckDB SQL query to find a valid UUID in the
180- S3 key:
181- [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
179+ A regular expression is used to extract AIP UUIDs from S3 keys by locating the
180+ last valid UUID before files like `/bagit.txt` or `/bag-info.txt`. There is some
181+ room for false positives here, but other checks confirm the validity of bags after
182+ a UUID + prefix key is identified from this Inventory data.
182183
183184 The dataframe includes information such as:
184185 - AIP UUID
@@ -190,47 +191,78 @@ def get_aips_df(self) -> pd.DataFrame:
190191 if self ._aips_df is not None :
191192 return self ._aips_df
192193
193- # ruff: noqa: E501
194+ aip_regex = (
195+ """(.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))/(.*)"""
196+ )
197+ # ruff: noqa: E501, UP032
194198 query = """
195- with cdps_aip_inventory as (
196- select * from inventory
197- where is_latest
198- and not is_delete_marker
199- ),
200- cdps_aip_inventory_with_aip_uuid as (
201- select
202- bucket,
203- -- extract the *first* UUID encountered in the key
204- case
205- when key ~ '.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*'
206- then regexp_extract(key, '.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*', 1)
207- else null
208- end as aip_uuid,
209- -- extract the inventory root as they key up until, and inclusive, of the first UUID
210- case
211- when key ~ '.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*'
212- then regexp_extract(key, '(.+?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*', 1)
213- else null
214- end as aip_s3_key,
215- key,
216- size,
217- last_modified_date
218- from cdps_aip_inventory
199+ -- CTE of all inventory data rows
200+ with cdps_aip_inventory as (
201+ select * from inventory
202+ where is_latest
203+ and not is_delete_marker
204+ ),
205+ -- CTE that attempts to extract UUID and AIP prefix from S3 object keys
206+ cdps_aip_inventory_with_aip_uuid as (
207+ select
208+ bucket,
209+ -- the 2nd group in the regex is the AIP UUID
210+ case
211+ when key ~ '{aip_regex}'
212+ then regexp_extract(key, '{aip_regex}', 2)
213+ end as aip_uuid,
214+ -- the 1st group in the regex match is the S3 prefix up until,
215+ -- and including, the AIP UUID
216+ case
217+ when key ~ '{aip_regex}'
218+ then regexp_extract(key, '{aip_regex}', 1)
219+ end as aip_s3_key,
220+ -- the 3rd group is any file suffix after the AIP UUID
221+ case
222+ when key ~ '{aip_regex}'
223+ then regexp_extract(key, '{aip_regex}', 3)
224+ end as aip_suffix,
225+ key,
226+ size,
227+ last_modified_date
228+ from cdps_aip_inventory
229+ ),
230+ -- CTE that groups AIPs by UUID and prefix
231+ aips as (
232+ select
233+ bucket,
234+ aip_uuid,
235+ aip_s3_key,
236+ concat('s3://', bucket, '/', aip_s3_key) as aip_s3_uri,
237+ count(*) as aip_files_count,
238+ sum(size) as total_size_bytes,
239+ min(last_modified_date) as earliest_file_date,
240+ max(last_modified_date) as latest_file_date,
241+ list(aip_suffix)::json as aip_file_keys
242+ from cdps_aip_inventory_with_aip_uuid
243+ where aip_uuid is not null
244+ group by bucket, aip_uuid, aip_s3_key, aip_s3_uri
245+ ),
246+ -- CTE that limits to what appear to be valid Bagit AIPs (has 'bagit.txt')
247+ bagit_aips as (
248+ select
249+ bucket,
250+ aip_uuid,
251+ aip_s3_key,
252+ aip_s3_uri,
253+ aip_files_count,
254+ total_size_bytes,
255+ earliest_file_date,
256+ latest_file_date
257+ from aips
258+ where 'bagit.txt' in aip_file_keys
259+ )
260+ select * from bagit_aips
261+ order by aip_files_count desc
262+ ;
263+ """ .format (
264+ aip_regex = aip_regex
219265 )
220- select
221- bucket,
222- aip_uuid,
223- aip_s3_key,
224- concat('s3://', bucket, '/', aip_s3_key) as aip_s3_uri,
225- count(*) as aip_files_count,
226- sum(size) as total_size_bytes,
227- min(last_modified_date) as earliest_file_date,
228- max(last_modified_date) as latest_file_date
229- from cdps_aip_inventory_with_aip_uuid
230- where aip_uuid is not null
231- group by bucket, aip_uuid, aip_s3_key, aip_s3_uri
232- order by aip_files_count desc;
233- """
234266 # ruff: enable: E501
235267
236268 aips_df = self .query_inventory (query )
@@ -261,9 +293,16 @@ def get_aip_from_uuid(self, aip_uuid: str) -> pd.Series:
261293 """Retrieve information about a specific AIP by its UUID."""
262294 aips_df = self .get_aips_df ()
263295
296+ # AIP UUID not found in Inventory data associated with a valid Bagit structure
264297 if aip_uuid not in aips_df ["aip_uuid" ].to_numpy ():
265- raise ValueError (f"AIP UUID '{ aip_uuid } ' not found in S3 Inventory data" )
298+ raise ValueError (
299+ f"AIP UUID '{ aip_uuid } ' not found in S3 Inventory data "
300+ "or not associated with a valid Bagit AIP"
301+ )
302+
266303 aip = aips_df .set_index ("aip_uuid" ).loc [aip_uuid ]
304+
305+ # AIP UUID found associated with multiple S3 prefixes (e.g. multiple buckets)
267306 if isinstance (aip , pd .DataFrame ):
268307 raise TypeError (
269308 f"Multiple entries found for AIP UUID '{ aip_uuid } 'in S3 Inventory data"
0 commit comments