materia-server: fix directory no parent

This commit is contained in:
L-Nafaryus 2024-07-10 01:20:51 +05:00
parent ec41110e0b
commit b89e8f3393
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
9 changed files with 402 additions and 289 deletions

View File

@ -10,29 +10,47 @@
bonfire.url = "github:L-Nafaryus/bonfire"; bonfire.url = "github:L-Nafaryus/bonfire";
}; };
outputs = {
outputs = { self, nixpkgs, dream2nix, bonfire, ... }: self,
let nixpkgs,
dream2nix,
bonfire,
...
}: let
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs {inherit system;};
bonpkgs = bonfire.packages.${system}; bonpkgs = bonfire.packages.${system};
bonlib = bonfire.lib; bonlib = bonfire.lib;
dreamBuildPackage = { module, meta ? {}, extraModules ? [], extraArgs ? {} }: ( dreamBuildPackage = {
module,
meta ? {},
extraModules ? [],
extraArgs ? {},
}:
(
nixpkgs.lib.evalModules { nixpkgs.lib.evalModules {
modules = [ module ] ++ extraModules; modules = [module] ++ extraModules;
specialArgs = { specialArgs =
{
inherit dream2nix; inherit dream2nix;
packageSets.nixpkgs = pkgs; packageSets.nixpkgs = pkgs;
} // extraArgs;
} }
).config.public // { inherit meta; }; // extraArgs;
}
in )
{ .config
.public
// {inherit meta;};
in {
packages.x86_64-linux = { packages.x86_64-linux = {
materia-frontend = dreamBuildPackage { materia-frontend = dreamBuildPackage {
module = { lib, config, dream2nix, ... }: { module = {
lib,
config,
dream2nix,
...
}: {
name = "materia-frontend"; name = "materia-frontend";
version = "0.0.1"; version = "0.0.1";
@ -59,7 +77,7 @@
meta = with nixpkgs.lib; { meta = with nixpkgs.lib; {
description = "Materia frontend"; description = "Materia frontend";
license = licenses.mit; license = licenses.mit;
maintainers = with bonlib.maintainers; [ L-Nafaryus ]; maintainers = with bonlib.maintainers; [L-Nafaryus];
broken = false; broken = false;
}; };
}; };
@ -68,13 +86,19 @@
extraArgs = { extraArgs = {
inherit (self.packages.x86_64-linux) materia-frontend; inherit (self.packages.x86_64-linux) materia-frontend;
}; };
module = {config, lib, dream2nix, materia-frontend, ...}: { module = {
imports = [ dream2nix.modules.dream2nix.WIP-python-pdm ]; config,
lib,
dream2nix,
materia-frontend,
...
}: {
imports = [dream2nix.modules.dream2nix.WIP-python-pdm];
pdm.lockfile = ./materia-web-client/pdm.lock; pdm.lockfile = ./materia-web-client/pdm.lock;
pdm.pyproject = ./materia-web-client/pyproject.toml; pdm.pyproject = ./materia-web-client/pyproject.toml;
deps = _ : { deps = _: {
python = pkgs.python3; python = pkgs.python3;
}; };
@ -91,19 +115,25 @@
meta = with nixpkgs.lib; { meta = with nixpkgs.lib; {
description = "Materia web client"; description = "Materia web client";
license = licenses.mit; license = licenses.mit;
maintainers = with bonlib.maintainers; [ L-Nafaryus ]; maintainers = with bonlib.maintainers; [L-Nafaryus];
broken = false; broken = false;
}; };
}; };
materia-server = dreamBuildPackage { materia-server = dreamBuildPackage {
module = {config, lib, dream2nix, materia-frontend, ...}: { module = {
imports = [ dream2nix.modules.dream2nix.WIP-python-pdm ]; config,
lib,
dream2nix,
materia-frontend,
...
}: {
imports = [dream2nix.modules.dream2nix.WIP-python-pdm];
pdm.lockfile = ./materia-server/pdm.lock; pdm.lockfile = ./materia-server/pdm.lock;
pdm.pyproject = ./materia-server/pyproject.toml; pdm.pyproject = ./materia-server/pyproject.toml;
deps = _ : { deps = _: {
python = pkgs.python3; python = pkgs.python3;
}; };
@ -115,13 +145,12 @@
nativeBuildInputs = [ nativeBuildInputs = [
pkgs.python3.pkgs.wrapPython pkgs.python3.pkgs.wrapPython
]; ];
}; };
}; };
meta = with nixpkgs.lib; { meta = with nixpkgs.lib; {
description = "Materia"; description = "Materia";
license = licenses.mit; license = licenses.mit;
maintainers = with bonlib.maintainers; [ L-Nafaryus ]; maintainers = with bonlib.maintainers; [L-Nafaryus];
broken = false; broken = false;
mainProgram = "materia-server"; mainProgram = "materia-server";
}; };
@ -135,13 +164,14 @@
initdb -U ${user} initdb -U ${user}
postgres -k ${dataDir} postgres -k ${dataDir}
''; '';
in pkgs.dockerTools.buildImage { in
pkgs.dockerTools.buildImage {
name = "postgresql"; name = "postgresql";
tag = "devel"; tag = "devel";
copyToRoot = pkgs.buildEnv { copyToRoot = pkgs.buildEnv {
name = "image-root"; name = "image-root";
pathsToLink = [ "/bin" "/etc" "/" ]; pathsToLink = ["/bin" "/etc" "/"];
paths = with pkgs; [ paths = with pkgs; [
bash bash
postgresql postgresql
@ -158,10 +188,10 @@
''; '';
config = { config = {
Entrypoint = [ "bash" "/entrypoint.sh" ]; Entrypoint = ["bash" "/entrypoint.sh"];
StopSignal = "SIGINT"; StopSignal = "SIGINT";
User = "${user}:${user}"; User = "${user}:${user}";
Env = [ "PGDATA=${dataDir}" ]; Env = ["PGDATA=${dataDir}"];
WorkingDir = dataDir; WorkingDir = dataDir;
ExposedPorts = { ExposedPorts = {
"5432/tcp" = {}; "5432/tcp" = {};
@ -177,13 +207,14 @@
--daemonize no \ --daemonize no \
--dir "${dataDir}" --dir "${dataDir}"
''; '';
in pkgs.dockerTools.buildImage { in
pkgs.dockerTools.buildImage {
name = "redis"; name = "redis";
tag = "devel"; tag = "devel";
copyToRoot = pkgs.buildEnv { copyToRoot = pkgs.buildEnv {
name = "image-root"; name = "image-root";
pathsToLink = [ "/bin" "/etc" "/" ]; pathsToLink = ["/bin" "/etc" "/"];
paths = with pkgs; [ paths = with pkgs; [
bash bash
redis redis
@ -200,7 +231,7 @@
''; '';
config = { config = {
Entrypoint = [ "bash" "/entrypoint.sh" ]; Entrypoint = ["bash" "/entrypoint.sh"];
StopSignal = "SIGINT"; StopSignal = "SIGINT";
User = "${user}:${user}"; User = "${user}:${user}";
WorkingDir = dataDir; WorkingDir = dataDir;
@ -219,9 +250,9 @@
}; };
devShells.x86_64-linux.default = pkgs.mkShell { devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [ postgresql redis pdm nodejs ]; buildInputs = with pkgs; [postgresql redis pdm nodejs];
# greenlet requires libstdc++ # greenlet requires libstdc++
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ]; LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath [pkgs.stdenv.cc.cc];
}; };
}; };
} }

View File

@ -1,11 +1,26 @@
from materia_server.models.auth import (
LoginType,
LoginSource,
OAuth2Application,
OAuth2Grant,
OAuth2AuthorizationCode,
)
from materia_server.models.auth import LoginType, LoginSource, OAuth2Application, OAuth2Grant, OAuth2AuthorizationCode from materia_server.models.database import (
Database,
from materia_server.models.database import Database, DatabaseError, DatabaseMigrationError, Cache, CacheError DatabaseError,
DatabaseMigrationError,
Cache,
CacheError,
)
from materia_server.models.user import User, UserCredentials, UserInfo from materia_server.models.user import User, UserCredentials, UserInfo
from materia_server.models.repository import Repository, RepositoryInfo from materia_server.models.repository import (
Repository,
RepositoryInfo,
RepositoryContent,
)
from materia_server.models.directory import Directory, DirectoryLink, DirectoryInfo from materia_server.models.directory import Directory, DirectoryLink, DirectoryInfo

View File

@ -59,7 +59,7 @@ class Directory(Base):
async def remove(self, db: database.Database): async def remove(self, db: database.Database):
async with db.session() as session: async with db.session() as session:
await session.execute(sa.delete(Directory).where(Directory.id == self.id)) await session.delete(self)
await session.commit() await session.commit()

View File

@ -56,7 +56,7 @@ class File(Base):
async def remove(self, db: database.Database): async def remove(self, db: database.Database):
async with db.session() as session: async with db.session() as session:
await session.execute(sa.delete(File).where(File.id == self.id)) await session.delete(self)
await session.commit() await session.commit()

View File

@ -1,12 +1,12 @@
from time import time from time import time
from typing import List, Self from typing import List, Self, Optional
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from sqlalchemy import BigInteger, ForeignKey from sqlalchemy import BigInteger, ForeignKey
from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm import mapped_column, Mapped, relationship
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
import sqlalchemy as sa import sqlalchemy as sa
from pydantic import BaseModel 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
@ -55,15 +55,24 @@ class Repository(Base):
async def remove(self, db: database.Database): async def remove(self, db: database.Database):
async with db.session() as session: async with db.session() as session:
await session.execute(sa.delete(Repository).where(Repository.id == self.id)) await session.delete(self)
await session.commit() await session.commit()
class RepositoryInfo(BaseModel): class RepositoryInfo(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
capacity: int capacity: int
used: int used: Optional[int] = None
class RepositoryContent(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
files: list["FileInfo"]
directories: list["DirectoryInfo"]
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, DirectoryInfo
from materia_server.models.file import File from materia_server.models.file import File, FileInfo

View File

@ -82,7 +82,7 @@ class User(Base):
async def remove(self, db: database.Database): async def remove(self, db: database.Database):
async with db.session() as session: async with db.session() as session:
await session.execute(sa.delete(User).where(User.id == self.id)) await session.delete(self)
await session.commit() await session.commit()

View File

@ -45,20 +45,23 @@ async def create(
name=part, name=part,
path=None if current_path == Path() else str(current_path), path=None if current_path == Path() else str(current_path),
) )
try:
(repository_path / current_path / part).mkdir(exist_ok=True)
except OSError:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
f"Failed to create a directory {current_path / part}",
)
async with ctx.database.session() as session:
session.add(directory) session.add(directory)
await session.commit()
await session.refresh(directory)
current_directory = directory current_directory = directory
current_path /= part current_path /= part
try:
(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"
)
await session.commit()
@router.get("/directory") @router.get("/directory")
async def info( async def info(

View File

@ -1,7 +1,15 @@
import shutil import shutil
from pathlib import Path
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,
RepositoryContent,
FileInfo,
DirectoryInfo,
)
from materia_server.routers import middleware from materia_server.routers import middleware
from materia_server.config import Config from materia_server.config import Config
@ -32,35 +40,24 @@ async def create(
@router.get("/repository", response_model=RepositoryInfo) @router.get("/repository", response_model=RepositoryInfo)
async def info( async def info(
user: User = Depends(middleware.user), ctx: middleware.Context = Depends() repository=Depends(middleware.repository), ctx: middleware.Context = Depends()
): ):
async with ctx.database.session() as session:
session.add(user)
await session.refresh(user, attribute_names=["repository"])
if not (repository := user.repository):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
async with ctx.database.session() as session: async with ctx.database.session() as session:
session.add(repository) session.add(repository)
await session.refresh(repository, attribute_names=["files"]) await session.refresh(repository, attribute_names=["files"])
return RepositoryInfo( info = RepositoryInfo.model_validate(repository)
capacity=repository.capacity, info.used = sum([file.size for file in repository.files])
used=sum([file.size for file in repository.files]),
) return info
@router.delete("/repository") @router.delete("/repository")
async def remove( async def remove(
user: User = Depends(middleware.user), ctx: middleware.Context = Depends() repository=Depends(middleware.repository),
repository_path=Depends(middleware.repository_path),
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: try:
if repository_path.exists(): if repository_path.exists():
shutil.rmtree(str(repository_path)) shutil.rmtree(str(repository_path))
@ -69,4 +66,33 @@ async def remove(
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove repository" status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove repository"
) )
await user.repository.remove(ctx.database) await repository.remove(ctx.database)
@router.get("/repository/content", response_model=RepositoryContent)
async def content(
repository=Depends(middleware.repository), ctx: middleware.Context = Depends()
):
async with ctx.database.session() as session:
session.add(repository)
await session.refresh(repository, attribute_names=["directories"])
await session.refresh(repository, attribute_names=["files"])
content = RepositoryContent(
files=list(
map(
lambda file: FileInfo.model_validate(file),
filter(lambda file: file.path is None, repository.files),
)
),
directories=list(
map(
lambda directory: DirectoryInfo.model_validate(directory),
filter(
lambda directory: directory.path is None, repository.directories
),
)
),
)
return content

View File

@ -1,6 +1,7 @@
from typing import Optional, Sequence from typing import Optional, Sequence
import uuid import uuid
from datetime import datetime from datetime import datetime
from pathlib import Path
from fastapi import HTTPException, Request, Response, status, Depends, Cookie from fastapi import HTTPException, Request, Response, status, Depends, Cookie
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
import jwt import jwt
@ -8,10 +9,17 @@ from sqlalchemy import select
from pydantic import BaseModel from pydantic import BaseModel
from enum import StrEnum from enum import StrEnum
from http import HTTPMethod as HttpMethod from http import HTTPMethod as HttpMethod
from fastapi.security import HTTPBearer, OAuth2PasswordBearer, OAuth2PasswordRequestForm, APIKeyQuery, APIKeyCookie, APIKeyHeader from fastapi.security import (
HTTPBearer,
OAuth2PasswordBearer,
OAuth2PasswordRequestForm,
APIKeyQuery,
APIKeyCookie,
APIKeyHeader,
)
from materia_server import security from materia_server import security
from materia_server.models import User from materia_server.models import User, Repository
class Context: class Context:
@ -23,7 +31,11 @@ class Context:
async def jwt_cookie(request: Request, response: Response, ctx: Context = Depends()): async def jwt_cookie(request: Request, response: Response, ctx: Context = Depends()):
if not (access_token := request.cookies.get(ctx.config.security.cookie_access_token_name)): if not (
access_token := request.cookies.get(
ctx.config.security.cookie_access_token_name
)
):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing token") raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing token")
refresh_token = request.cookies.get(ctx.config.security.cookie_refresh_token_name) refresh_token = request.cookies.get(ctx.config.security.cookie_refresh_token_name)
@ -35,7 +47,9 @@ async def jwt_cookie(request: Request, response: Response, ctx: Context = Depend
issuer = "{}://{}".format(ctx.config.server.scheme, ctx.config.server.domain) issuer = "{}://{}".format(ctx.config.server.scheme, ctx.config.server.domain)
try: try:
refresh_claims = security.validate_token(refresh_token, secret) if refresh_token else None refresh_claims = (
security.validate_token(refresh_token, secret) if refresh_token else None
)
if refresh_claims: if refresh_claims:
if refresh_claims.exp < datetime.now().timestamp(): if refresh_claims.exp < datetime.now().timestamp():
@ -52,16 +66,16 @@ async def jwt_cookie(request: Request, response: Response, ctx: Context = Depend
access_claims.sub, access_claims.sub,
str(secret), str(secret),
ctx.config.oauth2.access_token_lifetime, ctx.config.oauth2.access_token_lifetime,
issuer issuer,
) )
access_claims = security.validate_token(new_access_token, secret) access_claims = security.validate_token(new_access_token, secret)
response.set_cookie( response.set_cookie(
ctx.config.security.cookie_access_token_name, ctx.config.security.cookie_access_token_name,
value = new_access_token, value=new_access_token,
max_age = ctx.config.oauth2.access_token_lifetime, max_age=ctx.config.oauth2.access_token_lifetime,
secure = True, secure=True,
httponly = ctx.config.security.cookie_http_only, httponly=ctx.config.security.cookie_http_only,
samesite = "lax" samesite="lax",
) )
else: else:
access_claims = None access_claims = None
@ -74,8 +88,23 @@ async def jwt_cookie(request: Request, response: Response, ctx: Context = Depend
return access_claims return access_claims
async def user(claims = Depends(jwt_cookie), ctx: Context = Depends()): async def user(claims=Depends(jwt_cookie), ctx: Context = Depends()) -> User:
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 current_user return current_user
async def repository(user: User = Depends(user), ctx: Context = Depends()):
async with ctx.database.session() as session:
session.add(user)
await session.refresh(user, attribute_names=["repository"])
if not (repository := user.repository):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
return repository
async def repository_path(user: User = Depends(user), ctx: Context = Depends()) -> Path:
return ctx.config.data_dir() / "repository" / user.lower_name