materia-server: base CRD api, resources routes
This commit is contained in:
parent
4312d5b5d1
commit
aef6c2b541
@ -17,7 +17,13 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from materia_server import config as _config
|
from materia_server import config as _config
|
||||||
from materia_server.config import Config
|
from materia_server.config import Config
|
||||||
from materia_server._logging import make_logger, uvicorn_log_config, Logger
|
from materia_server._logging import make_logger, uvicorn_log_config, Logger
|
||||||
from materia_server.models import Database, DatabaseError, DatabaseMigrationError, Cache, CacheError
|
from materia_server.models import (
|
||||||
|
Database,
|
||||||
|
DatabaseError,
|
||||||
|
DatabaseMigrationError,
|
||||||
|
Cache,
|
||||||
|
CacheError,
|
||||||
|
)
|
||||||
from materia_server import routers
|
from materia_server import routers
|
||||||
|
|
||||||
|
|
||||||
@ -27,6 +33,7 @@ class AppContext(TypedDict):
|
|||||||
database: Database
|
database: Database
|
||||||
cache: Cache
|
cache: Cache
|
||||||
|
|
||||||
|
|
||||||
def make_lifespan(config: Config, logger: Logger):
|
def make_lifespan(config: Config, logger: Logger):
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]:
|
async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]:
|
||||||
@ -50,24 +57,20 @@ def make_lifespan(config: Config, logger: Logger):
|
|||||||
logger.error(f"Failed to connect redis: {e}")
|
logger.error(f"Failed to connect redis: {e}")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
yield AppContext(
|
yield AppContext(config=config, database=database, cache=cache, logger=logger)
|
||||||
config = config,
|
|
||||||
database = database,
|
|
||||||
cache = cache,
|
|
||||||
logger = logger
|
|
||||||
)
|
|
||||||
|
|
||||||
if database.engine is not None:
|
if database.engine is not None:
|
||||||
await database.dispose()
|
await database.dispose()
|
||||||
|
|
||||||
return lifespan
|
return lifespan
|
||||||
|
|
||||||
|
|
||||||
def make_application(config: Config, logger: Logger):
|
def make_application(config: Config, logger: Logger):
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="materia",
|
title="materia",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
docs_url="/api/docs",
|
docs_url="/api/docs",
|
||||||
lifespan = make_lifespan(config, logger)
|
lifespan=make_lifespan(config, logger),
|
||||||
)
|
)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
@ -77,5 +80,6 @@ def make_application(config: Config, logger: Logger):
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
app.include_router(routers.api.router)
|
app.include_router(routers.api.router)
|
||||||
|
app.include_router(routers.resources.router)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -21,10 +21,12 @@ from materia_server.models import Database, DatabaseError, Cache
|
|||||||
from materia_server import routers
|
from materia_server import routers
|
||||||
from materia_server.app import make_application
|
from materia_server.app import make_application
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def server():
|
def server():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@server.command()
|
@server.command()
|
||||||
@click.option("--config_path", type=Path)
|
@click.option("--config_path", type=Path)
|
||||||
@from_pydantic("application", _config.Application, prefix="app")
|
@from_pydantic("application", _config.Application, prefix="app")
|
||||||
@ -39,7 +41,9 @@ def start(application: _config.Application, config_path: Path, log: _config.Log)
|
|||||||
# if group := application.group:
|
# if group := application.group:
|
||||||
# os.setgid(pwd.getpwnam(user).pw_gid)
|
# os.setgid(pwd.getpwnam(user).pw_gid)
|
||||||
# TODO: merge cli options with config
|
# TODO: merge cli options with config
|
||||||
if working_directory := (application.working_directory or config.application.working_directory).resolve():
|
if working_directory := (
|
||||||
|
application.working_directory or config.application.working_directory
|
||||||
|
).resolve():
|
||||||
try:
|
try:
|
||||||
os.chdir(working_directory)
|
os.chdir(working_directory)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
@ -78,7 +82,7 @@ def start(application: _config.Application, config_path: Path, log: _config.Log)
|
|||||||
|
|
||||||
config.log.level = log.level
|
config.log.level = log.level
|
||||||
logger = make_logger(config)
|
logger = make_logger(config)
|
||||||
if (working_directory := config.application.working_directory.resolve()):
|
if working_directory := config.application.working_directory.resolve():
|
||||||
logger.debug(f"Change working directory: {working_directory}")
|
logger.debug(f"Change working directory: {working_directory}")
|
||||||
try:
|
try:
|
||||||
os.chdir(working_directory)
|
os.chdir(working_directory)
|
||||||
@ -99,13 +103,23 @@ def start(application: _config.Application, config_path: Path, log: _config.Log)
|
|||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@server.group()
|
@server.group()
|
||||||
def config():
|
def config():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@config.command("create", help="Create a new configuration file.")
|
@config.command("create", help="Create a new configuration file.")
|
||||||
@click.option("--path", "-p", type = Path, default = Path.cwd().joinpath("config.toml"), help = "Path to the file.")
|
@click.option(
|
||||||
@click.option("--force", "-f", is_flag = True, default = False, help = "Overwrite a file if exists.")
|
"--path",
|
||||||
|
"-p",
|
||||||
|
type=Path,
|
||||||
|
default=Path.cwd().joinpath("config.toml"),
|
||||||
|
help="Path to the file.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force", "-f", is_flag=True, default=False, help="Overwrite a file if exists."
|
||||||
|
)
|
||||||
def config_create(path: Path, force: bool):
|
def config_create(path: Path, force: bool):
|
||||||
path = path.resolve()
|
path = path.resolve()
|
||||||
config = Config()
|
config = Config()
|
||||||
@ -123,8 +137,15 @@ def config_create(path: Path, force: bool):
|
|||||||
config.write(path)
|
config.write(path)
|
||||||
logger.info("All done.")
|
logger.info("All done.")
|
||||||
|
|
||||||
|
|
||||||
@config.command("check", help="Check the configuration file.")
|
@config.command("check", help="Check the configuration file.")
|
||||||
@click.option("--path", "-p", type = Path, default = Path.cwd().joinpath("config.toml"), help = "Path to the file.")
|
@click.option(
|
||||||
|
"--path",
|
||||||
|
"-p",
|
||||||
|
type=Path,
|
||||||
|
default=Path.cwd().joinpath("config.toml"),
|
||||||
|
help="Path to the file.",
|
||||||
|
)
|
||||||
def config_check(path: Path):
|
def config_check(path: Path):
|
||||||
path = path.resolve()
|
path = path.resolve()
|
||||||
config = Config()
|
config = Config()
|
||||||
@ -141,9 +162,6 @@ def config_check(path: Path):
|
|||||||
else:
|
else:
|
||||||
logger.info("OK.")
|
logger.info("OK.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
server()
|
server()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,8 +15,12 @@ class Directory(Base):
|
|||||||
__tablename__ = "directory"
|
__tablename__ = "directory"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
repository_id: Mapped[int] = mapped_column(ForeignKey("repository.id", ondelete = "CASCADE"))
|
repository_id: Mapped[int] = mapped_column(
|
||||||
parent_id: Mapped[int] = mapped_column(ForeignKey("directory.id", ondelete = "CASCADE"), nullable = True)
|
ForeignKey("repository.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
parent_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("directory.id", ondelete="CASCADE"), nullable=True
|
||||||
|
)
|
||||||
created: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
created: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
||||||
updated: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
updated: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
||||||
name: Mapped[str]
|
name: Mapped[str]
|
||||||
@ -24,30 +28,54 @@ class Directory(Base):
|
|||||||
is_public: Mapped[bool] = mapped_column(default=False)
|
is_public: Mapped[bool] = mapped_column(default=False)
|
||||||
|
|
||||||
repository: Mapped["Repository"] = relationship(back_populates="directories")
|
repository: Mapped["Repository"] = relationship(back_populates="directories")
|
||||||
directories: Mapped[List["Directory"]] = relationship(back_populates = "parent", remote_side = [id])
|
directories: Mapped[List["Directory"]] = relationship(
|
||||||
|
back_populates="parent", remote_side=[id]
|
||||||
|
)
|
||||||
parent: Mapped["Directory"] = relationship(back_populates="directories")
|
parent: Mapped["Directory"] = relationship(back_populates="directories")
|
||||||
files: Mapped[List["File"]] = relationship(back_populates="parent")
|
files: Mapped[List["File"]] = relationship(back_populates="parent")
|
||||||
link: Mapped["DirectoryLink"] = relationship(back_populates="directory")
|
link: Mapped["DirectoryLink"] = relationship(back_populates="directory")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def by_path(repository_id: int, path: Path | None, name: str, db: database.Database) -> Self | None:
|
async def by_path(
|
||||||
|
repository_id: int, path: Path | None, name: str, db: database.Database
|
||||||
|
) -> Self | None:
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
query_path = Directory.path == str(path) if isinstance(path, Path) else Directory.path.is_(None)
|
query_path = (
|
||||||
return (await session
|
Directory.path == str(path)
|
||||||
.scalars(sa.select(Directory)
|
if isinstance(path, Path)
|
||||||
.where(sa.and_(Directory.repository_id == repository_id, Directory.name == name, query_path)))
|
else Directory.path.is_(None)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
await session.scalars(
|
||||||
|
sa.select(Directory).where(
|
||||||
|
sa.and_(
|
||||||
|
Directory.repository_id == repository_id,
|
||||||
|
Directory.name == name,
|
||||||
|
query_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
async def remove(self, db: database.Database):
|
||||||
|
async with db.session() as session:
|
||||||
|
await session.execute(sa.delete(Directory).where(Directory.id == self.id))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
class DirectoryLink(Base):
|
class DirectoryLink(Base):
|
||||||
__tablename__ = "directory_link"
|
__tablename__ = "directory_link"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
directory_id: Mapped[int] = mapped_column(ForeignKey("directory.id", ondelete = "CASCADE"))
|
directory_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("directory.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
created: Mapped[int] = mapped_column(BigInteger, default=time)
|
created: Mapped[int] = mapped_column(BigInteger, default=time)
|
||||||
url: Mapped[str]
|
url: Mapped[str]
|
||||||
|
|
||||||
directory: Mapped["Directory"] = relationship(back_populates="link")
|
directory: Mapped["Directory"] = relationship(back_populates="link")
|
||||||
|
|
||||||
|
|
||||||
class DirectoryInfo(BaseModel):
|
class DirectoryInfo(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@ -10,12 +10,17 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from materia_server.models.base import Base
|
from materia_server.models.base import Base
|
||||||
from materia_server.models import database
|
from materia_server.models import database
|
||||||
|
|
||||||
|
|
||||||
class File(Base):
|
class File(Base):
|
||||||
__tablename__ = "file"
|
__tablename__ = "file"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
repository_id: Mapped[int] = mapped_column(ForeignKey("repository.id", ondelete = "CASCADE"))
|
repository_id: Mapped[int] = mapped_column(
|
||||||
parent_id: Mapped[int] = mapped_column(ForeignKey("directory.id"), nullable = True)
|
ForeignKey("repository.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
parent_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("directory.id", ondelete="CASCADE"), nullable=True
|
||||||
|
)
|
||||||
created: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
created: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
||||||
updated: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
updated: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
||||||
name: Mapped[str]
|
name: Mapped[str]
|
||||||
@ -28,12 +33,25 @@ class File(Base):
|
|||||||
link: Mapped["FileLink"] = relationship(back_populates="file")
|
link: Mapped["FileLink"] = relationship(back_populates="file")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def by_path(repository_id: int, path: Path | None, name: str, db: database.Database) -> Self | None:
|
async def by_path(
|
||||||
|
repository_id: int, path: Path | None, name: str, db: database.Database
|
||||||
|
) -> Self | None:
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
query_path = File.path == str(path) if isinstance(path, Path) else File.path.is_(None)
|
query_path = (
|
||||||
return (await session
|
File.path == str(path)
|
||||||
.scalars(sa.select(File)
|
if isinstance(path, Path)
|
||||||
.where(sa.and_(File.repository_id == repository_id, File.name == name, query_path)))
|
else File.path.is_(None)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
await session.scalars(
|
||||||
|
sa.select(File).where(
|
||||||
|
sa.and_(
|
||||||
|
File.repository_id == repository_id,
|
||||||
|
File.name == name,
|
||||||
|
query_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
async def remove(self, db: database.Database):
|
async def remove(self, db: database.Database):
|
||||||
@ -41,6 +59,7 @@ class File(Base):
|
|||||||
await session.execute(sa.delete(File).where(File.id == self.id))
|
await session.execute(sa.delete(File).where(File.id == self.id))
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
class FileLink(Base):
|
class FileLink(Base):
|
||||||
__tablename__ = "file_link"
|
__tablename__ = "file_link"
|
||||||
|
|
||||||
@ -51,6 +70,7 @@ class FileLink(Base):
|
|||||||
|
|
||||||
file: Mapped["File"] = relationship(back_populates="link")
|
file: Mapped["File"] = relationship(back_populates="link")
|
||||||
|
|
||||||
|
|
||||||
class FileInfo(BaseModel):
|
class FileInfo(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 86dd738cbd40
|
||||||
|
Revises: 939b37d98be0
|
||||||
|
Create Date: 2024-07-05 16:42:31.645410
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '86dd738cbd40'
|
||||||
|
down_revision: Union[str, None] = '939b37d98be0'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('file_parent_id_fkey', 'file', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'file', 'directory', ['parent_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('repository_user_id_fkey', 'repository', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'repository', 'user', ['user_id'], ['id'], ondelete='CASCADE')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'repository', type_='foreignkey')
|
||||||
|
op.create_foreign_key('repository_user_id_fkey', 'repository', 'user', ['user_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'file', type_='foreignkey')
|
||||||
|
op.create_foreign_key('file_parent_id_fkey', 'file', 'directory', ['parent_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
@ -16,7 +16,7 @@ class Repository(Base):
|
|||||||
__tablename__ = "repository"
|
__tablename__ = "repository"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id"))
|
user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"))
|
||||||
capacity: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
capacity: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||||
|
|
||||||
user: Mapped["User"] = relationship(back_populates="repository")
|
user: Mapped["User"] = relationship(back_populates="repository")
|
||||||
@ -24,7 +24,11 @@ class Repository(Base):
|
|||||||
files: Mapped[List["File"]] = relationship(back_populates="repository")
|
files: Mapped[List["File"]] = relationship(back_populates="repository")
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return { k: getattr(self, k) for k, v in Repository.__dict__.items() if isinstance(v, InstrumentedAttribute) }
|
return {
|
||||||
|
k: getattr(self, k)
|
||||||
|
for k, v in Repository.__dict__.items()
|
||||||
|
if isinstance(v, InstrumentedAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
async def create(self, db: database.Database):
|
async def create(self, db: database.Database):
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
@ -33,19 +37,33 @@ class Repository(Base):
|
|||||||
|
|
||||||
async def update(self, db: database.Database):
|
async def update(self, db: database.Database):
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
await session.execute(sa.update(Repository).where(Repository.id == self.id).values(self.to_dict()))
|
await session.execute(
|
||||||
|
sa.update(Repository)
|
||||||
|
.where(Repository.id == self.id)
|
||||||
|
.values(self.to_dict())
|
||||||
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def by_user_id(user_id: UUID, db: database.Database) -> Self | None:
|
async def by_user_id(user_id: UUID, db: database.Database) -> Self | None:
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
return (await session.scalars(sa.select(Repository).where(Repository.user_id == user_id))).first()
|
return (
|
||||||
|
await session.scalars(
|
||||||
|
sa.select(Repository).where(Repository.user_id == user_id)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
async def remove(self, db: database.Database):
|
||||||
|
async with db.session() as session:
|
||||||
|
await session.execute(sa.delete(Repository).where(Repository.id == self.id))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
class RepositoryInfo(BaseModel):
|
class RepositoryInfo(BaseModel):
|
||||||
capacity: int
|
capacity: int
|
||||||
used: int
|
used: int
|
||||||
|
|
||||||
|
|
||||||
from materia_server.models.user import User
|
from materia_server.models.user import User
|
||||||
from materia_server.models.directory import Directory
|
from materia_server.models.directory import Directory
|
||||||
from materia_server.models.file import File
|
from materia_server.models.file import File
|
||||||
|
@ -17,6 +17,7 @@ from loguru import logger
|
|||||||
valid_username = re.compile(r"^[\da-zA-Z][-.\w]*$")
|
valid_username = re.compile(r"^[\da-zA-Z][-.\w]*$")
|
||||||
invalid_username = re.compile(r"[-._]{2,}|[-._]$")
|
invalid_username = re.compile(r"[-._]{2,}|[-._]$")
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
|
|
||||||
@ -63,23 +64,34 @@ class User(Base):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def by_name(name: str, db: database.Database):
|
async def by_name(name: str, db: database.Database):
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
return (await session.scalars(sa.select(User).where(User.name == name))).first()
|
return (
|
||||||
|
await session.scalars(sa.select(User).where(User.name == name))
|
||||||
|
).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def by_email(email: str, db: database.Database):
|
async def by_email(email: str, db: database.Database):
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
return (await session.scalars(sa.select(User).where(User.email == email))).first()
|
return (
|
||||||
|
await session.scalars(sa.select(User).where(User.email == email))
|
||||||
|
).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def by_id(id: UUID, db: database.Database):
|
async def by_id(id: UUID, db: database.Database):
|
||||||
async with db.session() as session:
|
async with db.session() as session:
|
||||||
return (await session.scalars(sa.select(User).where(User.id == id))).first()
|
return (await session.scalars(sa.select(User).where(User.id == id))).first()
|
||||||
|
|
||||||
|
async def remove(self, db: database.Database):
|
||||||
|
async with db.session() as session:
|
||||||
|
await session.execute(sa.delete(User).where(User.id == self.id))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
class UserCredentials(BaseModel):
|
class UserCredentials(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
password: str
|
password: str
|
||||||
email: Optional[EmailStr]
|
email: Optional[EmailStr]
|
||||||
|
|
||||||
|
|
||||||
class UserInfo(BaseModel):
|
class UserInfo(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@ -87,7 +99,7 @@ class UserInfo(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
lower_name: str
|
lower_name: str
|
||||||
full_name: Optional[str]
|
full_name: Optional[str]
|
||||||
email: str
|
email: Optional[str]
|
||||||
is_email_private: bool
|
is_email_private: bool
|
||||||
must_change_password: bool
|
must_change_password: bool
|
||||||
|
|
||||||
@ -102,4 +114,5 @@ class UserInfo(BaseModel):
|
|||||||
|
|
||||||
avatar: Optional[str]
|
avatar: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
from materia_server.models.repository import Repository
|
from materia_server.models.repository import Repository
|
||||||
|
@ -1 +1 @@
|
|||||||
from materia_server.routers import middleware, api
|
from materia_server.routers import middleware, api, resources
|
||||||
|
@ -2,7 +2,7 @@ from fastapi import APIRouter
|
|||||||
from materia_server.routers.api.auth import auth, oauth
|
from materia_server.routers.api.auth import auth, oauth
|
||||||
from materia_server.routers.api import user, repository, directory, file
|
from materia_server.routers.api import user, repository, directory, file
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(prefix="/api")
|
||||||
router.include_router(auth.router)
|
router.include_router(auth.router)
|
||||||
router.include_router(oauth.router)
|
router.include_router(oauth.router)
|
||||||
router.include_router(user.router)
|
router.include_router(user.router)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
@ -10,30 +11,39 @@ from materia_server.config import Config
|
|||||||
|
|
||||||
router = APIRouter(tags=["directory"])
|
router = APIRouter(tags=["directory"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/directory")
|
@router.post("/directory")
|
||||||
async def create(path: Path = Path(), user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def create(
|
||||||
|
path: Path = Path(),
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
blacklist = [os.sep, ".", "..", "*"]
|
blacklist = [os.sep, ".", "..", "*"]
|
||||||
directory_path = Path(os.sep.join(filter(lambda part: part not in blacklist, path.parts)))
|
directory_path = Path(
|
||||||
|
os.sep.join(filter(lambda part: part not in blacklist, path.parts))
|
||||||
|
)
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
|
|
||||||
if not user.repository:
|
if not user.repository:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository is not found")
|
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 directory_path.parts:
|
||||||
if not await Directory.by_path(user.repository.id, current_path, part, ctx.database):
|
if not await Directory.by_path(
|
||||||
|
user.repository.id, current_path, part, ctx.database
|
||||||
|
):
|
||||||
directory = Directory(
|
directory = Directory(
|
||||||
repository_id=user.repository.id,
|
repository_id=user.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,
|
||||||
path = None if current_path == Path() else str(current_path)
|
path=None if current_path == Path() else str(current_path),
|
||||||
)
|
)
|
||||||
session.add(directory)
|
session.add(directory)
|
||||||
|
|
||||||
@ -43,22 +53,37 @@ async def create(path: Path = Path(), user: User = Depends(middleware.user), ctx
|
|||||||
try:
|
try:
|
||||||
(repository_path / directory_path).mkdir(parents=True, exist_ok=True)
|
(repository_path / directory_path).mkdir(parents=True, exist_ok=True)
|
||||||
except OSError:
|
except OSError:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to created a directory")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to created a directory"
|
||||||
|
)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/directory")
|
@router.get("/directory")
|
||||||
async def info(path: Path, user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def info(
|
||||||
|
path: Path,
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
|
|
||||||
if not user.repository:
|
if not user.repository:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository is not found")
|
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)):
|
if not (
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory is not found")
|
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")
|
||||||
|
|
||||||
|
async with ctx.database.session() as session:
|
||||||
session.add(directory)
|
session.add(directory)
|
||||||
await session.refresh(directory, attribute_names=["files"])
|
await session.refresh(directory, attribute_names=["files"])
|
||||||
|
|
||||||
@ -67,3 +92,40 @@ async def info(path: Path, user: User = Depends(middleware.user), ctx: middlewar
|
|||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/directory")
|
||||||
|
async def remove(
|
||||||
|
path: Path,
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
|
|
||||||
|
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 (
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -10,32 +10,42 @@ from materia_server.config import Config
|
|||||||
|
|
||||||
router = APIRouter(tags=["file"])
|
router = APIRouter(tags=["file"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/file")
|
@router.post("/file")
|
||||||
async def create(upload_file: UploadFile, path: Path = Path(), user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def create(
|
||||||
|
upload_file: UploadFile,
|
||||||
|
path: Path = Path(),
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
if not upload_file.filename:
|
if not upload_file.filename:
|
||||||
raise HTTPException(status.HTTP_417_EXPECTATION_FAILED, "Cannot upload file without name")
|
raise HTTPException(
|
||||||
|
status.HTTP_417_EXPECTATION_FAILED, "Cannot upload file without name"
|
||||||
|
)
|
||||||
|
|
||||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
blacklist = [os.sep, ".", "..", "*"]
|
blacklist = [os.sep, ".", "..", "*"]
|
||||||
directory_path = Path(os.sep.join(filter(lambda part: part not in blacklist, path.parts)))
|
directory_path = Path(
|
||||||
|
os.sep.join(filter(lambda part: part not in blacklist, path.parts))
|
||||||
|
)
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
|
|
||||||
if not user.repository:
|
if not user.repository:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository is not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
||||||
|
|
||||||
if not directory_path == Path():
|
if not directory_path == Path():
|
||||||
directory = await Directory.by_path(
|
directory = await Directory.by_path(
|
||||||
user.repository.id,
|
user.repository.id,
|
||||||
None if directory_path.parent == Path() else directory_path.parent,
|
None if directory_path.parent == Path() else directory_path.parent,
|
||||||
directory_path.name,
|
directory_path.name,
|
||||||
ctx.database
|
ctx.database,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not directory:
|
if not directory:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory is not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
|
||||||
else:
|
else:
|
||||||
directory = None
|
directory = None
|
||||||
|
|
||||||
@ -44,25 +54,34 @@ async def create(upload_file: UploadFile, path: Path = Path(), user: User = Depe
|
|||||||
parent_id=directory.id if directory else None,
|
parent_id=directory.id if directory else None,
|
||||||
name=upload_file.filename,
|
name=upload_file.filename,
|
||||||
path=None if directory_path == Path() else str(directory_path),
|
path=None if directory_path == Path() else str(directory_path),
|
||||||
size = upload_file.size
|
size=upload_file.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_path = repository_path.joinpath(directory_path, upload_file.filename)
|
file_path = repository_path.joinpath(directory_path, upload_file.filename)
|
||||||
|
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
raise HTTPException(status.HTTP_409_CONFLICT, "File with given name already exists")
|
raise HTTPException(
|
||||||
|
status.HTTP_409_CONFLICT, "File with given name already exists"
|
||||||
|
)
|
||||||
|
|
||||||
file_path.write_bytes(await upload_file.read())
|
file_path.write_bytes(await upload_file.read())
|
||||||
except OSError:
|
except OSError:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to write a file")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to write a file"
|
||||||
|
)
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(file)
|
session.add(file)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/file")
|
@router.get("/file")
|
||||||
async def info(path: Path, user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def info(
|
||||||
|
path: Path,
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
@ -70,15 +89,27 @@ async def info(path: Path, user: User = Depends(middleware.user), ctx: middlewar
|
|||||||
if not user.repository:
|
if not user.repository:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
||||||
|
|
||||||
if not(file := await File.by_path(user.repository.id, None if path.parent == Path() else path.parent, path.name, ctx.database)):
|
if not (
|
||||||
|
file := await File.by_path(
|
||||||
|
user.repository.id,
|
||||||
|
None if path.parent == Path() else path.parent,
|
||||||
|
path.name,
|
||||||
|
ctx.database,
|
||||||
|
)
|
||||||
|
):
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
||||||
|
|
||||||
info = FileInfo.model_validate(file)
|
info = FileInfo.model_validate(file)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/file")
|
@router.delete("/file")
|
||||||
async def remove(path: Path, user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def remove(
|
||||||
|
path: Path,
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
@ -86,7 +117,14 @@ async def remove(path: Path, user: User = Depends(middleware.user), ctx: middlew
|
|||||||
if not user.repository:
|
if not user.repository:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
||||||
|
|
||||||
if not(file := await File.by_path(user.repository.id, None if path.parent == Path() else path.parent, path.name, ctx.database)):
|
if not (
|
||||||
|
file := await File.by_path(
|
||||||
|
user.repository.id,
|
||||||
|
None if path.parent == Path() else path.parent,
|
||||||
|
path.name,
|
||||||
|
ctx.database,
|
||||||
|
)
|
||||||
|
):
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
||||||
|
|
||||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
@ -97,6 +135,8 @@ async def remove(path: Path, user: User = Depends(middleware.user), ctx: middlew
|
|||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
file_path.unlink(missing_ok=True)
|
file_path.unlink(missing_ok=True)
|
||||||
except OSError:
|
except OSError:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove a file")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove a file"
|
||||||
|
)
|
||||||
|
|
||||||
await file.remove(ctx.database)
|
await file.remove(ctx.database)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import shutil
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
from materia_server.models import User, Repository, RepositoryInfo
|
from materia_server.models import User, Repository, RepositoryInfo
|
||||||
@ -7,39 +8,65 @@ from materia_server.config import Config
|
|||||||
|
|
||||||
router = APIRouter(tags=["repository"])
|
router = APIRouter(tags=["repository"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/repository")
|
@router.post("/repository")
|
||||||
async def create(user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def create(
|
||||||
|
user: User = Depends(middleware.user), ctx: middleware.Context = Depends()
|
||||||
|
):
|
||||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
|
|
||||||
if await Repository.by_user_id(user.id, ctx.database):
|
if await Repository.by_user_id(user.id, ctx.database):
|
||||||
raise HTTPException(status.HTTP_409_CONFLICT, "Repository already exists")
|
raise HTTPException(status.HTTP_409_CONFLICT, "Repository already exists")
|
||||||
|
|
||||||
repository = Repository(
|
repository = Repository(user_id=user.id, capacity=ctx.config.repository.capacity)
|
||||||
user_id = user.id,
|
|
||||||
capacity = ctx.config.repository.capacity
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
repository_path.mkdir(parents=True, exist_ok=True)
|
repository_path.mkdir(parents=True, exist_ok=True)
|
||||||
except OSError:
|
except OSError:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to created a repository")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to created a repository"
|
||||||
|
)
|
||||||
|
|
||||||
await repository.create(ctx.database)
|
await repository.create(ctx.database)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/repository", response_model=RepositoryInfo)
|
@router.get("/repository", response_model=RepositoryInfo)
|
||||||
async def info(user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def info(
|
||||||
|
user: User = Depends(middleware.user), ctx: middleware.Context = Depends()
|
||||||
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
|
|
||||||
if not (repository := user.repository):
|
if not (repository := user.repository):
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository is not found")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
||||||
|
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
session.add(repository)
|
||||||
await session.refresh(repository, attribute_names=["files"])
|
await session.refresh(repository, attribute_names=["files"])
|
||||||
|
|
||||||
return RepositoryInfo(
|
return RepositoryInfo(
|
||||||
capacity=repository.capacity,
|
capacity=repository.capacity,
|
||||||
used = sum([ file.size for file in repository.files ])
|
used=sum([file.size for file in repository.files]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/repository")
|
||||||
|
async def remove(
|
||||||
|
user: User = Depends(middleware.user), ctx: middleware.Context = Depends()
|
||||||
|
):
|
||||||
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
|
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
session.add(user)
|
||||||
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
if repository_path.exists():
|
||||||
|
shutil.rmtree(str(repository_path))
|
||||||
|
except OSError:
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.repository.remove(ctx.database)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import io
|
import io
|
||||||
|
import shutil
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -14,15 +14,48 @@ from materia_server.routers import middleware
|
|||||||
|
|
||||||
router = APIRouter(tags=["user"])
|
router = APIRouter(tags=["user"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user", response_model=UserInfo)
|
@router.get("/user", response_model=UserInfo)
|
||||||
async def info(claims = Depends(middleware.jwt_cookie), ctx: middleware.Context = Depends()):
|
async def info(
|
||||||
|
claims=Depends(middleware.jwt_cookie), ctx: middleware.Context = Depends()
|
||||||
|
):
|
||||||
if not (current_user := await User.by_id(uuid.UUID(claims.sub), ctx.database)):
|
if not (current_user := await User.by_id(uuid.UUID(claims.sub), ctx.database)):
|
||||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
|
||||||
|
|
||||||
return UserInfo.model_validate(current_user)
|
info = UserInfo.model_validate(current_user)
|
||||||
|
if current_user.is_email_private:
|
||||||
|
info.email = None
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/user")
|
||||||
|
async def remove(
|
||||||
|
user: User = Depends(middleware.user), ctx: middleware.Context = Depends()
|
||||||
|
):
|
||||||
|
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||||
|
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
session.add(user)
|
||||||
|
await session.refresh(user, attribute_names=["repository"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
if repository_path.exists():
|
||||||
|
shutil.rmtree(str(repository_path))
|
||||||
|
except OSError:
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove user"
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.repository.remove(ctx.database)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/user/avatar")
|
@router.post("/user/avatar")
|
||||||
async def avatar(file: UploadFile, user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
async def avatar(
|
||||||
|
file: UploadFile,
|
||||||
|
user: User = Depends(middleware.user),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
avatars: list[str] = (await session.scalars(sa.select(User.avatar))).all()
|
avatars: list[str] = (await session.scalars(sa.select(User.avatar))).all()
|
||||||
avatars = list(filter(lambda avatar_hash: avatar_hash, avatars))
|
avatars = list(filter(lambda avatar_hash: avatar_hash, avatars))
|
||||||
@ -32,19 +65,25 @@ async def avatar(file: UploadFile, user: User = Depends(middleware.user), ctx: m
|
|||||||
try:
|
try:
|
||||||
img = Image.open(io.BytesIO(await file.read()))
|
img = Image.open(io.BytesIO(await file.read()))
|
||||||
except OSError as _:
|
except OSError as _:
|
||||||
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "Failed to read file data")
|
raise HTTPException(
|
||||||
|
status.HTTP_422_UNPROCESSABLE_ENTITY, "Failed to read file data"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not (avatars_dir := Config.data_dir() / "avatars").exists():
|
if not (avatars_dir := Config.data_dir() / "avatars").exists():
|
||||||
avatars_dir.mkdir()
|
avatars_dir.mkdir()
|
||||||
img.save(avatars_dir / avatar_id, format=img.format)
|
img.save(avatars_dir / avatar_id, format=img.format)
|
||||||
except OSError as _:
|
except OSError as _:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to save avatar")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to save avatar"
|
||||||
|
)
|
||||||
|
|
||||||
if old_avatar := user.avatar:
|
if old_avatar := user.avatar:
|
||||||
if (old_file := Config.data_dir() / "avatars" / old_avatar).exists():
|
if (old_file := Config.data_dir() / "avatars" / old_avatar).exists():
|
||||||
old_file.unlink()
|
old_file.unlink()
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
await session.execute(sa.update(user.User).where(user.User.id == user.id).values(avatar = avatar_id))
|
await session.execute(
|
||||||
|
sa.update(User).where(User.id == user.id).values(avatar=avatar_id)
|
||||||
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
39
materia-server/src/materia_server/routers/resources.py
Normal file
39
materia-server/src/materia_server/routers/resources.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
from materia_server.routers import middleware
|
||||||
|
from materia_server.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(tags=["resources"], prefix="/resources")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/avatars/{avatar_id}")
|
||||||
|
async def avatar(
|
||||||
|
avatar_id: str, format: str = "png", ctx: middleware.Context = Depends()
|
||||||
|
):
|
||||||
|
avatar_path = Config.data_dir() / "avatars" / avatar_id
|
||||||
|
format = format.upper()
|
||||||
|
|
||||||
|
if not avatar_path.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_404_NOT_FOUND, "Failed to find the given avatar"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
img = Image.open(avatar_path)
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
|
||||||
|
if format == "JPEG":
|
||||||
|
img.convert("RGB")
|
||||||
|
|
||||||
|
img.save(buffer, format=format)
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_422_UNPROCESSABLE_ENTITY, "Failed to process image file"
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(content=buffer.getvalue(), media_type=Image.MIME[format])
|
Loading…
Reference in New Issue
Block a user