blob: af077e7d08e6e1e36f8c4ebf3144e970ccb8502f [file] [log] [blame]
Peter Pentchevb91fc932023-01-19 15:27:13 +02001# SPDX-FileCopyrightText: StorPool <support@storpool.com>
2# SPDX-License-Identifier: BSD-2-Clause
3"""Parse base/URL repository location pairs."""
4
5from __future__ import annotations
6
7import dataclasses
8import os
9import re
10import urllib.parse as uparse
11
12from . import defs
13from . import util
14
15
16_RE_ENV_BASE = re.compile(r"^ [A-Z][A-Z0-9_]* $", re.X)
17
18
19@dataclasses.dataclass(frozen=True)
20class RepoURLResult:
21 """Base class for the OK/error parsed URL dichotomy."""
22
23
24@dataclasses.dataclass(frozen=True)
25class RepoURLOK(RepoURLResult):
26 """Successfully parsed a base URL for repositories."""
27
28 url: defs.RepoURL
29
30
31@dataclasses.dataclass(frozen=True)
32class RepoURLError(RepoURLResult):
33 """Could not parse a base URL for repositories."""
34
35 name: str
36 value: str
37 err: ValueError
38
39
40@dataclasses.dataclass(frozen=True)
41class RepoURLPair:
42 """A base/URL pair for an URL obtained from the command line."""
43
44 base: str
45 url: defs.RepoURL
46
47
48def _slash_extend(url: uparse.ParseResult) -> uparse.ParseResult:
49 """Add a / at the end of the path if there is none."""
50 if url.path.endswith("/"):
51 return url
52
53 return url._replace(path=url.path + "/")
54
55
56def _validate_file(name: str, value: str, url: uparse.ParseResult) -> RepoURLResult:
57 """Make sure a file:// URL has no host and an absolute path."""
58 if url.netloc:
59 return RepoURLError(name, value, ValueError("No hostname expected for a 'file' URL"))
60 if not url.path.startswith("/"):
61 return RepoURLError(name, value, ValueError("Expected an absolute path for a 'file' URL"))
62
63 url = _slash_extend(url)
64 return RepoURLOK(defs.RepoURL(url))
65
66
67def _validate_http(name: str, value: str, url: uparse.ParseResult) -> RepoURLResult:
68 """Make sure a http(s):// URL has a host, slash-terminate the path."""
69 if not url.netloc:
70 return RepoURLError(
Peter Pentchev85a2ad12024-01-18 13:34:37 +020071 name,
72 value,
73 ValueError("Expected a hostname for 'http' or 'https' URLs"),
Peter Pentchevb91fc932023-01-19 15:27:13 +020074 )
75
76 url = _slash_extend(url)
77 return RepoURLOK(defs.RepoURL(url))
78
79
80_SCHEME_VALIDATORS = {
81 "file": _validate_file,
82 "http": _validate_http,
83 "https": _validate_http,
84}
85
86
87def parse_url(name: str, base: str, value: str) -> RepoURLResult:
88 """Parse and validate a single base/URL pair."""
89 if not _RE_ENV_BASE.match(base):
90 return RepoURLError(name, value, ValueError(f"Invalid URL base {base!r}"))
91
92 try:
93 url = uparse.urlparse(value)
94 except ValueError as err:
95 return RepoURLError(name, value, err)
96
97 validator = _SCHEME_VALIDATORS.get(url.scheme)
98 if validator is None:
99 return RepoURLError(
Peter Pentchev85a2ad12024-01-18 13:34:37 +0200100 name,
101 value,
102 ValueError("Expected 'http', 'https', or 'file' as the URL scheme"),
Peter Pentchevb91fc932023-01-19 15:27:13 +0200103 )
104 return validator(name, value, url)
105
106
107def get_env_repo_urls(environ: dict[str, str] | None = None) -> dict[str, RepoURLResult]:
108 """Parse the REPO_URL_<base> environment variables."""
109 if environ is None:
110 environ = dict(os.environ)
111
112 res: dict[str, RepoURLResult] = {
Peter Pentchev85a2ad12024-01-18 13:34:37 +0200113 "OPENSTACK": RepoURLOK(defs.RepoURL(uparse.urlparse("https://github.com/openstack"))),
Peter Pentchevb91fc932023-01-19 15:27:13 +0200114 }
115 for name, value in environ.items():
116 base = util.str_removeprefix(name, "REPO_URL_")
117 if base == name:
118 continue
119 res[base] = parse_url(name, base, value)
120
121 return res
122
123
124def parse_base_url_pair(arg: str) -> RepoURLPair:
125 """Parse a `--repo-url base=url` command-line argument."""
126 raise NotImplementedError(repr(arg))