Skip to content

Commit a42f44e

Browse files
committed
Automatically fix permissions issues
1 parent 95c6e7f commit a42f44e

File tree

12 files changed

+101
-68
lines changed

12 files changed

+101
-68
lines changed

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proxy_scraper_checker/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Monkeypatch os.link to make aiofiles work on Termux
66
if not hasattr(_os, "link"):
7-
from .typing_compat import Any as _Any
7+
from typing_extensions import Any as _Any
88

99
def _link(*args: _Any, **kwargs: _Any) -> None: # noqa: ARG001
1010
raise RuntimeError

proxy_scraper_checker/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from rich.logging import RichHandler
1414
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
1515
from rich.table import Table
16+
from typing_extensions import Any
1617

1718
from . import checker, geodb, http, output, scraper, sort, utils
1819
from .settings import Settings
1920
from .storage import ProxyStorage
20-
from .typing_compat import Any
2121

2222
if sys.version_info >= (3, 11):
2323
try:

proxy_scraper_checker/cache.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

proxy_scraper_checker/fs.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from pathlib import Path
5+
6+
import platformdirs
7+
8+
logger = logging.getLogger(__name__)
9+
CACHE_PATH = platformdirs.user_cache_path("proxy_scraper_checker")
10+
11+
12+
def add_permission(path: Path, permission: int, /) -> None:
13+
current_permissions = path.stat().st_mode
14+
new_permissions = current_permissions | permission
15+
if current_permissions != new_permissions:
16+
path.chmod(new_permissions)
17+
logger.info(
18+
"Changed permissions of %s from %o to %o",
19+
path,
20+
current_permissions,
21+
new_permissions,
22+
)
23+
24+
25+
def maybe_add_permission(path: Path, permission: int, /) -> None:
26+
try:
27+
add_permission(path, permission)
28+
except FileNotFoundError:
29+
pass
30+
31+
32+
def create_or_fix_dir(path: Path, /, *, permissions: int) -> None:
33+
try:
34+
path.mkdir(parents=True)
35+
except FileExistsError:
36+
if not path.is_dir():
37+
msg = f"{path} is not a directory"
38+
raise ValueError(msg) from None
39+
add_permission(path, permissions)

proxy_scraper_checker/geodb.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
from __future__ import annotations
22

33
import logging
4-
from pathlib import Path
4+
import stat
55
from typing import Optional
66

77
import aiofiles
8-
import aiofiles.ospath
98
from aiohttp import ClientResponse, ClientSession, hdrs
109
from rich.progress import Progress, TaskID
1110

12-
from . import cache
13-
from .utils import IS_DOCKER, bytes_decode
11+
from . import fs
12+
from .utils import IS_DOCKER, asyncify, bytes_decode
1413

1514
logger = logging.getLogger(__name__)
1615

1716
GEODB_URL = "https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb"
18-
GEODB_PATH = Path(cache.DIR, "geolocation_database.mmdb")
17+
GEODB_PATH = fs.CACHE_PATH / "geolocation_database.mmdb"
1918
GEODB_ETAG_PATH = GEODB_PATH.with_suffix(".mmdb.etag")
2019

2120

2221
async def _read_etag() -> Optional[str]:
2322
try:
23+
await asyncify(fs.add_permission)(GEODB_ETAG_PATH, stat.S_IRUSR)
2424
async with aiofiles.open(GEODB_ETAG_PATH, "rb") as etag_file:
2525
content = await etag_file.read()
2626
except FileNotFoundError:
@@ -29,6 +29,7 @@ async def _read_etag() -> Optional[str]:
2929

3030

3131
async def _save_etag(etag: str, /) -> None:
32+
await asyncify(fs.maybe_add_permission)(GEODB_ETAG_PATH, stat.S_IWUSR)
3233
async with aiofiles.open(
3334
GEODB_ETAG_PATH, "w", encoding="utf-8"
3435
) as etag_file:
@@ -38,6 +39,7 @@ async def _save_etag(etag: str, /) -> None:
3839
async def _save_geodb(
3940
*, progress: Progress, response: ClientResponse, task: TaskID
4041
) -> None:
42+
await asyncify(fs.maybe_add_permission)(GEODB_PATH, stat.S_IWUSR)
4143
async with aiofiles.open(GEODB_PATH, "wb") as geodb:
4244
async for chunk in response.content.iter_any():
4345
await geodb.write(chunk)
@@ -47,7 +49,7 @@ async def _save_geodb(
4749
async def download_geodb(*, progress: Progress, session: ClientSession) -> None:
4850
headers = (
4951
{hdrs.IF_NONE_MATCH: current_etag}
50-
if await aiofiles.ospath.exists(GEODB_PATH)
52+
if await asyncify(GEODB_PATH.exists)()
5153
and (current_etag := await _read_etag())
5254
else None
5355
)

proxy_scraper_checker/output.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22

33
import json
44
import logging
5+
import stat
56
from shutil import rmtree
67
from typing import Sequence, Union
78

8-
import aiofiles.ospath
99
import maxminddb
1010

11-
from . import sort
11+
from . import fs, sort
1212
from .geodb import GEODB_PATH
1313
from .null_context import NullContext
1414
from .proxy import Proxy
1515
from .settings import Settings
1616
from .storage import ProxyStorage
17-
from .utils import IS_DOCKER
17+
from .utils import IS_DOCKER, asyncify
1818

1919
logger = logging.getLogger(__name__)
2020

@@ -30,14 +30,16 @@ def _create_proxy_list_str(
3030
)
3131

3232

33-
@aiofiles.ospath.wrap
33+
@asyncify
3434
def save_proxies(*, settings: Settings, storage: ProxyStorage) -> None:
3535
if settings.output_json:
36-
mmdb: Union[maxminddb.Reader, NullContext] = (
37-
maxminddb.open_database(GEODB_PATH)
38-
if settings.enable_geolocation
39-
else NullContext()
40-
)
36+
if settings.enable_geolocation:
37+
fs.add_permission(GEODB_PATH, stat.S_IRUSR)
38+
mmdb: Union[maxminddb.Reader, NullContext] = (
39+
maxminddb.open_database(GEODB_PATH)
40+
)
41+
else:
42+
mmdb = NullContext()
4143
with mmdb as mmdb_reader:
4244
proxy_dicts = [
4345
{
@@ -54,16 +56,19 @@ def save_proxies(*, settings: Settings, storage: ProxyStorage) -> None:
5456
}
5557
for proxy in sorted(storage, key=sort.timeout_sort_key)
5658
]
57-
with (settings.output_path / "proxies.json").open(
58-
"w", encoding="utf-8"
59-
) as f:
60-
json.dump(
61-
proxy_dicts, f, ensure_ascii=False, separators=(",", ":")
62-
)
63-
with (settings.output_path / "proxies_pretty.json").open(
64-
"w", encoding="utf-8"
65-
) as f:
66-
json.dump(proxy_dicts, f, ensure_ascii=False, indent="\t")
59+
for path, indent, separators in (
60+
(settings.output_path / "proxies.json", None, (",", ":")),
61+
(settings.output_path / "proxies_pretty.json", "\t", None),
62+
):
63+
path.unlink(missing_ok=True)
64+
with path.open("w", encoding="utf-8") as f:
65+
json.dump(
66+
proxy_dicts,
67+
f,
68+
ensure_ascii=False,
69+
indent=indent,
70+
separators=separators,
71+
)
6772
if settings.output_txt:
6873
sorted_proxies = sorted(storage, key=settings.sorting_key)
6974
grouped_proxies = tuple(
@@ -78,7 +83,7 @@ def save_proxies(*, settings: Settings, storage: ProxyStorage) -> None:
7883
rmtree(folder)
7984
except FileNotFoundError:
8085
pass
81-
folder.mkdir(parents=True, exist_ok=True)
86+
folder.mkdir()
8287
text = _create_proxy_list_str(
8388
proxies=sorted_proxies,
8489
anonymous_only=anonymous_only,

proxy_scraper_checker/settings.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import json
66
import logging
77
import math
8-
import os
8+
import stat
99
import sys
1010
from pathlib import Path
1111
from typing import (
@@ -25,13 +25,13 @@
2525
import platformdirs
2626
from aiohttp import ClientSession, ClientTimeout
2727
from aiohttp_socks import ProxyType
28+
from typing_extensions import Any, Literal, Self
2829

29-
from . import cache, sort
30+
from . import fs, sort
3031
from .http import get_response_text
3132
from .null_context import NullContext
3233
from .parsers import parse_ipv4
33-
from .typing_compat import Any, Literal, Self
34-
from .utils import IS_DOCKER, create_or_check_dir
34+
from .utils import IS_DOCKER, asyncify
3535

3636
if TYPE_CHECKING:
3737
from .proxy import Proxy
@@ -269,12 +269,17 @@ async def from_mapping(
269269
output_path = (
270270
platformdirs.user_data_path("proxy_scraper_checker")
271271
if IS_DOCKER
272-
else cfg["output"]["path"]
272+
else Path(cfg["output"]["path"])
273273
)
274274

275275
_, _, (check_website_type, real_ip) = await asyncio.gather(
276-
create_or_check_dir(output_path, mode=os.W_OK | os.X_OK),
277-
create_or_check_dir(cache.DIR, mode=os.R_OK | os.W_OK | os.X_OK),
276+
asyncify(fs.create_or_fix_dir)(
277+
output_path, permissions=stat.S_IXUSR | stat.S_IWUSR
278+
),
279+
asyncify(fs.create_or_fix_dir)(
280+
fs.CACHE_PATH,
281+
permissions=stat.S_IRUSR | stat.S_IXUSR | stat.S_IWUSR,
282+
),
278283
_get_check_website_type_and_real_ip(
279284
check_website=cfg["check_website"], session=session
280285
),

proxy_scraper_checker/typing_compat.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

proxy_scraper_checker/utils.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import functools
45
import os
5-
from pathlib import Path
6-
from typing import Union
6+
from typing import Callable
77
from urllib.parse import urlparse
88

9-
import aiofiles.os
10-
import aiofiles.ospath
119
import charset_normalizer
10+
from typing_extensions import ParamSpec, TypeVar
11+
12+
T = TypeVar("T")
13+
P = ParamSpec("P")
1214

1315
IS_DOCKER = os.getenv("IS_DOCKER") == "1"
1416

@@ -22,14 +24,10 @@ def bytes_decode(value: bytes, /) -> str:
2224
return str(charset_normalizer.from_bytes(value)[0])
2325

2426

25-
async def create_or_check_dir(path: Union[Path, str], /, *, mode: int) -> None:
26-
try:
27-
await aiofiles.os.makedirs(path)
28-
except FileExistsError:
29-
access_task = asyncio.create_task(aiofiles.os.access(path, mode))
30-
if not await aiofiles.ospath.isdir(path):
31-
msg = f"{path} is not a directory"
32-
raise ValueError(msg) from None
33-
if not await access_task:
34-
msg = f"{path} is not accessible"
35-
raise ValueError(msg) from None
27+
def asyncify(f: Callable[P, T], /) -> Callable[P, asyncio.Future[T]]:
28+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> asyncio.Future[T]:
29+
return asyncio.get_running_loop().run_in_executor(
30+
None, functools.partial(f, *args, **kwargs)
31+
)
32+
33+
return functools.update_wrapper(wrapper, f)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ maxminddb = ">=1.3,<3"
2626
platformdirs = "<5"
2727
rich = ">=12.3,<14"
2828
tomli = { version = "<3", python = "<3.11" }
29-
typing-extensions = { version = "^4.4", python = "<3.11" }
29+
typing-extensions = "^4.4"
3030
uvloop = { version = ">=0.14,<0.20", optional = true, markers = "implementation_name == 'cpython' and (sys_platform == 'darwin' or sys_platform == 'linux')" }
3131

3232
[tool.poetry.extras]

ruff.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ ignore = [
5151
]
5252
ignore-init-module-imports = true
5353
select = ["ALL"]
54-
typing-modules = ["proxy_scraper_checker.typing_compat"]
5554

5655
[lint.flake8-self]
5756
ignore-names = []

0 commit comments

Comments
 (0)