|
30 | 30 | from rocrate_validator.errors import ROCrateInvalidURIError |
31 | 31 | from rocrate_validator.utils.uri import validate_rocrate_uri |
32 | 32 | from rocrate_validator.utils.http import HttpRequester |
33 | | -from rocrate_validator.utils.uri import URI |
| 33 | +from rocrate_validator.utils.uri import URI, AvailabilityStatus, is_external_reference |
34 | 34 |
|
35 | 35 | # set up logging |
36 | 36 | logger = logging.getLogger(__name__) |
@@ -140,8 +140,17 @@ def id_as_path(self) -> Path: |
140 | 140 | @classmethod |
141 | 141 | def get_id_as_uri(cls, entity_id: str, ro_crate: ROCrate) -> URI: |
142 | 142 | assert entity_id, "Entity ID cannot be None" |
143 | | - if entity_id.startswith("http"): |
| 143 | + # Per RO-Crate 1.1 § 4.2.2, an `@id` is either a relative URI path or |
| 144 | + # an external URI/IRI (RFC 3986/3987). External references are used |
| 145 | + # as-is (without resolving them against the crate URI) so the entity |
| 146 | + # is classified as remote/web-based; this covers both authority-based |
| 147 | + # forms (``http://``, ``scp://``) and scheme-only ones (``urn:``, |
| 148 | + # ``doi:``, ``arcp:``). |
| 149 | + if is_external_reference(entity_id): |
144 | 150 | return URI(entity_id) |
| 151 | + # Otherwise the `@id` is a relative path: if the RO-Crate itself is |
| 152 | + # remote, resolve it against the crate URI so the entity is still |
| 153 | + # classified as remote/web-based. |
145 | 154 | if ro_crate.uri.is_remote_resource(): |
146 | 155 | if entity_id.startswith("./"): |
147 | 156 | return URI(f"{ro_crate.uri}/{entity_id[2:]}") |
@@ -208,58 +217,66 @@ def raw_data(self) -> object: |
208 | 217 | def is_local(self) -> bool: |
209 | 218 | return not self.is_remote() |
210 | 219 |
|
211 | | - def is_available(self) -> bool: |
| 220 | + def check_availability(self) -> AvailabilityStatus: |
| 221 | + """ |
| 222 | + Return a fine-grained availability status for this entity. |
| 223 | +
|
| 224 | + This is the primary check; :meth:`is_available` is the boolean |
| 225 | + shortcut built on top of it. The status distinguishes definitely |
| 226 | + unavailable resources, auth-protected ones, and remote URIs whose |
| 227 | + scheme the validator cannot natively check (scp://, s3://, ...). |
| 228 | + """ |
212 | 229 | try: |
213 | | - # check if the entity points to an external file |
214 | | - if self.id.startswith("http"): |
| 230 | + entity_uri = self.id_as_uri |
| 231 | + # Remote entities with a scheme we can natively reach are checked |
| 232 | + # by inspecting the remote response status. |
| 233 | + if entity_uri.is_natively_checkable(): |
215 | 234 | logger.debug("Checking the availability of a remote entity") |
216 | | - return self.ro_crate.get_external_file_size(self.id) > 0 |
217 | | - |
218 | | - # check if the entity is part of the local RO-Crate |
| 235 | + return entity_uri.check_availability() |
| 236 | + |
| 237 | + # Remote entities with a non-natively-checkable scheme cannot be |
| 238 | + # verified (scp://, sftp://, s3://, ...): report UNCHECKABLE so |
| 239 | + # callers can warn without invalidating the validation. |
| 240 | + if entity_uri.is_remote_resource(): |
| 241 | + logger.debug( |
| 242 | + "Cannot natively verify availability for entity '%s' (scheme '%s')", |
| 243 | + self.id, |
| 244 | + entity_uri.scheme, |
| 245 | + ) |
| 246 | + return AvailabilityStatus.UNCHECKABLE |
| 247 | + |
| 248 | + # Local entity: locate it inside the (local or remote) RO-Crate. |
219 | 249 | if self.ro_crate.uri.is_local_resource(): |
220 | | - # check if the file exists in the local file system |
221 | 250 | if isinstance(self.ro_crate, ROCrateLocalFolder): |
222 | | - logger.debug( |
223 | | - "Checking the availability of a local entity in a local folder" |
224 | | - ) |
225 | | - return self.ro_crate.has_file( |
226 | | - self.id_as_path |
227 | | - ) or self.ro_crate.has_directory(self.id_as_path) |
228 | | - # check if the file exists in the local zip file |
| 251 | + found = self.ro_crate.has_file(self.id_as_path) or self.ro_crate.has_directory(self.id_as_path) |
| 252 | + return AvailabilityStatus.AVAILABLE if found else AvailabilityStatus.UNAVAILABLE |
229 | 253 | if isinstance(self.ro_crate, ROCrateLocalZip): |
230 | | - logger.debug( |
231 | | - "Checking the availability of a local entity in a local zip file" |
232 | | - ) |
233 | | - # Skip the check for the root of a ZIP archive |
234 | 254 | if self.id == "./": |
235 | | - logger.debug( |
236 | | - "Skipping the check for the presence of the Data Entity '%s' within the RO-Crate " |
237 | | - "as it is the root of a ZIP archive", |
238 | | - self.id, |
239 | | - ) |
240 | | - return True |
241 | | - return self.ro_crate.has_directory( |
| 255 | + return AvailabilityStatus.AVAILABLE |
| 256 | + found = self.ro_crate.has_directory(unquote(str(self.id))) or self.ro_crate.has_file( |
242 | 257 | unquote(str(self.id)) |
243 | | - ) or self.ro_crate.has_file(unquote(str(self.id))) |
| 258 | + ) |
| 259 | + return AvailabilityStatus.AVAILABLE if found else AvailabilityStatus.UNAVAILABLE |
244 | 260 |
|
245 | | - # check if the entity is part of the remote RO-Crate |
246 | | - logger.debug( |
247 | | - "Checking the availability of a remote entity in a remote RO-Crate" |
248 | | - ) |
249 | 261 | if self.ro_crate.uri.is_remote_resource(): |
250 | 262 | if self.id == "./": |
251 | | - return self.ro_crate.get_file_size(Path(self.id_as_uri())) > 0 |
252 | | - return self.ro_crate.has_directory( |
253 | | - unquote(str(self.id)) |
254 | | - ) or self.ro_crate.has_file(unquote(str(self.id))) |
| 263 | + found = self.ro_crate.get_file_size(Path(self.id_as_uri())) > 0 |
| 264 | + else: |
| 265 | + found = self.ro_crate.has_directory(unquote(str(self.id))) or self.ro_crate.has_file( |
| 266 | + unquote(str(self.id)) |
| 267 | + ) |
| 268 | + return AvailabilityStatus.AVAILABLE if found else AvailabilityStatus.UNAVAILABLE |
255 | 269 | except Exception as e: |
256 | 270 | if logger.isEnabledFor(logging.DEBUG): |
257 | 271 | logger.exception(e) |
258 | | - return False |
| 272 | + return AvailabilityStatus.UNAVAILABLE |
259 | 273 |
|
260 | | - raise ROCrateInvalidURIError( |
261 | | - uri=self.id, message="Could not determine the availability of the entity" |
262 | | - ) |
| 274 | + # Fallthrough: the crate URI is neither a recognized local nor a |
| 275 | + # remote resource — the entity location cannot be determined. |
| 276 | + raise ROCrateInvalidURIError(uri=self.id, message="Could not determine the availability of the entity") |
| 277 | + |
| 278 | + def is_available(self) -> bool: |
| 279 | + return self.check_availability() == AvailabilityStatus.AVAILABLE |
263 | 280 |
|
264 | 281 | def get_size(self) -> int: |
265 | 282 | try: |
|
0 commit comments