Files
LangBot/src/langbot/pkg/storage/providers/localstorage.py
T
2026-07-29 11:32:26 +08:00

90 lines
2.8 KiB
Python

from __future__ import annotations
import asyncio
import os
import aiofiles
import shutil
from ...core import app
from .. import provider
LOCAL_STORAGE_PATH = os.path.join('data', 'storage')
def _safe_resolve(base: str, key: str) -> str:
"""Resolve *key* under *base* and ensure the result stays inside *base*.
Raises ``ValueError`` if the resolved path escapes the storage root
(e.g. via absolute paths, ``..`` components, or symlinks).
"""
# os.path.realpath resolves symlinks and normalises the path.
resolved = os.path.realpath(os.path.join(base, key))
canonical_base = os.path.realpath(base)
# The resolved path must be *strictly* inside the base directory (or equal
# to it only for directory operations). We append os.sep so that a base of
# "/data/storage" does not match "/data/storage_evil".
if not (resolved == canonical_base or resolved.startswith(canonical_base + os.sep)):
raise ValueError(f'Path traversal detected: key {key!r} resolves outside storage root')
return resolved
class LocalStorageProvider(provider.StorageProvider):
def __init__(self, ap: app.Application):
super().__init__(ap)
if not os.path.exists(LOCAL_STORAGE_PATH):
os.makedirs(LOCAL_STORAGE_PATH)
async def save(
self,
key: str,
value: bytes,
):
resolved = await asyncio.to_thread(_safe_resolve, LOCAL_STORAGE_PATH, key)
parent = os.path.dirname(resolved)
await asyncio.to_thread(os.makedirs, parent, exist_ok=True)
async with aiofiles.open(resolved, 'wb') as f:
await f.write(value)
async def load(
self,
key: str,
) -> bytes:
resolved = await asyncio.to_thread(_safe_resolve, LOCAL_STORAGE_PATH, key)
async with aiofiles.open(resolved, 'rb') as f:
return await f.read()
async def exists(
self,
key: str,
) -> bool:
resolved = await asyncio.to_thread(_safe_resolve, LOCAL_STORAGE_PATH, key)
return await asyncio.to_thread(os.path.exists, resolved)
async def delete(
self,
key: str,
):
resolved = await asyncio.to_thread(_safe_resolve, LOCAL_STORAGE_PATH, key)
await asyncio.to_thread(os.remove, resolved)
async def size(
self,
key: str,
) -> int:
resolved = await asyncio.to_thread(_safe_resolve, LOCAL_STORAGE_PATH, key)
return await asyncio.to_thread(os.path.getsize, resolved)
async def delete_dir_recursive(
self,
dir_path: str,
):
resolved = await asyncio.to_thread(
_safe_resolve,
LOCAL_STORAGE_PATH,
dir_path,
)
if await asyncio.to_thread(os.path.exists, resolved):
await asyncio.to_thread(shutil.rmtree, resolved)