Compare commits

..

2 Commits

Author SHA1 Message Date
577f6f3ddf
materia-web-client: repository view 2024-07-10 01:21:21 +05:00
b89e8f3393
materia-server: fix directory no parent 2024-07-10 01:20:51 +05:00
18 changed files with 587 additions and 290 deletions

449
flake.nix
View File

@ -1,227 +1,258 @@
{
description = "Materia";
description = "Materia";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
dream2nix = {
url = "github:nix-community/dream2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
bonfire.url = "github:L-Nafaryus/bonfire";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
dream2nix = {
url = "github:nix-community/dream2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
bonfire.url = "github:L-Nafaryus/bonfire";
};
outputs = {
self,
nixpkgs,
dream2nix,
bonfire,
...
}: let
system = "x86_64-linux";
pkgs = import nixpkgs {inherit system;};
bonpkgs = bonfire.packages.${system};
bonlib = bonfire.lib;
outputs = { self, nixpkgs, dream2nix, bonfire, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
bonpkgs = bonfire.packages.${system};
bonlib = bonfire.lib;
dreamBuildPackage = { module, meta ? {}, extraModules ? [], extraArgs ? {} }: (
nixpkgs.lib.evalModules {
modules = [ module ] ++ extraModules;
specialArgs = {
inherit dream2nix;
packageSets.nixpkgs = pkgs;
} // extraArgs;
dreamBuildPackage = {
module,
meta ? {},
extraModules ? [],
extraArgs ? {},
}:
(
nixpkgs.lib.evalModules {
modules = [module] ++ extraModules;
specialArgs =
{
inherit dream2nix;
packageSets.nixpkgs = pkgs;
}
).config.public // { inherit meta; };
// extraArgs;
}
)
.config
.public
// {inherit meta;};
in {
packages.x86_64-linux = {
materia-frontend = dreamBuildPackage {
module = {
lib,
config,
dream2nix,
...
}: {
name = "materia-frontend";
version = "0.0.1";
in
{
packages.x86_64-linux = {
materia-frontend = dreamBuildPackage {
module = { lib, config, dream2nix, ... }: {
name = "materia-frontend";
version = "0.0.1";
imports = [
dream2nix.modules.dream2nix.WIP-nodejs-builder-v3
];
imports = [
dream2nix.modules.dream2nix.WIP-nodejs-builder-v3
];
mkDerivation = {
src = ./materia-web-client/src/materia-frontend;
};
mkDerivation = {
src = ./materia-web-client/src/materia-frontend;
};
deps = {nixpkgs, ...}: {
inherit
(nixpkgs)
fetchFromGitHub
stdenv
;
};
deps = {nixpkgs, ...}: {
inherit
(nixpkgs)
fetchFromGitHub
stdenv
;
};
WIP-nodejs-builder-v3 = {
packageLockFile = "${config.mkDerivation.src}/package-lock.json";
};
};
meta = with nixpkgs.lib; {
description = "Materia frontend";
license = licenses.mit;
maintainers = with bonlib.maintainers; [L-Nafaryus];
broken = false;
};
};
WIP-nodejs-builder-v3 = {
packageLockFile = "${config.mkDerivation.src}/package-lock.json";
};
};
meta = with nixpkgs.lib; {
description = "Materia frontend";
license = licenses.mit;
maintainers = with bonlib.maintainers; [ L-Nafaryus ];
broken = false;
};
};
materia-web-client = dreamBuildPackage {
extraArgs = {
inherit (self.packages.x86_64-linux) materia-frontend;
};
module = {config, lib, dream2nix, materia-frontend, ...}: {
imports = [ dream2nix.modules.dream2nix.WIP-python-pdm ];
pdm.lockfile = ./materia-web-client/pdm.lock;
pdm.pyproject = ./materia-web-client/pyproject.toml;
deps = _ : {
python = pkgs.python3;
};
mkDerivation = {
src = ./materia-web-client;
buildInputs = [
pkgs.python3.pkgs.pdm-backend
];
configurePhase = ''
cp -rv ${materia-frontend}/dist ./src/materia-frontend/
'';
};
};
meta = with nixpkgs.lib; {
description = "Materia web client";
license = licenses.mit;
maintainers = with bonlib.maintainers; [ L-Nafaryus ];
broken = false;
};
};
materia-server = dreamBuildPackage {
module = {config, lib, dream2nix, materia-frontend, ...}: {
imports = [ dream2nix.modules.dream2nix.WIP-python-pdm ];
pdm.lockfile = ./materia-server/pdm.lock;
pdm.pyproject = ./materia-server/pyproject.toml;
deps = _ : {
python = pkgs.python3;
};
mkDerivation = {
src = ./materia-server;
buildInputs = [
pkgs.python3.pkgs.pdm-backend
];
nativeBuildInputs = [
pkgs.python3.pkgs.wrapPython
];
};
};
meta = with nixpkgs.lib; {
description = "Materia";
license = licenses.mit;
maintainers = with bonlib.maintainers; [ L-Nafaryus ];
broken = false;
mainProgram = "materia-server";
};
};
postgresql = let
user = "postgres";
database = "postgres";
dataDir = "/var/lib/postgresql";
entryPoint = pkgs.writeTextDir "entrypoint.sh" ''
initdb -U ${user}
postgres -k ${dataDir}
'';
in pkgs.dockerTools.buildImage {
name = "postgresql";
tag = "devel";
copyToRoot = pkgs.buildEnv {
name = "image-root";
pathsToLink = [ "/bin" "/etc" "/" ];
paths = with pkgs; [
bash
postgresql
entryPoint
];
};
runAsRoot = with pkgs; ''
#!${runtimeShell}
${dockerTools.shadowSetup}
groupadd -r ${user}
useradd -r -g ${user} --home-dir=${dataDir} ${user}
mkdir -p ${dataDir}
chown -R ${user}:${user} ${dataDir}
'';
config = {
Entrypoint = [ "bash" "/entrypoint.sh" ];
StopSignal = "SIGINT";
User = "${user}:${user}";
Env = [ "PGDATA=${dataDir}" ];
WorkingDir = dataDir;
ExposedPorts = {
"5432/tcp" = {};
};
};
};
redis = let
user = "redis";
dataDir = "/var/lib/redis";
entryPoint = pkgs.writeTextDir "entrypoint.sh" ''
redis-server \
--daemonize no \
--dir "${dataDir}"
'';
in pkgs.dockerTools.buildImage {
name = "redis";
tag = "devel";
copyToRoot = pkgs.buildEnv {
name = "image-root";
pathsToLink = [ "/bin" "/etc" "/" ];
paths = with pkgs; [
bash
redis
entryPoint
];
};
runAsRoot = with pkgs; ''
#!${runtimeShell}
${dockerTools.shadowSetup}
groupadd -r ${user}
useradd -r -g ${user} --home-dir=${dataDir} ${user}
mkdir -p ${dataDir}
chown -R ${user}:${user} ${dataDir}
'';
config = {
Entrypoint = [ "bash" "/entrypoint.sh" ];
StopSignal = "SIGINT";
User = "${user}:${user}";
WorkingDir = dataDir;
ExposedPorts = {
"6379/tcp" = {};
};
};
materia-web-client = dreamBuildPackage {
extraArgs = {
inherit (self.packages.x86_64-linux) materia-frontend;
};
module = {
config,
lib,
dream2nix,
materia-frontend,
...
}: {
imports = [dream2nix.modules.dream2nix.WIP-python-pdm];
pdm.lockfile = ./materia-web-client/pdm.lock;
pdm.pyproject = ./materia-web-client/pyproject.toml;
deps = _: {
python = pkgs.python3;
};
mkDerivation = {
src = ./materia-web-client;
buildInputs = [
pkgs.python3.pkgs.pdm-backend
];
configurePhase = ''
cp -rv ${materia-frontend}/dist ./src/materia-frontend/
'';
};
};
meta = with nixpkgs.lib; {
description = "Materia web client";
license = licenses.mit;
maintainers = with bonlib.maintainers; [L-Nafaryus];
broken = false;
};
};
materia-server = dreamBuildPackage {
module = {
config,
lib,
dream2nix,
materia-frontend,
...
}: {
imports = [dream2nix.modules.dream2nix.WIP-python-pdm];
pdm.lockfile = ./materia-server/pdm.lock;
pdm.pyproject = ./materia-server/pyproject.toml;
deps = _: {
python = pkgs.python3;
};
mkDerivation = {
src = ./materia-server;
buildInputs = [
pkgs.python3.pkgs.pdm-backend
];
nativeBuildInputs = [
pkgs.python3.pkgs.wrapPython
];
};
};
meta = with nixpkgs.lib; {
description = "Materia";
license = licenses.mit;
maintainers = with bonlib.maintainers; [L-Nafaryus];
broken = false;
mainProgram = "materia-server";
};
};
postgresql = let
user = "postgres";
database = "postgres";
dataDir = "/var/lib/postgresql";
entryPoint = pkgs.writeTextDir "entrypoint.sh" ''
initdb -U ${user}
postgres -k ${dataDir}
'';
in
pkgs.dockerTools.buildImage {
name = "postgresql";
tag = "devel";
copyToRoot = pkgs.buildEnv {
name = "image-root";
pathsToLink = ["/bin" "/etc" "/"];
paths = with pkgs; [
bash
postgresql
entryPoint
];
};
runAsRoot = with pkgs; ''
#!${runtimeShell}
${dockerTools.shadowSetup}
groupadd -r ${user}
useradd -r -g ${user} --home-dir=${dataDir} ${user}
mkdir -p ${dataDir}
chown -R ${user}:${user} ${dataDir}
'';
config = {
Entrypoint = ["bash" "/entrypoint.sh"];
StopSignal = "SIGINT";
User = "${user}:${user}";
Env = ["PGDATA=${dataDir}"];
WorkingDir = dataDir;
ExposedPorts = {
"5432/tcp" = {};
};
};
};
apps.x86_64-linux = {
materia-server = {
type = "app";
program = "${self.packages.x86_64-linux.materia-server}/bin/materia-server";
};
};
redis = let
user = "redis";
dataDir = "/var/lib/redis";
entryPoint = pkgs.writeTextDir "entrypoint.sh" ''
redis-server \
--daemonize no \
--dir "${dataDir}"
'';
in
pkgs.dockerTools.buildImage {
name = "redis";
tag = "devel";
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [ postgresql redis pdm nodejs ];
# greenlet requires libstdc++
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
copyToRoot = pkgs.buildEnv {
name = "image-root";
pathsToLink = ["/bin" "/etc" "/"];
paths = with pkgs; [
bash
redis
entryPoint
];
};
runAsRoot = with pkgs; ''
#!${runtimeShell}
${dockerTools.shadowSetup}
groupadd -r ${user}
useradd -r -g ${user} --home-dir=${dataDir} ${user}
mkdir -p ${dataDir}
chown -R ${user}:${user} ${dataDir}
'';
config = {
Entrypoint = ["bash" "/entrypoint.sh"];
StopSignal = "SIGINT";
User = "${user}:${user}";
WorkingDir = dataDir;
ExposedPorts = {
"6379/tcp" = {};
};
};
};
};
apps.x86_64-linux = {
materia-server = {
type = "app";
program = "${self.packages.x86_64-linux.materia-server}/bin/materia-server";
};
};
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [postgresql redis pdm nodejs];
# greenlet requires libstdc++
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, DatabaseError, DatabaseMigrationError, Cache, CacheError
from materia_server.models.database import (
Database,
DatabaseError,
DatabaseMigrationError,
Cache,
CacheError,
)
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

View File

@ -59,7 +59,7 @@ class Directory(Base):
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.delete(self)
await session.commit()

View File

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

View File

@ -1,12 +1,12 @@
from time import time
from typing import List, Self
from typing import List, Self, Optional
from uuid import UUID, uuid4
from sqlalchemy import BigInteger, ForeignKey
from sqlalchemy.orm import mapped_column, Mapped, relationship
from sqlalchemy.orm.attributes import InstrumentedAttribute
import sqlalchemy as sa
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from materia_server.models.base import Base
from materia_server.models import database
@ -55,15 +55,24 @@ class Repository(Base):
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.delete(self)
await session.commit()
class RepositoryInfo(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: 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.directory import Directory
from materia_server.models.file import File
from materia_server.models.directory import Directory, DirectoryInfo
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 with db.session() as session:
await session.execute(sa.delete(User).where(User.id == self.id))
await session.delete(self)
await session.commit()

View File

@ -28,36 +28,39 @@ async def create(
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 user.repository:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
current_directory = None
current_path = Path()
directory = None
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
):
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),
)
session.add(directory)
current_directory = directory
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"
for part in directory_path.parts:
if not await Directory.by_path(
user.repository.id, current_path, part, ctx.database
):
directory = Directory(
repository_id=user.repository.id,
parent_id=current_directory.id if current_directory else None,
name=part,
path=None if current_path == Path() else str(current_path),
)
await session.commit()
try:
(repository_path / current_path / part).mkdir(exist_ok=True)
except OSError:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
f"Failed to create a directory {current_path / part}",
)
async with ctx.database.session() as session:
session.add(directory)
await session.commit()
await session.refresh(directory)
current_directory = directory
current_path /= part
@router.get("/directory")

View File

@ -1,7 +1,15 @@
import shutil
from pathlib import Path
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.config import Config
@ -32,35 +40,24 @@ async def create(
@router.get("/repository", response_model=RepositoryInfo)
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:
session.add(repository)
await session.refresh(repository, attribute_names=["files"])
return RepositoryInfo(
capacity=repository.capacity,
used=sum([file.size for file in repository.files]),
)
info = RepositoryInfo.model_validate(repository)
info.used = sum([file.size for file in repository.files])
return info
@router.delete("/repository")
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:
if repository_path.exists():
shutil.rmtree(str(repository_path))
@ -69,4 +66,33 @@ async def remove(
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,67 +1,81 @@
from typing import Optional, Sequence
import uuid
from datetime import datetime
from pathlib import Path
from fastapi import HTTPException, Request, Response, status, Depends, Cookie
from fastapi.security.base import SecurityBase
import jwt
from sqlalchemy import select
from sqlalchemy import select
from pydantic import BaseModel
from enum import StrEnum
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.models import User
from materia_server.models import User, Repository
class Context:
def __init__(self, request: Request):
self.config = request.state.config
self.database = request.state.database
self.cache = request.state.cache
self.database = request.state.database
self.cache = request.state.cache
self.logger = request.state.logger
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")
refresh_token = request.cookies.get(ctx.config.security.cookie_refresh_token_name)
if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"]:
secret = ctx.config.oauth2.jwt_secret
secret = ctx.config.oauth2.jwt_secret
else:
secret = ctx.config.oauth2.jwt_signing_key
issuer = "{}://{}".format(ctx.config.server.scheme, ctx.config.server.domain)
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.exp < datetime.now().timestamp():
refresh_claims = None
except jwt.PyJWTError:
refresh_claims = None
refresh_claims = None
try:
access_claims = security.validate_token(access_token, secret)
if access_claims.exp < datetime.now().timestamp():
if refresh_claims:
new_access_token = security.generate_token(
access_claims.sub,
access_claims.sub,
str(secret),
ctx.config.oauth2.access_token_lifetime,
issuer
issuer,
)
access_claims = security.validate_token(new_access_token, secret)
response.set_cookie(
ctx.config.security.cookie_access_token_name,
value = new_access_token,
max_age = ctx.config.oauth2.access_token_lifetime,
secure = True,
httponly = ctx.config.security.cookie_http_only,
samesite = "lax"
ctx.config.security.cookie_access_token_name,
value=new_access_token,
max_age=ctx.config.oauth2.access_token_lifetime,
secure=True,
httponly=ctx.config.security.cookie_http_only,
samesite="lax",
)
else:
access_claims = None
@ -74,8 +88,23 @@ async def jwt_cookie(request: Request, response: Response, ctx: Context = Depend
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)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing 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

View File

@ -0,0 +1,13 @@
import { api_client, type ResponseError, handle_error } from "@/client";
export interface DirectoryInfo {
id: number,
repository_id: number,
parent_id?: number,
created: number,
updated: number,
name: string,
path?: string,
is_public: boolean,
used?: number
}

View File

@ -0,0 +1,13 @@
import { api_client, type ResponseError, handle_error } from "@/client";
export interface FileInfo {
id: number,
repository_id: number,
parent_id?: number,
created: number,
updated: number,
name: string,
path?: string,
is_public: boolean,
size: number
}

View File

@ -1,3 +1,5 @@
export * as auth from "@/api/auth";
export * as user from "@/api/user";
export * as repository from "@/api/repository";
export * as directory from "@/api/directory";
export * as file from "@/api/file";

View File

@ -1,4 +1,5 @@
import { api_client, type ResponseError, handle_error } from "@/client";
import { file, directory } from "@/api"
export interface RepositoryInfo {
id: number,
@ -6,6 +7,11 @@ export interface RepositoryInfo {
used?: number
}
export interface RepositoryContent {
files: file.FileInfo[],
directories: directory.DirectoryInfo[]
}
export async function info(): Promise<RepositoryInfo | ResponseError> {
return await api_client.get("/repository")
.then(async response => { return Promise.resolve<RepositoryInfo>(response.data); })
@ -21,3 +27,9 @@ export async function remove(): Promise<null | ResponseError> {
return await api_client.delete("/repository")
.catch(handle_error);
}
export async function content(): Promise<RepositoryContent | ResponseError> {
return await api_client.get("/repository/content")
.then(async response => { return Promise.resolve<RepositoryContent>(response.data); })
.catch(handle_error);
}

View File

@ -0,0 +1,37 @@
<template>
<div class="fixed h-1/3 z-50 context-menu" :style="{ top: y + 'px', left: x + 'px' }">
<div v-for="action in actions" :key="action.action" @click="emitAction(action.action)">
{{ action.label }}
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
const { actions, x, y } = defineProps(['actions', 'x', 'y']);
const emit = defineEmits(['action-clicked']);
const emitAction = (action) => {
emit('action-clicked', action);
};
</script>
<style scoped>
.context-menu {
position: absolute;
background: white;
border: 1px solid #ccc;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
min-width: 150px;
}
.context-menu div {
padding: 10px;
cursor: pointer;
}
.context-menu div:hover {
background-color: #f0f0f0;
}
</style>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
</script>
<template>
<div class="fixed z-50 cursor-pointer" :style="{ top: pos_y + 'px', left: pos_x + 'px' }">
</div>
</template>

View File

@ -0,0 +1,9 @@
<template>
<svg aria-hidden="true" focusable="false" role="img" class="icon-directory" viewBox="0 0 16 16" width="16"
height="16" fill="currentColor"
style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;">
<path
d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z">
</path>
</svg>
</template>

View File

@ -0,0 +1,9 @@
<template>
<svg aria-hidden="true" focusable="false" role="img" class="icon-directory" viewBox="0 0 16 16" width="16"
height="16" fill="currentColor"
style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;">
<path
d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z">
</path>
</svg>
</template>

View File

@ -1,6 +1,9 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/Error.vue";
import IconDirectory from "@/components/icons/IconDirectory.vue";
import IconFile from "@/components/icons/IconFile.vue";
import ContextMenu from "@/components/ContextMenu.vue";
import { ref, onMounted, watch } from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router"
@ -15,6 +18,7 @@ const error = ref<string>(null);
const repository_info = ref(null);
const is_created = ref(null);
const repository_content = ref(null);
onMounted(async () => {
await repository.info()
@ -25,6 +29,16 @@ onMounted(async () => {
.catch(err => {
is_created.value = false;
});
if (is_created.value) {
await repository.content()
.then(async _repository_content => {
repository_content.value = _repository_content;
})
.catch(err => {
error.value = err;
})
}
});
async function create_repository() {
@ -57,6 +71,42 @@ function size_procent() {
return Math.round(repository_info.value.used / repository_info.value.capacity) * 100;
}
function format_creation_time(timestamp: number): string {
const date = new Date(timestamp * 1000);
return `${date.getDate()}-${date.getMonth()}-${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}`;
}
const showMenu = ref(false);
const ctxMenuPosX = ref(0);
const ctxMenuPosY = ref(0);
const targetRow = ref({});
const contextMenuActions = ref([
{ label: 'Edit', action: 'edit' },
{ label: 'Delete', action: 'delete' },
]);
const showContextMenu = (event, user) => {
event.preventDefault();
showMenu.value = true;
targetRow.value = user;
ctxMenuPosX.value = event.clientX;
ctxMenuPosY.value = event.clientY;
};
const closeContextMenu = () => {
showMenu.value = false;
};
function handleActionClick(action) {
console.log(action);
console.log(targetRow.value);
}
function close_menu() {
showMenu.value = false;
}
</script>
<template>
@ -67,9 +117,39 @@ function size_procent() {
<div class="w-full rounded-full h-2.5 bg-ctp-surface0">
<div class="bg-ctp-lavender h-2.5 rounded-full" :style="{ width: size_procent() + '%' }"></div>
</div>
<span class="min-w-32 text-center">{{ round_size(repository_info.used, "GB") }} / {{
<span class="min-w-48 text-center">{{ round_size(repository_info.used, "MB").toFixed(2) }} MB / {{
round_size(repository_info.capacity, "GB") }} GB</span>
</div>
<table v-if="repository_content" class="table-auto w-full mt-8 mb-8 pl-8 pr-8 text-ctp-text">
<tbody>
<tr class="hover:bg-ctp-surface0" v-for="directory in repository_content.directories"
@contextmenu.prevent="showContextMenu($event, directory)">
<td>
<IconDirectory />
<div class="inline ml-4">{{ directory.name }}</div>
</td>
<td class="text-right">-</td>
<td class="text-right w-48">
{{ format_creation_time(directory.created) }}
</td>
</tr>
<tr class="hover:bg-ctp-surface0" v-for="file in repository_content.files">
<td>
<IconFile />
<div class="inline ml-4">{{ file.name }}</div>
</td>
<td class="text-right">{{ round_size(file.size, "MB").toFixed(2) }} MB</td>
<td class="text-right w-48">
{{ format_creation_time(file.created) }}
</td>
</tr>
</tbody>
</table>
<ContextMenu v-if="showMenu" :actions="contextMenuActions" @action-clicked="handleActionClick" :x="ctxMenuPosX"
:y="ctxMenuPosY" v-click-outside="close_menu" />
</section>
<section v-else>
<p>It looks like you don't have a repository yet...</p>