This commit is contained in:
retoor 2025-05-09 05:38:29 +02:00
parent c56bf4fb49
commit ee40c905d4
15 changed files with 427 additions and 3 deletions

View File

@ -1,11 +1,14 @@
import argparse import argparse
import uvloop
from aiohttp import web from aiohttp import web
import asyncio
from snek.app import Application from snek.app import Application
def main(): def main():
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
parser = argparse.ArgumentParser(description="Run the web application.") parser = argparse.ArgumentParser(description="Run the web application.")
parser.add_argument( parser.add_argument(
"--port", "--port",

View File

@ -39,6 +39,10 @@ from snek.view.logout import LogoutView
from snek.view.register import RegisterView from snek.view.register import RegisterView
from snek.view.rpc import RPCView from snek.view.rpc import RPCView
from snek.view.search_user import SearchUserView from snek.view.search_user import SearchUserView
from snek.view.settings.repositories import RepositoriesIndexView
from snek.view.settings.repositories import RepositoriesCreateView
from snek.view.settings.repositories import RepositoriesUpdateView
from snek.view.settings.repositories import RepositoriesDeleteView
from snek.view.settings.index import SettingsIndexView from snek.view.settings.index import SettingsIndexView
from snek.view.settings.profile import SettingsProfileView from snek.view.settings.profile import SettingsProfileView
from snek.view.stats import StatsView from snek.view.stats import StatsView
@ -175,6 +179,10 @@ class Application(BaseApplication):
self.router.add_view("/drive/{drive}.json", DriveView) self.router.add_view("/drive/{drive}.json", DriveView)
self.router.add_view("/stats.json", StatsView) self.router.add_view("/stats.json", StatsView)
self.router.add_view("/user/{user}.html", UserView) self.router.add_view("/user/{user}.html", UserView)
self.router.add_view("/settings/repositories/index.html", RepositoriesIndexView)
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
self.router.add_view("/settings/repositories/respository/{name}/delete.html", RepositoriesDeleteView)
self.webdav = WebdavApplication(self) self.webdav = WebdavApplication(self)
self.add_subapp("/webdav", self.webdav) self.add_subapp("/webdav", self.webdav)

View File

@ -8,6 +8,7 @@ from snek.mapper.drive_item import DriveItemMapper
from snek.mapper.notification import NotificationMapper from snek.mapper.notification import NotificationMapper
from snek.mapper.user import UserMapper from snek.mapper.user import UserMapper
from snek.mapper.user_property import UserPropertyMapper from snek.mapper.user_property import UserPropertyMapper
from snek.mapper.repository import RepositoryMapper
from snek.system.object import Object from snek.system.object import Object
@ -23,6 +24,7 @@ def get_mappers(app=None):
"drive_item": DriveItemMapper(app=app), "drive_item": DriveItemMapper(app=app),
"drive": DriveMapper(app=app), "drive": DriveMapper(app=app),
"user_property": UserPropertyMapper(app=app), "user_property": UserPropertyMapper(app=app),
"repository": RepositoryMapper(app=app),
} }
) )

View File

@ -0,0 +1,7 @@
from snek.model.repository import RepositoryModel
from snek.system.mapper import BaseMapper
class RepositoryMapper(BaseMapper):
model_class = RepositoryModel
table_name = "repository"

View File

@ -10,6 +10,7 @@ from snek.model.drive_item import DriveItemModel
from snek.model.notification import NotificationModel from snek.model.notification import NotificationModel
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.model.user_property import UserPropertyModel from snek.model.user_property import UserPropertyModel
from snek.model.repository import RepositoryModel
from snek.system.object import Object from snek.system.object import Object
@ -25,6 +26,7 @@ def get_models():
"drive": DriveModel, "drive": DriveModel,
"notification": NotificationModel, "notification": NotificationModel,
"user_property": UserPropertyModel, "user_property": UserPropertyModel,
"repository": RepositoryModel,
} }
) )

View File

@ -0,0 +1,14 @@
from snek.model.user import UserModel
from snek.system.model import BaseModel, ModelField
class RepositoryModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
is_private = ModelField(name="is_private", required=False, kind=bool)

View File

@ -11,6 +11,7 @@ from snek.service.socket import SocketService
from snek.service.user import UserService from snek.service.user import UserService
from snek.service.user_property import UserPropertyService from snek.service.user_property import UserPropertyService
from snek.service.util import UtilService from snek.service.util import UtilService
from snek.service.repository import RepositoryService
from snek.system.object import Object from snek.system.object import Object
@ -29,6 +30,7 @@ def get_services(app):
"drive": DriveService(app=app), "drive": DriveService(app=app),
"drive_item": DriveItemService(app=app), "drive_item": DriveItemService(app=app),
"user_property": UserPropertyService(app=app), "user_property": UserPropertyService(app=app),
"repository": RepositoryService(app=app),
} }
) )

View File

@ -0,0 +1,39 @@
from snek.system.service import BaseService
import asyncio
class RepositoryService(BaseService):
mapper_name = "repository"
async def exists(self, user_uid, name, **kwargs):
kwargs["user_uid"] = user_uid
kwargs["name"] = name
return await self.exists(**kwargs)
async def init(self, user_uid, name):
repository_path = await self.services.user.get_repository_path(user_uid)
if not repository_path.exists():
repository_path.mkdir(parents=True)
repository_path = repository_path.joinpath(name)
command = ['git', 'init', '--bare', repository_path]
process = await asyncio.subprocess.create_subprocess_exec(
*command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
print(f"Bare Git repository created at: {repo_path}")
else:
print(f"Error creating repository: {stderr.decode().strip()}")
async def create(self, user_uid, name,is_private=False):
if await self.exists(user_uid=user_uid, name=name):
return False
model = await self.new()
model["user_uid"] = user_uid
model["name"] = name
model["is_private"] = is_private
return await self.save(model)

View File

@ -42,6 +42,12 @@ class UserService(BaseService):
def get_admin_uids(self): def get_admin_uids(self):
return self.mapper.get_admin_uids() return self.mapper.get_admin_uids()
async def get_repository_path(self, user_uid):
path = pathlib.Path(f"./drive/repositories/{user_uid}")
if not path.exists():
return None
return path
async def get_static_path(self, user_uid): async def get_static_path(self, user_uid):
path = pathlib.Path(f"./drive/{user_uid}/snek/static") path = pathlib.Path(f"./drive/{user_uid}/snek/static")
if not path.exists(): if not path.exists():

View File

@ -0,0 +1,59 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Repository</h1>{% endblock %}
{% block main %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
.container {
div,input,label,button{
padding-bottom: 15px;
}
}
form {
padding: 2rem;
border-radius: 10px;
}
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
input[type="text"] {
padding: 0.5rem;
border: 1px solid #ccc; border-radius: 5px;
font-size: 1rem;
}
button, a.button {
background: #198754; color: #fff; border: none; border-radius: 5px;
padding: 0.1rem 0.8rem; text-decoration: none; cursor: pointer;
transition: background 0.2s;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.
.cancel {
background: #6c757d;
}
@media (max-width: 600px) {
.container { max-width: 98vw; }
form { padding: 1rem; }
}
</style>
</head>
<body>
<div class="container">
<form action="/settings/repositories/create.html" method="post">
<div>
<label for="name"><i class="fa-solid fa-book"></i> Name</label>
<input type="text" id="name" name="name" required placeholder="Repository name">
</div>
<div>
<label>
<input type="checkbox" name="is_private" value="1">
<i class="fa-solid fa-lock"></i> Private
</label>
</div>
<button type="submit"><i class="fa-solid fa-plus"></i> Create</button>
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i> Back</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Delete Repository</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
body { font-family: sans-serif; margin: 2rem; }
.container { max-width: 400px; margin: 0 auto; }
.confirm-box {
background: #ffe5e8;
border: 1.5px solid #dc3545;
padding: 2rem;
border-radius: 10px;
text-align: center;
margin-top: 2rem;
}
.repo-name {
font-weight: bold;
font-size: 1.2rem;
margin: 1rem 0;
color: #dc3545;
}
.actions {
display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem;
}
button, a {
background: #dc3545; color: #fff;
border: none; border-radius: 5px; padding: 0.6rem 1.2rem;
font-size: 1rem; cursor: pointer;
display: flex; align-items: center; gap: 0.5rem; text-decoration: none; justify-content: center;
transition: background 0.2s;
}
.cancel {
background: #6c757d;
}
@media (max-width: 600px) {
.container { max-width: 98vw; }
.confirm-box { padding: 1rem; }
}
</style>
</head>
<body>
<div class="container">
<h1><i class="fa-solid fa-trash-can"></i> Delete Repository</h1>
<div class="confirm-box">
<div>
<i class="fa-solid fa-triangle-exclamation" style="font-size:2rem; color:#dc3545;"></i>
</div>
<p>Are you sure you want to <strong>delete</strong> the following repository?</p>
<div class="repo-name"><i class="fa-solid fa-book"></i> my-first-repo</div>
<form action="/repositories/delete" method="post" style="margin-top:1.5rem;">
<input type="hidden" name="id" value="1">
<div class="actions">
<button type="submit"><i class="fa-solid fa-trash"></i> Yes, delete</button>
<a href="repositories.html" class="cancel"><i class="fa-solid fa-ban"></i> Cancel</a>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,106 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-database"></i> Repositories</h1>{% endblock %}
{% block main %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Repositories - List</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
.actions {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.repo-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.repo-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-radius: 8px;
flex-wrap: wrap;
}
.repo-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 220px;
}
.repo-name {
font-size: 1.1rem;
font-weight: 600;
}
@media (max-width: 600px) {
.repo-row { flex-direction: column; align-items: stretch; }
.actions { justify-content: flex-start; }
}
.topbar {
display: flex;
/* justify-content: flex-end;*/
margin-bottom: 1rem;
}
button, a.button {
background: #198754; color: #fff; border: none; border-radius: 5px;
padding: 0.4rem 0.8rem; text-decoration: none; cursor: pointer;
transition: background 0.2s;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.button.delete { background: #dc3545; }
.button.edit { background: #0d6efd; }
.button.clone { background: #6c757d; }
.button.browse { background: #ffc107; color: #212529; }
.button.create { background: #20c997; margin-left: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="button create" href="/settings/repositories/create.html">
<i class="fa-solid fa-plus"></i> New Repository
</a>
</div>
<section class="repo-list">
<!-- Example repository entries; replace with your templating/iteration -->
{% for repo in repositories %}
<div class="repo-row">
<div class="repo-info">
<span class="repo-name"><i class="fa-solid fa-book"></i> {{ repo.name }}</span>
<span title="Public">
<i class="fa-solid {% if repo.is_private %}fa-lock{% else %}fa-lock-open{% endif %}"></i>
{% if repo.is_private %}Private{% else %}Public{% endif %}
</span>
</div>
<div class="actions">
<a class="button browse" href="/repositories/{{ user.username }}/{{ repo.name }}" target="_blank">
<i class="fa-solid fa-folder-open"></i> Browse
</a>
<a class="button clone" href="/repositories/{{ user.username }}/{{ repo.name }}/clone">
<i class="fa-solid fa-code-branch"></i> Clone
</a>
<a class="button edit" href="/settings/repositories/repository/{{ repo.name }}/update.html">
<i class="fa-solid fa-pen"></i> Edit
</a>
<a class="button delete" href="/settings/repositories/{{ repo.name }}/delete.html">
<i class="fa-solid fa-trash"></i> Delete
</a>
</div>
</div>
{% endfor %}
<!-- ... -->
</section>
</div>
</body>
</html>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "settings/index.html" %}
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Repository</h1>{% endblock %}
{% block main %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
form {
padding: 2rem;
border-radius: 10px;
}
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
button {
background: #0d6efd; color: #fff;
border: none; border-radius: 5px; padding: 0.6rem 1rem;
cursor: pointer;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.cancel {
background: #6c757d;
}
@media (max-width: 600px) {
.container { max-width: 98vw; }
form { padding: 1rem; }
}
</style>
<div class="container">
<form method="post">
<!-- Assume hidden id for backend use -->
<input type="hidden" name="id" value="{{ repository.id }}">
<div>
<label for="name"><i class="fa-solid fa-book"></i> Name</label>
<input type="text" id="name" name="name" value="{{ repository.name }}" readonly>
</div>
<div>
<label>
<input type="checkbox" name="is_private" value="1" {% if repository.is_private %}checked{% endif %}>
<i class="fa-solid fa-lock"></i> Private
</label>
</div>
<button type="submit"><i class="fa-solid fa-pen"></i> Update</button>
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
</form>
</div>
{% endblock %}

View File

@ -3,7 +3,7 @@
<h2>You</h2> <h2>You</h2>
<ul> <ul>
<li><a class="no-select" href="/settings/profile.html">Profile</a></li> <li><a class="no-select" href="/settings/profile.html">Profile</a></li>
<li><a class="no-select" href="/settings/gists.html">Gists</a></li> <li><a class="no-select" href="/settings/repositories/index.html">Repositories</a></li>
</ul> </ul>
</aside> </aside>

View File

@ -0,0 +1,68 @@
import asyncio
from aiohttp import web
from snek.system.view import BaseFormView
import pathlib
class RepositoriesIndexView(BaseFormView):
login_required = True
async def get(self):
user_uid = self.session.get("uid")
repositories = []
async for repository in self.services.repository.find(user_uid=user_uid):
repositories.append(repository.record)
return await self.render_template("settings/repositories/index.html", {"repositories": repositories})
class RepositoriesCreateView(BaseFormView):
login_required = True
async def get(self):
return await self.render_template("settings/repositories/create.html")
async def post(self):
data = await self.request.post()
repository = await self.services.repository.create(user_uid=self.session.get("uid"), name=data['name'], is_private=int(data.get('is_private',0)))
return web.HTTPFound("/settings/repositories/index.html")
class RepositoriesUpdateView(BaseFormView):
login_required = True
async def get(self):
repository = await self.services.repository.get(
user_uid=self.session.get("uid"), name=self.request.match_info["name"]
)
if not repository:
return web.HTTPNotFound()
return await self.render_template("settings/repositories/update.html", {"repository": repository.record})
async def post(self):
data = await self.request.post()
repository = await self.services.repository.get(
user_uid=self.session.get("uid"), name=self.request.match_info["name"]
)
repository['is_private'] = int(data.get('is_private',0))
await self.services.repository.save(repository)
return web.HTTPFound("/settings/repositories/index.html")
class RepositoriesDeleteView(BaseFormView):
login_required = True
async def get(self):
return await self.render_template("settings/repositories/delete.html")