repository and directory tests

This commit is contained in:
L-Nafaryus 2024-08-15 01:02:12 +05:00
parent 58e7175d45
commit 680b0172f0
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
7 changed files with 147 additions and 104 deletions

View File

@ -25,6 +25,11 @@ from materia.models.repository import (
RepositoryError, RepositoryError,
) )
from materia.models.directory import Directory, DirectoryLink, DirectoryInfo from materia.models.directory import (
Directory,
DirectoryPath,
DirectoryLink,
DirectoryInfo,
)
from materia.models.file import File, FileLink, FileInfo from materia.models.file import File, FileLink, FileInfo

View File

@ -3,7 +3,7 @@ import os
from typing import AsyncIterator, Self, TypeAlias from typing import AsyncIterator, Self, TypeAlias
from pathlib import Path from pathlib import Path
from pydantic import BaseModel, PostgresDsn from pydantic import BaseModel, PostgresDsn, ValidationError
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (
AsyncConnection, AsyncConnection,
AsyncEngine, AsyncEngine,
@ -102,7 +102,7 @@ class Database:
try: try:
yield session yield session
except HTTPException as e: except (HTTPException, ValidationError) as e:
await session.rollback() await session.rollback()
raise e from None raise e from None
except Exception as e: except Exception as e:

View File

@ -3,11 +3,12 @@ from typing import List, Optional, Self
from pathlib import Path from pathlib import Path
import shutil import shutil
import aiofiles import aiofiles
import re
from sqlalchemy import BigInteger, ForeignKey, inspect from sqlalchemy import BigInteger, ForeignKey, inspect
from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm import mapped_column, Mapped, relationship
import sqlalchemy as sa import sqlalchemy as sa
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict, ValidationError
from materia.models.base import Base from materia.models.base import Base
from materia.models import database from materia.models import database
@ -202,8 +203,17 @@ class Directory(Base):
await session.flush() await session.flush()
return self return self
async def info(self) -> "DirectoryInfo": async def info(self, session: SessionContext) -> "DirectoryInfo":
return DirectoryInfo.model_validate(self) info = DirectoryInfo.model_validate(self)
session.add(self)
await session.refresh(self, attribute_names=["files"])
info.used = sum([file.size for file in self.files])
return info
class DirectoryPath(BaseModel):
path: Path
class DirectoryLink(Base): class DirectoryLink(Base):
@ -228,7 +238,6 @@ class DirectoryInfo(BaseModel):
created: int created: int
updated: int updated: int
name: str name: str
path: Optional[str]
is_public: bool is_public: bool
used: Optional[int] = None used: Optional[int] = None

View File

@ -6,6 +6,8 @@ from aiofiles import ospath as async_path
import aioshutil import aioshutil
import re import re
valid_path = re.compile(r"^/(.*/)*([^/]*)$")
class FileSystemError(Exception): class FileSystemError(Exception):
pass pass
@ -184,3 +186,14 @@ class FileSystem:
f"Failed to write file to /{self.relative_path}:", f"Failed to write file to /{self.relative_path}:",
*e.args, *e.args,
) )
@staticmethod
def check_path(path: Path) -> bool:
return bool(valid_path.match(str(path)))
@staticmethod
def normalize(path: Path) -> Path:
if not path.is_absolute():
path = Path("/").joinpath(path)
return Path(*path.resolve().parts[1:])

View File

@ -4,130 +4,111 @@ import shutil
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from materia.models import User, Directory, DirectoryInfo from materia.models import User, Directory, DirectoryPath, DirectoryInfo, FileSystem
from materia.routers import middleware from materia.routers import middleware
from materia.config import Config from materia.config import Config
from pydantic import BaseModel
router = APIRouter(tags=["directory"]) router = APIRouter(tags=["directory"])
@router.post("/directory") @router.post("/directory")
async def create( async def create(
path: Path = Path(), path: DirectoryPath,
user: User = Depends(middleware.user), repository=Depends(middleware.repository),
ctx: middleware.Context = Depends(), ctx: middleware.Context = Depends(),
): ):
repository_path = Config.data_dir() / "repository" / user.lower_name if not FileSystem.check_path(path.path):
blacklist = [os.sep, ".", "..", "*"] raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
directory_path = Path(
os.sep.join(filter(lambda part: part not in blacklist, path.parts)) path = FileSystem.normalize(path.path)
)
async with ctx.database.session() as session: async with ctx.database.session() as session:
session.add(user)
await session.refresh(user, attribute_names=["repository"])
if not user.repository:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
current_directory = None current_directory = None
current_path = Path() current_path = Path()
directory = None directory = None
for part in directory_path.parts: for part in path.parts:
if not await Directory.by_path( if not (
user.repository.id, current_path, part, ctx.database directory := await Directory.by_path(
repository, current_path.joinpath(part), session, ctx.config
)
): ):
directory = Directory( directory = await Directory(
repository_id=user.repository.id, repository_id=repository.id,
parent_id=current_directory.id if current_directory else None, parent_id=current_directory.id if current_directory else None,
name=part, name=part,
) ).new(session, ctx.config)
try:
(repository_path / current_path / part).mkdir(exist_ok=True)
except OSError:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
f"Failed to create a directory {current_path / part}",
)
async with ctx.database.session() as session:
session.add(directory)
await session.commit()
await session.refresh(directory)
current_directory = directory current_directory = directory
current_path /= part current_path /= part
await session.commit()
@router.get("/directory") @router.get("/directory")
async def info( async def info(
path: Path, path: Path,
user: User = Depends(middleware.user), repository=Depends(middleware.repository),
ctx: middleware.Context = Depends(), ctx: middleware.Context = Depends(),
): ):
if not FileSystem.check_path(path):
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
path = FileSystem.normalize(path)
ctx.logger.info(path)
async with ctx.database.session() as session: async with ctx.database.session() as session:
session.add(user)
await session.refresh(user, attribute_names=["repository"])
if not user.repository:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
if not ( if not (
directory := await Directory.by_path( directory := await Directory.by_path(
user.repository.id, repository,
None if path.parent == Path() else path.parent, path,
path.name, session,
ctx.database, ctx.config,
) )
): ):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found") raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
ctx.logger.info(directory)
async with ctx.database.session() as session: info = await directory.info(session)
session.add(directory) ctx.logger.info(info)
await session.refresh(directory, attribute_names=["files"])
info = DirectoryInfo.model_validate(directory)
info.used = sum([file.size for file in directory.files])
return info return info
@router.delete("/directory") @router.delete("/directory")
async def remove( async def remove(
path: Path, path: Path,
user: User = Depends(middleware.user), repository=Depends(middleware.repository),
ctx: middleware.Context = Depends(), ctx: middleware.Context = Depends(),
): ):
repository_path = Config.data_dir() / "repository" / user.lower_name if not FileSystem.check_path(path):
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
path = FileSystem.normalize(path)
async with ctx.database.session() as session: async with ctx.database.session() as session:
session.add(user)
await session.refresh(user, attribute_names=["repository"])
if not user.repository:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
if not ( if not (
directory := await Directory.by_path( directory := await Directory.by_path(
user.repository.id, repository,
None if path.parent == Path() else path.parent, path,
path.name, session,
ctx.database, ctx.config,
) )
): ):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found") raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
directory_path = repository_path / path await directory.remove(session, ctx.config)
try:
if directory_path.is_dir():
shutil.rmtree(str(directory_path))
except OSError:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove directory"
)
await directory.remove(ctx.database) @router.patch("/directory/rename")
async def rename():
pass
@router.patch("/directory/move")
async def move():
pass
@router.post("/directory/copy")
async def copy():
pass

View File

@ -48,18 +48,14 @@ async def info(
@router.delete("/repository") @router.delete("/repository")
async def remove( async def remove(
repository=Depends(middleware.repository), repository=Depends(middleware.repository),
repository_path=Depends(middleware.repository_path),
ctx: middleware.Context = Depends(), ctx: middleware.Context = Depends(),
): ):
try: try:
if repository_path.exists(): async with ctx.database.session() as session:
shutil.rmtree(str(repository_path)) await repository.remove(session, ctx.config)
except OSError: await session.commit()
raise HTTPException( except Exception as e:
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove repository" raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, f"{e}")
)
await repository.remove(ctx.database)
@router.get("/repository/content", response_model=RepositoryContent) @router.get("/repository/content", response_model=RepositoryContent)

View File

@ -82,5 +82,44 @@ async def test_repository(auth_client: AsyncClient, api_config: Config):
create = await auth_client.post("/api/repository") create = await auth_client.post("/api/repository")
assert create.status_code == 409, create.text assert create.status_code == 409, create.text
assert api_config.application.working_directory.joinpath(
"repository", "PyTest".lower(), "default"
).exists()
info = await auth_client.get("/api/repository") info = await auth_client.get("/api/repository")
assert info.status_code == 200, info.text assert info.status_code == 200, info.text
delete = await auth_client.delete("/api/repository")
assert delete.status_code == 200, delete.text
info = await auth_client.get("/api/repository")
assert info.status_code == 404, info.text
# TODO: content
@pytest.mark.asyncio
async def test_directory(auth_client: AsyncClient, api_config: Config):
create = await auth_client.post("/api/repository")
assert create.status_code == 200, create.text
create = await auth_client.post("/api/directory", json={"path": "first_dir"})
assert create.status_code == 500, create.text
create = await auth_client.post("/api/directory", json={"path": "/first_dir"})
assert create.status_code == 200, create.text
assert api_config.application.working_directory.joinpath(
"repository", "PyTest".lower(), "default", "first_dir"
).exists()
info = await auth_client.get("/api/directory", params=[("path", "/first_dir")])
assert info.status_code == 200, info.text
assert info.json()["used"] == 0
delete = await auth_client.delete("/api/directory", params=[("path", "/first_dir")])
assert delete.status_code == 200, delete.text
assert not api_config.application.working_directory.joinpath(
"repository", "PyTest".lower(), "default", "first_dir"
).exists()