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) current_directory = None
await session.refresh(user, attribute_names=["repository"]) current_path = Path()
directory = None
if not user.repository: for part in path.parts:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found") if not (
directory := await Directory.by_path(
current_directory = None repository, current_path.joinpath(part), session, ctx.config
current_path = Path()
directory = None
for part in directory_path.parts:
if not await Directory.by_path(
user.repository.id, current_path, part, ctx.database
):
directory = Directory(
repository_id=user.repository.id,
parent_id=current_directory.id if current_directory else None,
name=part,
)
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}",
) )
):
directory = await Directory(
repository_id=repository.id,
parent_id=current_directory.id if current_directory else None,
name=part,
).new(session, ctx.config)
async with ctx.database.session() as session: current_directory = directory
session.add(directory) current_path /= part
await session.commit()
await session.refresh(directory)
current_directory = directory await session.commit()
current_path /= part
@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) if not (
await session.refresh(user, attribute_names=["repository"]) directory := await Directory.by_path(
repository,
if not user.repository: path,
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found") session,
ctx.config,
if not ( )
directory := await Directory.by_path( ):
user.repository.id, raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
None if path.parent == Path() else path.parent, ctx.logger.info(directory)
path.name, info = await directory.info(session)
ctx.database, ctx.logger.info(info)
) return info
):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
async with ctx.database.session() as session:
session.add(directory)
await session.refresh(directory, attribute_names=["files"])
info = DirectoryInfo.model_validate(directory)
info.used = sum([file.size for file in directory.files])
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) if not (
await session.refresh(user, attribute_names=["repository"]) directory := await Directory.by_path(
repository,
path,
session,
ctx.config,
)
):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
if not user.repository: await directory.remove(session, ctx.config)
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
if not (
directory := await Directory.by_path(
user.repository.id,
None if path.parent == Path() else path.parent,
path.name,
ctx.database,
)
):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
directory_path = repository_path / path @router.patch("/directory/rename")
async def rename():
pass
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/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()