Write user files and media atomically
This commit is contained in:
parent
9d3e2dbfc4
commit
190eb58187
1
TODO.md
1
TODO.md
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
## Before release
|
## Before release
|
||||||
|
|
||||||
- Atomic
|
|
||||||
- Catch server 5xx errors when sending message and retry
|
- Catch server 5xx errors when sending message and retry
|
||||||
- Update README.md
|
- Update README.md
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,11 @@ from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional
|
from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
import nio
|
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
from .utils import Size
|
import nio
|
||||||
|
|
||||||
|
from .utils import Size, atomic_write
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .backend import Backend
|
from .backend import Backend
|
||||||
|
@ -138,7 +138,7 @@ class Media:
|
||||||
|
|
||||||
self.local_path.parent.mkdir(parents=True, exist_ok=True)
|
self.local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
async with aiofiles.open(self.local_path, "wb") as file:
|
async with atomic_write(self.local_path, binary=True) as file:
|
||||||
await file.write(data)
|
await file.write(data)
|
||||||
|
|
||||||
return self.local_path
|
return self.local_path
|
||||||
|
@ -212,7 +212,7 @@ class Media:
|
||||||
media.local_path.parent.mkdir(parents=True, exist_ok=True)
|
media.local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if not media.local_path.exists() or overwrite:
|
if not media.local_path.exists() or overwrite:
|
||||||
async with aiofiles.open(media.local_path, "wb") as file:
|
async with atomic_write(media.local_path, binary=True) as file:
|
||||||
await file.write(data)
|
await file.write(data)
|
||||||
|
|
||||||
return media
|
return media
|
||||||
|
|
|
@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional
|
||||||
import aiofiles
|
import aiofiles
|
||||||
|
|
||||||
from .theme_parser import convert_to_qml
|
from .theme_parser import convert_to_qml
|
||||||
from .utils import dict_update_recursive
|
from .utils import atomic_write, dict_update_recursive
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .backend import Backend
|
from .backend import Backend
|
||||||
|
@ -88,7 +88,7 @@ class DataFile:
|
||||||
if not self.create_missing and not self.path.exists():
|
if not self.create_missing and not self.path.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
async with aiofiles.open(self.path, "w") as new:
|
async with atomic_write(self.path) as new:
|
||||||
await new.write(self._to_write)
|
await new.write(self._to_write)
|
||||||
|
|
||||||
self._to_write = None
|
self._to_write = None
|
||||||
|
|
|
@ -8,16 +8,23 @@ import inspect
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
|
import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from enum import auto as autostr
|
from enum import auto as autostr
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Dict, Mapping, Sequence, Tuple, Type
|
from typing import (
|
||||||
|
Any, AsyncIterator, Dict, Mapping, Sequence, Tuple, Type, Union,
|
||||||
|
)
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
import filetype
|
import filetype
|
||||||
from aiofiles.threadpool.binary import AsyncBufferedReader
|
from aiofiles.threadpool.binary import AsyncBufferedReader
|
||||||
|
from aiofiles.threadpool.text import AsyncTextIOWrapper
|
||||||
|
|
||||||
from nio.crypto import AsyncDataT as File
|
from nio.crypto import AsyncDataT as File
|
||||||
from nio.crypto import async_generator_from_data
|
from nio.crypto import async_generator_from_data
|
||||||
|
|
||||||
|
@ -185,3 +192,21 @@ def classes_defined_in(module: ModuleType) -> Dict[str, Type]:
|
||||||
if not m[0].startswith("_") and
|
if not m[0].startswith("_") and
|
||||||
m[1].__module__.startswith(module.__name__)
|
m[1].__module__.startswith(module.__name__)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def atomic_write(
|
||||||
|
path: Union[Path, str], binary: bool = False, **kwargs,
|
||||||
|
) -> AsyncIterator[Union[AsyncTextIOWrapper, AsyncBufferedReader]]:
|
||||||
|
"""Write a file asynchronously (using aiofiles) and atomically."""
|
||||||
|
|
||||||
|
mode = "wb" if binary else "w"
|
||||||
|
path = Path(path)
|
||||||
|
temp = NamedTemporaryFile(dir=path.parent, delete=False)
|
||||||
|
temp_path = Path(temp.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(temp_path, mode, **kwargs) as out:
|
||||||
|
yield out
|
||||||
|
finally:
|
||||||
|
temp_path.replace(path)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user