Skip to content

Commit f7a0ce7

Browse files
committed
Use anyio based async file io operations
1 parent bb7f43b commit f7a0ce7

File tree

7 files changed

+57
-43
lines changed

7 files changed

+57
-43
lines changed

goosebit/api/v1/software/routes.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from pathlib import Path
2-
31
import aiofiles
2+
from anyio import Path
43
from fastapi import APIRouter, File, Form, HTTPException, Security, UploadFile
54
from fastapi.requests import Request
65

@@ -42,8 +41,8 @@ async def software_delete(_: Request, delete_req: SoftwareDeleteRequest) -> Stat
4241

4342
if software.local:
4443
path = software.path
45-
if path.exists():
46-
path.unlink()
44+
if await path.exists():
45+
await path.unlink()
4746

4847
await software.delete()
4948
success = True
@@ -68,11 +67,13 @@ async def post_update(_: Request, file: UploadFile | None = File(None), url: str
6867
software = await create_software_update(url, None)
6968
elif file is not None:
7069
# local file
71-
file_path = config.artifacts_dir.joinpath(file.filename)
70+
artifacts_dir = Path(config.artifacts_dir)
71+
file_path = artifacts_dir.joinpath(file.filename)
7272

7373
async with aiofiles.tempfile.NamedTemporaryFile("w+b") as f:
7474
await f.write(await file.read())
75-
software = await create_software_update(file_path.absolute().as_uri(), Path(str(f.name)))
75+
absolute = await file_path.absolute()
76+
software = await create_software_update(absolute.as_uri(), Path(f.name))
7677
else:
7778
raise HTTPException(422)
7879

goosebit/db/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from enum import IntEnum
2-
from pathlib import Path
32
from typing import Self
43
from urllib.parse import unquote, urlparse
54
from urllib.request import url2pathname
65

76
import semver
7+
from anyio import Path
88
from tortoise import Model, fields
99

1010
from goosebit.api.telemetry.metrics import devices_count
@@ -132,7 +132,7 @@ async def latest(cls, device: Device) -> Self | None:
132132
)[0]
133133

134134
@property
135-
def path(self):
135+
def path(self) -> Path:
136136
return Path(url2pathname(unquote(urlparse(self.uri).path)))
137137

138138
@property

goosebit/ui/bff/software/routes.py

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

3-
import aiofiles
3+
from anyio import Path, open_file
44
from fastapi import APIRouter, Form, HTTPException, Security, UploadFile
55
from fastapi.requests import Request
66
from tortoise.expressions import Q
@@ -64,20 +64,20 @@ async def post_update(
6464
await create_software_update(url, None)
6565
else:
6666
# local file
67-
file = config.artifacts_dir.joinpath(filename)
68-
config.artifacts_dir.mkdir(parents=True, exist_ok=True)
67+
artifacts_dir = Path(config.artifacts_dir)
68+
file = artifacts_dir.joinpath(filename)
69+
await artifacts_dir.mkdir(parents=True, exist_ok=True)
6970

7071
temp_file = file.with_suffix(".tmp")
7172
if init:
72-
temp_file.unlink(missing_ok=True)
73+
await temp_file.unlink(missing_ok=True)
7374

74-
contents = await chunk.read()
75-
76-
async with aiofiles.open(temp_file, mode="ab") as f:
77-
await f.write(contents)
75+
async with await open_file(temp_file, "ab") as f:
76+
await f.write(await chunk.read())
7877

7978
if done:
8079
try:
81-
await create_software_update(file.absolute().as_uri(), temp_file)
80+
absolute = await file.absolute()
81+
await create_software_update(absolute.as_uri(), temp_file)
8282
finally:
83-
temp_file.unlink(missing_ok=True)
83+
await temp_file.unlink(missing_ok=True)

goosebit/updates/__init__.py

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

3-
import shutil
4-
from pathlib import Path
53
from urllib.parse import unquote, urlparse
64
from urllib.request import url2pathname
75

6+
from anyio import Path
87
from fastapi import HTTPException
98
from fastapi.requests import Request
109
from tortoise.expressions import Q
@@ -46,10 +45,11 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
4645
# for local file: rename temp file to final name
4746
if parsed_uri.scheme == "file" and temp_file is not None:
4847
filename = Path(url2pathname(unquote(parsed_uri.path))).name
49-
path = config.artifacts_dir.joinpath(update_info["hash"], filename)
50-
path.parent.mkdir(parents=True, exist_ok=True)
51-
shutil.copy(temp_file, path)
52-
uri = path.absolute().as_uri()
48+
path = Path(config.artifacts_dir).joinpath(update_info["hash"], filename)
49+
await path.parent.mkdir(parents=True, exist_ok=True)
50+
await temp_file.rename(path)
51+
absolute = await path.absolute()
52+
uri = absolute.as_uri()
5353

5454
# create software
5555
software = await Software.create(

goosebit/updates/swdesc.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import hashlib
22
import logging
3-
from pathlib import Path
43
from typing import Any
54

65
import aiofiles
76
import httpx
87
import libconf
98
import semver
9+
from anyio import AsyncFile, Path, open_file
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -41,7 +41,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
4141

4242

4343
async def parse_file(file: Path):
44-
async with aiofiles.open(file, "r+b") as f:
44+
async with await open_file(file, "r+b") as f:
4545
# get file size
4646
size = int((await f.read(110))[54:62], 16)
4747
filename = b""
@@ -59,8 +59,9 @@ async def parse_file(file: Path):
5959
swdesc = libconf.loads((await f.read(size)).decode("utf-8"))
6060

6161
swdesc_attrs = parse_descriptor(swdesc)
62-
swdesc_attrs["size"] = file.stat().st_size
63-
swdesc_attrs["hash"] = _sha1_hash_file(file)
62+
stat = await file.stat()
63+
swdesc_attrs["size"] = stat.st_size
64+
swdesc_attrs["hash"] = await _sha1_hash_file(f)
6465
return swdesc_attrs
6566

6667

@@ -72,7 +73,17 @@ async def parse_remote(url: str):
7273
return await parse_file(Path(str(f.name)))
7374

7475

75-
def _sha1_hash_file(file_path: Path):
76-
with file_path.open("rb") as f:
77-
sha1_hash = hashlib.file_digest(f, "sha1")
76+
async def _sha1_hash_file(fileobj: AsyncFile):
77+
last = await fileobj.tell()
78+
await fileobj.seek(0)
79+
sha1_hash = hashlib.sha1()
80+
buf = bytearray(2**18)
81+
view = memoryview(buf)
82+
while True:
83+
size = await fileobj.readinto(buf)
84+
if size == 0:
85+
break
86+
sha1_hash.update(view[:size])
87+
88+
await fileobj.seek(last)
7889
return sha1_hash.hexdigest()

tests/api/v1/software/test_routes.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
from pathlib import Path
2-
31
import pytest
2+
from anyio import Path, open_file
43

54

65
@pytest.mark.asyncio
76
async def test_create_software_local(async_client, test_data):
8-
path = Path(__file__).resolve().parent / "software-header.swu"
9-
with open(path, "rb") as file:
10-
files = {"file": file}
11-
response = await async_client.post(f"/api/v1/software", files=files)
7+
resolved = await Path(__file__).resolve()
8+
path = resolved.parent / "software-header.swu"
9+
async with await open_file(path, "rb") as file:
10+
files = {"file": await file.read()}
11+
12+
response = await async_client.post(f"/api/v1/software", files=files)
1213

1314
assert response.status_code == 200
1415
software = response.json()
@@ -17,9 +18,10 @@ async def test_create_software_local(async_client, test_data):
1718

1819
@pytest.mark.asyncio
1920
async def test_create_software_remote(async_client, httpserver, test_data):
20-
path = Path(__file__).resolve().parent / "software-header.swu"
21-
with open(path, "rb") as file:
22-
byte_array = file.read()
21+
resolved = await Path(__file__).resolve()
22+
path = resolved.parent / "software-header.swu"
23+
async with await open_file(path, "rb") as file:
24+
byte_array = await file.read()
2325

2426
httpserver.expect_request("/software-header.swu").respond_with_data(byte_array)
2527

tests/updates/test_swdesc.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from pathlib import Path
2-
31
import pytest
2+
from anyio import Path
43
from libconf import AttrDict
54

65
from goosebit.updates.swdesc import parse_descriptor, parse_file
@@ -102,7 +101,8 @@ def test_parse_descriptor_several_boardname():
102101

103102
@pytest.mark.asyncio
104103
async def test_parse_software_header():
105-
swdesc_attrs = await parse_file(Path(__file__).resolve().parent / "software-header.swu")
104+
resolved = await Path(__file__).resolve()
105+
swdesc_attrs = await parse_file(resolved.parent / "software-header.swu")
106106
assert str(swdesc_attrs["version"]) == "8.8.1-11-g8c926e5+188370"
107107
assert swdesc_attrs["compatibility"] == [
108108
{"hw_model": "smart-gateway-mt7688", "hw_revision": "0.5"},

0 commit comments

Comments
 (0)