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.config import Config
|
||||
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
|
||||
|
||||
|
||||
@ -27,6 +33,7 @@ class AppContext(TypedDict):
|
||||
database: Database
|
||||
cache: Cache
|
||||
|
||||
|
||||
def make_lifespan(config: Config, logger: Logger):
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]:
|
||||
@ -50,32 +57,29 @@ def make_lifespan(config: Config, logger: Logger):
|
||||
logger.error(f"Failed to connect redis: {e}")
|
||||
sys.exit()
|
||||
|
||||
yield AppContext(
|
||||
config = config,
|
||||
database = database,
|
||||
cache = cache,
|
||||
logger = logger
|
||||
)
|
||||
yield AppContext(config=config, database=database, cache=cache, logger=logger)
|
||||
|
||||
if database.engine is not None:
|
||||
await database.dispose()
|
||||
|
||||
return lifespan
|
||||
|
||||
|
||||
def make_application(config: Config, logger: Logger):
|
||||
app = FastAPI(
|
||||
title = "materia",
|
||||
version = "0.1.0",
|
||||
docs_url = "/api/docs",
|
||||
lifespan = make_lifespan(config, logger)
|
||||
title="materia",
|
||||
version="0.1.0",
|
||||
docs_url="/api/docs",
|
||||
lifespan=make_lifespan(config, logger),
|
||||
)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins = [ "http://localhost", "http://localhost:5173" ],
|
||||
allow_credentials = True,
|
||||
allow_methods = ["*"],
|
||||
allow_headers = ["*"],
|
||||
allow_origins=["http://localhost", "http://localhost:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(routers.api.router)
|
||||
app.include_router(routers.resources.router)
|
||||
|
||||
return app
|
||||
|
@ -21,25 +21,29 @@ from materia_server.models import Database, DatabaseError, Cache
|
||||
from materia_server import routers
|
||||
from materia_server.app import make_application
|
||||
|
||||
|
||||
@click.group()
|
||||
def server():
|
||||
pass
|
||||
|
||||
|
||||
@server.command()
|
||||
@click.option("--config_path", type = Path)
|
||||
@from_pydantic("application", _config.Application, prefix = "app")
|
||||
@from_pydantic("log", _config.Log, prefix = "log")
|
||||
@click.option("--config_path", type=Path)
|
||||
@from_pydantic("application", _config.Application, prefix="app")
|
||||
@from_pydantic("log", _config.Log, prefix="log")
|
||||
def start(application: _config.Application, config_path: Path, log: _config.Log):
|
||||
config = Config()
|
||||
config.log = log
|
||||
logger = make_logger(config)
|
||||
|
||||
#if user := application.user:
|
||||
# if user := application.user:
|
||||
# os.setuid(pwd.getpwnam(user).pw_uid)
|
||||
#if group := application.group:
|
||||
# if group := application.group:
|
||||
# os.setgid(pwd.getpwnam(user).pw_gid)
|
||||
# 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:
|
||||
os.chdir(working_directory)
|
||||
except FileNotFoundError as e:
|
||||
@ -78,7 +82,7 @@ def start(application: _config.Application, config_path: Path, log: _config.Log)
|
||||
|
||||
config.log.level = log.level
|
||||
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}")
|
||||
try:
|
||||
os.chdir(working_directory)
|
||||
@ -91,21 +95,31 @@ def start(application: _config.Application, config_path: Path, log: _config.Log)
|
||||
try:
|
||||
uvicorn.run(
|
||||
make_application(config, logger),
|
||||
port = config.server.port,
|
||||
host = str(config.server.address),
|
||||
port=config.server.port,
|
||||
host=str(config.server.address),
|
||||
# reload = config.application.mode == "development",
|
||||
log_config = uvicorn_log_config(config),
|
||||
log_config=uvicorn_log_config(config),
|
||||
)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
|
||||
|
||||
@server.group()
|
||||
def config():
|
||||
pass
|
||||
|
||||
@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("--force", "-f", is_flag = True, default = False, help = "Overwrite a file if exists.")
|
||||
|
||||
@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(
|
||||
"--force", "-f", is_flag=True, default=False, help="Overwrite a file if exists."
|
||||
)
|
||||
def config_create(path: Path, force: bool):
|
||||
path = path.resolve()
|
||||
config = Config()
|
||||
@ -117,14 +131,21 @@ def config_create(path: Path, force: bool):
|
||||
|
||||
if not path.parent.exists():
|
||||
logger.info("Creating directory at {}", path)
|
||||
path.mkdir(parents = True)
|
||||
path.mkdir(parents=True)
|
||||
|
||||
logger.info("Writing configuration file at {}", path)
|
||||
config.write(path)
|
||||
logger.info("All done.")
|
||||
|
||||
@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.")
|
||||
|
||||
@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.",
|
||||
)
|
||||
def config_check(path: Path):
|
||||
path = path.resolve()
|
||||
config = Config()
|
||||
@ -141,9 +162,6 @@ def config_check(path: Path):
|
||||
else:
|
||||
logger.info("OK.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server()
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -14,42 +14,70 @@ from materia_server.models import database
|
||||
class Directory(Base):
|
||||
__tablename__ = "directory"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key = True)
|
||||
repository_id: Mapped[int] = mapped_column(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)
|
||||
updated: Mapped[int] = mapped_column(BigInteger, nullable = False, default = time)
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
repository_id: Mapped[int] = mapped_column(
|
||||
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)
|
||||
updated: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
||||
name: Mapped[str]
|
||||
path: Mapped[str] = mapped_column(nullable = True)
|
||||
is_public: Mapped[bool] = mapped_column(default = False)
|
||||
path: Mapped[str] = mapped_column(nullable=True)
|
||||
is_public: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
repository: Mapped["Repository"] = relationship(back_populates = "directories")
|
||||
directories: Mapped[List["Directory"]] = relationship(back_populates = "parent", remote_side = [id])
|
||||
parent: Mapped["Directory"] = relationship(back_populates = "directories")
|
||||
files: Mapped[List["File"]] = relationship(back_populates = "parent")
|
||||
link: Mapped["DirectoryLink"] = relationship(back_populates = "directory")
|
||||
repository: Mapped["Repository"] = relationship(back_populates="directories")
|
||||
directories: Mapped[List["Directory"]] = relationship(
|
||||
back_populates="parent", remote_side=[id]
|
||||
)
|
||||
parent: Mapped["Directory"] = relationship(back_populates="directories")
|
||||
files: Mapped[List["File"]] = relationship(back_populates="parent")
|
||||
link: Mapped["DirectoryLink"] = relationship(back_populates="directory")
|
||||
|
||||
@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:
|
||||
query_path = Directory.path == str(path) if isinstance(path, 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)))
|
||||
query_path = (
|
||||
Directory.path == str(path)
|
||||
if isinstance(path, 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()
|
||||
|
||||
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):
|
||||
__tablename__ = "directory_link"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key = True)
|
||||
directory_id: Mapped[int] = mapped_column(ForeignKey("directory.id", ondelete = "CASCADE"))
|
||||
created: Mapped[int] = mapped_column(BigInteger, default = time)
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
directory_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("directory.id", ondelete="CASCADE")
|
||||
)
|
||||
created: Mapped[int] = mapped_column(BigInteger, default=time)
|
||||
url: Mapped[str]
|
||||
|
||||
directory: Mapped["Directory"] = relationship(back_populates = "link")
|
||||
directory: Mapped["Directory"] = relationship(back_populates="link")
|
||||
|
||||
|
||||
class DirectoryInfo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes = True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
repository_id: int
|
||||
|
@ -10,30 +10,48 @@ from pydantic import BaseModel, ConfigDict
|
||||
from materia_server.models.base import Base
|
||||
from materia_server.models import database
|
||||
|
||||
|
||||
class File(Base):
|
||||
__tablename__ = "file"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key = True)
|
||||
repository_id: Mapped[int] = mapped_column(ForeignKey("repository.id", ondelete = "CASCADE"))
|
||||
parent_id: Mapped[int] = mapped_column(ForeignKey("directory.id"), nullable = True)
|
||||
created: Mapped[int] = mapped_column(BigInteger, nullable = False, default = time)
|
||||
updated: Mapped[int] = mapped_column(BigInteger, nullable = False, default = time)
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
repository_id: Mapped[int] = mapped_column(
|
||||
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)
|
||||
updated: Mapped[int] = mapped_column(BigInteger, nullable=False, default=time)
|
||||
name: Mapped[str]
|
||||
path: Mapped[str] = mapped_column(nullable = True)
|
||||
is_public: Mapped[bool] = mapped_column(default = False)
|
||||
path: Mapped[str] = mapped_column(nullable=True)
|
||||
is_public: Mapped[bool] = mapped_column(default=False)
|
||||
size: Mapped[int] = mapped_column(BigInteger)
|
||||
|
||||
repository: Mapped["Repository"] = relationship(back_populates = "files")
|
||||
parent: Mapped["Directory"] = relationship(back_populates = "files")
|
||||
link: Mapped["FileLink"] = relationship(back_populates = "file")
|
||||
repository: Mapped["Repository"] = relationship(back_populates="files")
|
||||
parent: Mapped["Directory"] = relationship(back_populates="files")
|
||||
link: Mapped["FileLink"] = relationship(back_populates="file")
|
||||
|
||||
@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:
|
||||
query_path = File.path == str(path) if isinstance(path, 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)))
|
||||
query_path = (
|
||||
File.path == str(path)
|
||||
if isinstance(path, 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()
|
||||
|
||||
async def remove(self, db: database.Database):
|
||||
@ -41,18 +59,20 @@ class File(Base):
|
||||
await session.execute(sa.delete(File).where(File.id == self.id))
|
||||
await session.commit()
|
||||
|
||||
|
||||
class FileLink(Base):
|
||||
__tablename__ = "file_link"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key = True)
|
||||
file_id: Mapped[int] = mapped_column(ForeignKey("file.id", ondelete = "CASCADE"))
|
||||
created: Mapped[int] = mapped_column(BigInteger, default = time)
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
file_id: Mapped[int] = mapped_column(ForeignKey("file.id", ondelete="CASCADE"))
|
||||
created: Mapped[int] = mapped_column(BigInteger, default=time)
|
||||
url: Mapped[str]
|
||||
|
||||
file: Mapped["File"] = relationship(back_populates = "link")
|
||||
file: Mapped["File"] = relationship(back_populates="link")
|
||||
|
||||
|
||||
class FileInfo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes = True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
repository_id: int
|
||||
|
@ -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 ###
|
@ -15,16 +15,20 @@ from materia_server.models import database
|
||||
class Repository(Base):
|
||||
__tablename__ = "repository"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key = True)
|
||||
user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id"))
|
||||
capacity: Mapped[int] = mapped_column(BigInteger, nullable = False)
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"))
|
||||
capacity: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
|
||||
user: Mapped["User"] = relationship(back_populates = "repository")
|
||||
directories: Mapped[List["Directory"]] = relationship(back_populates = "repository")
|
||||
files: Mapped[List["File"]] = relationship(back_populates = "repository")
|
||||
user: Mapped["User"] = relationship(back_populates="repository")
|
||||
directories: Mapped[List["Directory"]] = relationship(back_populates="repository")
|
||||
files: Mapped[List["File"]] = relationship(back_populates="repository")
|
||||
|
||||
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 with db.session() as session:
|
||||
@ -33,19 +37,33 @@ class Repository(Base):
|
||||
|
||||
async def update(self, db: database.Database):
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
async def by_user_id(user_id: UUID, db: database.Database) -> Self | None:
|
||||
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):
|
||||
capacity: int
|
||||
used: int
|
||||
|
||||
|
||||
from materia_server.models.user import User
|
||||
from materia_server.models.directory import Directory
|
||||
from materia_server.models.file import File
|
||||
|
@ -17,30 +17,31 @@ from loguru import logger
|
||||
valid_username = re.compile(r"^[\da-zA-Z][-.\w]*$")
|
||||
invalid_username = re.compile(r"[-._]{2,}|[-._]$")
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "user"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key = True, default = uuid4)
|
||||
name: Mapped[str] = mapped_column(unique = True)
|
||||
lower_name: Mapped[str] = mapped_column(unique = True)
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
name: Mapped[str] = mapped_column(unique=True)
|
||||
lower_name: Mapped[str] = mapped_column(unique=True)
|
||||
full_name: Mapped[Optional[str]]
|
||||
email: Mapped[str]
|
||||
is_email_private: Mapped[bool] = mapped_column(default = True)
|
||||
is_email_private: Mapped[bool] = mapped_column(default=True)
|
||||
hashed_password: Mapped[str]
|
||||
must_change_password: Mapped[bool] = mapped_column(default = False)
|
||||
must_change_password: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
login_type: Mapped["LoginType"]
|
||||
|
||||
created: Mapped[int] = mapped_column(BigInteger, default = time.time)
|
||||
updated: Mapped[int] = mapped_column(BigInteger, default = time.time)
|
||||
last_login: Mapped[int] = mapped_column(BigInteger, nullable = True)
|
||||
created: Mapped[int] = mapped_column(BigInteger, default=time.time)
|
||||
updated: Mapped[int] = mapped_column(BigInteger, default=time.time)
|
||||
last_login: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
is_active: Mapped[bool] = mapped_column(default = False)
|
||||
is_admin: Mapped[bool] = mapped_column(default = False)
|
||||
is_active: Mapped[bool] = mapped_column(default=False)
|
||||
is_admin: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
avatar: Mapped[Optional[str]]
|
||||
|
||||
repository: Mapped["Repository"] = relationship(back_populates = "user")
|
||||
repository: Mapped["Repository"] = relationship(back_populates="user")
|
||||
|
||||
def update_last_login(self):
|
||||
self.last_login = int(time.time())
|
||||
@ -63,31 +64,42 @@ class User(Base):
|
||||
@staticmethod
|
||||
async def by_name(name: str, db: database.Database):
|
||||
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
|
||||
async def by_email(email: str, db: database.Database):
|
||||
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
|
||||
async def by_id(id: UUID, db: database.Database):
|
||||
async with db.session() as session:
|
||||
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):
|
||||
name: str
|
||||
password: str
|
||||
email: Optional[EmailStr]
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes = True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
lower_name: str
|
||||
full_name: Optional[str]
|
||||
email: str
|
||||
email: Optional[str]
|
||||
is_email_private: bool
|
||||
must_change_password: bool
|
||||
|
||||
@ -102,4 +114,5 @@ class UserInfo(BaseModel):
|
||||
|
||||
avatar: Optional[str]
|
||||
|
||||
|
||||
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 import user, repository, directory, file
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/api")
|
||||
router.include_router(auth.router)
|
||||
router.include_router(oauth.router)
|
||||
router.include_router(user.router)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
@ -8,32 +9,41 @@ from materia_server.routers import middleware
|
||||
from materia_server.config import Config
|
||||
|
||||
|
||||
router = APIRouter(tags = ["directory"])
|
||||
router = APIRouter(tags=["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
|
||||
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:
|
||||
session.add(user)
|
||||
await session.refresh(user, attribute_names = ["repository"])
|
||||
await session.refresh(user, attribute_names=["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_path = Path()
|
||||
directory = None
|
||||
|
||||
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(
|
||||
repository_id = user.repository.id,
|
||||
parent_id = current_directory.id if current_directory else None,
|
||||
name = part,
|
||||
path = None if current_path == Path() else str(current_path)
|
||||
repository_id=user.repository.id,
|
||||
parent_id=current_directory.id if current_directory else None,
|
||||
name=part,
|
||||
path=None if current_path == Path() else str(current_path),
|
||||
)
|
||||
session.add(directory)
|
||||
|
||||
@ -41,29 +51,81 @@ async def create(path: Path = Path(), user: User = Depends(middleware.user), ctx
|
||||
current_path /= part
|
||||
|
||||
try:
|
||||
(repository_path / directory_path).mkdir(parents = True, exist_ok = True)
|
||||
(repository_path / directory_path).mkdir(parents=True, exist_ok=True)
|
||||
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()
|
||||
|
||||
|
||||
@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:
|
||||
session.add(user)
|
||||
await session.refresh(user, attribute_names = ["repository"])
|
||||
await session.refresh(user, attribute_names=["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)):
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory is 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")
|
||||
|
||||
async with ctx.database.session() as session:
|
||||
session.add(directory)
|
||||
await session.refresh(directory, attribute_names = ["files"])
|
||||
await session.refresh(directory, attribute_names=["files"])
|
||||
|
||||
info = DirectoryInfo.model_validate(directory)
|
||||
info.used = sum([ file.size for file in directory.files ])
|
||||
info.used = sum([file.size for file in directory.files])
|
||||
|
||||
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)
|
||||
|
@ -8,85 +8,123 @@ from materia_server.routers import middleware
|
||||
from materia_server.config import Config
|
||||
|
||||
|
||||
router = APIRouter(tags = ["file"])
|
||||
router = APIRouter(tags=["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:
|
||||
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
|
||||
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:
|
||||
session.add(user)
|
||||
await session.refresh(user, attribute_names = ["repository"])
|
||||
await session.refresh(user, attribute_names=["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():
|
||||
directory = await Directory.by_path(
|
||||
user.repository.id,
|
||||
None if directory_path.parent == Path() else directory_path.parent,
|
||||
directory_path.name,
|
||||
ctx.database
|
||||
ctx.database,
|
||||
)
|
||||
|
||||
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:
|
||||
directory = None
|
||||
|
||||
file = File(
|
||||
repository_id = user.repository.id,
|
||||
parent_id = directory.id if directory else None,
|
||||
name = upload_file.filename,
|
||||
path = None if directory_path == Path() else str(directory_path),
|
||||
size = upload_file.size
|
||||
repository_id=user.repository.id,
|
||||
parent_id=directory.id if directory else None,
|
||||
name=upload_file.filename,
|
||||
path=None if directory_path == Path() else str(directory_path),
|
||||
size=upload_file.size,
|
||||
)
|
||||
|
||||
try:
|
||||
file_path = repository_path.joinpath(directory_path, upload_file.filename)
|
||||
|
||||
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())
|
||||
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:
|
||||
session.add(file)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@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:
|
||||
session.add(user)
|
||||
await session.refresh(user, attribute_names = ["repository"])
|
||||
await session.refresh(user, attribute_names=["repository"])
|
||||
|
||||
if not user.repository:
|
||||
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")
|
||||
|
||||
info = FileInfo.model_validate(file)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
@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:
|
||||
session.add(user)
|
||||
await session.refresh(user, attribute_names = ["repository"])
|
||||
await session.refresh(user, attribute_names=["repository"])
|
||||
|
||||
if not user.repository:
|
||||
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")
|
||||
|
||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
||||
@ -95,8 +133,10 @@ async def remove(path: Path, user: User = Depends(middleware.user), ctx: middlew
|
||||
file_path = repository_path.joinpath(path)
|
||||
|
||||
if file_path.exists():
|
||||
file_path.unlink(missing_ok = True)
|
||||
file_path.unlink(missing_ok=True)
|
||||
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)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import shutil
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from materia_server.models import User, Repository, RepositoryInfo
|
||||
@ -5,41 +6,67 @@ from materia_server.routers import middleware
|
||||
from materia_server.config import Config
|
||||
|
||||
|
||||
router = APIRouter(tags = ["repository"])
|
||||
router = APIRouter(tags=["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
|
||||
|
||||
if await Repository.by_user_id(user.id, ctx.database):
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Repository already exists")
|
||||
|
||||
repository = Repository(
|
||||
user_id = user.id,
|
||||
capacity = ctx.config.repository.capacity
|
||||
)
|
||||
repository = Repository(user_id=user.id, capacity=ctx.config.repository.capacity)
|
||||
|
||||
try:
|
||||
repository_path.mkdir(parents = True, exist_ok = True)
|
||||
repository_path.mkdir(parents=True, exist_ok=True)
|
||||
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)
|
||||
|
||||
|
||||
@router.get("/repository", response_model = RepositoryInfo)
|
||||
async def info(user: User = Depends(middleware.user), ctx: middleware.Context = Depends()):
|
||||
@router.get("/repository", response_model=RepositoryInfo)
|
||||
async def info(
|
||||
user: User = Depends(middleware.user), ctx: middleware.Context = Depends()
|
||||
):
|
||||
async with ctx.database.session() as session:
|
||||
session.add(user)
|
||||
await session.refresh(user, attribute_names = ["repository"])
|
||||
await session.refresh(user, attribute_names=["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")
|
||||
|
||||
await session.refresh(repository, attribute_names = ["files"])
|
||||
async with ctx.database.session() as session:
|
||||
session.add(repository)
|
||||
await session.refresh(repository, attribute_names=["files"])
|
||||
|
||||
return RepositoryInfo(
|
||||
capacity = repository.capacity,
|
||||
used = sum([ file.size for file in repository.files ])
|
||||
capacity=repository.capacity,
|
||||
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 io
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
||||
import sqlalchemy as sa
|
||||
@ -12,39 +12,78 @@ from materia_server.models import User, UserInfo
|
||||
from materia_server.routers import middleware
|
||||
|
||||
|
||||
router = APIRouter(tags = ["user"])
|
||||
router = APIRouter(tags=["user"])
|
||||
|
||||
@router.get("/user", response_model = UserInfo)
|
||||
async def info(claims = Depends(middleware.jwt_cookie), ctx: middleware.Context = Depends()):
|
||||
|
||||
@router.get("/user", response_model=UserInfo)
|
||||
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)):
|
||||
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")
|
||||
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:
|
||||
avatars: list[str] = (await session.scalars(sa.select(User.avatar))).all()
|
||||
avatars = list(filter(lambda avatar_hash: avatar_hash, avatars))
|
||||
|
||||
avatar_id = Sqids(min_length = 10, blocklist = avatars).encode([len(avatars)])
|
||||
avatar_id = Sqids(min_length=10, blocklist=avatars).encode([len(avatars)])
|
||||
|
||||
try:
|
||||
img = Image.open(io.BytesIO(await file.read()))
|
||||
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:
|
||||
if not (avatars_dir := Config.data_dir() / "avatars").exists():
|
||||
avatars_dir.mkdir()
|
||||
img.save(avatars_dir / avatar_id, format = img.format)
|
||||
img.save(avatars_dir / avatar_id, format=img.format)
|
||||
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_file := Config.data_dir() / "avatars" / old_avatar).exists():
|
||||
old_file.unlink()
|
||||
|
||||
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()
|
||||
|
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…
x
Reference in New Issue
Block a user