This commit is contained in:
retoor 2025-05-10 20:38:32 +02:00
parent 3412aa0bf0
commit 9133b7c3ce
11 changed files with 129 additions and 4 deletions

View File

@ -53,6 +53,7 @@ from snek.view.terminal import TerminalSocketView, TerminalView
from snek.view.upload import UploadView
from snek.view.user import UserView
from snek.view.web import WebView
from snek.view.channel import ChannelAttachmentView
from snek.webdav import WebdavApplication
from snek.sgit import GitApplication
@ -175,6 +176,8 @@ class Application(BaseApplication):
self.router.add_get("/http-get", self.handle_http_get)
self.router.add_get("/http-photo", self.handle_http_photo)
self.router.add_get("/rpc.ws", RPCView)
self.router.add_view("/channel/{channel_uid}/attachment.bin",ChannelAttachmentView)
self.router.add_view("/channel/attachment/{relative_url:.*}",ChannelAttachmentView)
self.router.add_view("/channel/{channel}.html", WebView)
self.router.add_view("/threads.html", ThreadsView)
self.router.add_view("/terminal.ws", TerminalSocketView)

View File

@ -9,6 +9,7 @@ from snek.mapper.notification import NotificationMapper
from snek.mapper.user import UserMapper
from snek.mapper.user_property import UserPropertyMapper
from snek.mapper.repository import RepositoryMapper
from snek.mapper.channel_attachment import ChannelAttachmentMapper
from snek.system.object import Object
@ -25,6 +26,7 @@ def get_mappers(app=None):
"drive": DriveMapper(app=app),
"user_property": UserPropertyMapper(app=app),
"repository": RepositoryMapper(app=app),
"channel_attachment": ChannelAttachmentMapper(app=app),
}
)

View File

@ -0,0 +1,7 @@
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.system.mapper import BaseMapper
class ChannelAttachmentMapper(BaseMapper):
table_name = "channel_attachment"
model_class = ChannelAttachmentModel

View File

@ -11,6 +11,7 @@ from snek.model.notification import NotificationModel
from snek.model.user import UserModel
from snek.model.user_property import UserPropertyModel
from snek.model.repository import RepositoryModel
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.system.object import Object
@ -27,6 +28,7 @@ def get_models():
"notification": NotificationModel,
"user_property": UserPropertyModel,
"repository": RepositoryModel,
"channel_attachment": ChannelAttachmentModel,
}
)

View File

@ -0,0 +1,16 @@
from snek.system.model import BaseModel
from snek.system.model import BaseModel, ModelField
class ChannelAttachmentModel(BaseModel):
name = ModelField(name="name", required=True, kind=str)
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
path = ModelField(name="path", required=True, kind=str)
size = ModelField(name="size", required=False, kind=int)
user_uid = ModelField(name="user_uid", required=True, kind=str)
mime_type = ModelField(name="type", required=True, kind=str)
relative_url = ModelField(name="relative_url", required=True, kind=str)
resource_type = ModelField(name="resource_type", required=True, kind=str,value="file")

View File

@ -12,6 +12,7 @@ from snek.service.user import UserService
from snek.service.user_property import UserPropertyService
from snek.service.util import UtilService
from snek.service.repository import RepositoryService
from snek.service.channel_attachment import ChannelAttachmentService
from snek.system.object import Object
from snek.service.db import DBService
@ -32,6 +33,7 @@ def get_services(app):
"user_property": UserPropertyService(app=app),
"repository": RepositoryService(app=app),
"db": DBService(app=app),
"channel_attachment": ChannelAttachmentService(app=app),
}
)

View File

@ -3,10 +3,19 @@ from datetime import datetime
from snek.system.model import now
from snek.system.service import BaseService
import pathlib
class ChannelService(BaseService):
mapper_name = "channel"
async def get_attachment_folder(self, channel_uid,ensure=False):
path = pathlib.Path(f"./drive/{channel_uid}/attachments")
if ensure:
path.mkdir(
parents=True, exist_ok=True
)
return path
async def get(self, uid=None, **kwargs):
if uid:
kwargs["uid"] = uid

View File

@ -0,0 +1,25 @@
from snek.system.service import BaseService
import urllib.parse
import pathlib
import mimetypes
import uuid
class ChannelAttachmentService(BaseService):
mapper_name="channel_attachment"
async def create_file(self, channel_uid, user_uid, name):
attachment = await self.new()
attachment["channel_uid"] = channel_uid
attachment['user_uid'] = user_uid
attachment["name"] = name
attachment["mime_type"] = mimetypes.guess_type(name)[0]
attachment['resource_type'] = "file"
real_file_name = f"{attachment['uid']}-{name}"
attachment["relative_url"] = urllib.parse.quote(f"{attachment['uid']}/{name}")
attachment_folder = await self.services.channel.get_attachment_folder(channel_uid)
attachment_path = attachment_folder.joinpath(real_file_name)
attachment["path"] = str(attachment_path)
if await self.save(attachment):
return attachment
raise Exception(f"Failed to create channel attachment: {attachment.errors}.")

View File

@ -21,13 +21,13 @@ class UploadButtonElement extends HTMLElement {
const files = fileInput.files;
const formData = new FormData();
formData.append('channel_uid', this.channelUid);
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
const request = new XMLHttpRequest();
request.open('POST', '/drive.bin', true);
request.responseType = 'json';
request.open('POST', `/channel/${this.channelUid}/attachment.bin`, true);
request.upload.onprogress = function (event) {
if (event.lengthComputable) {
@ -35,9 +35,10 @@ class UploadButtonElement extends HTMLElement {
uploadButton.innerText = `${Math.round(percentComplete)}%`;
}
};
const me = this
request.onload = function () {
if (request.status === 200) {
me.dispatchEvent(new CustomEvent('uploaded', { detail: request.response }));
uploadButton.innerHTML = '📤';
} else {
alert('Upload failed');

View File

@ -47,6 +47,11 @@
document.querySelector("upload-button").addEventListener("upload",function(e){
getInputField().focus();
})
document.querySelector("upload-button").addEventListener("uploaded",function(e){
e.detail.files.forEach((file)=>{
app.rpc.sendMessage(channelUid,`![${file.name}](/channel/attachment/${file.relative_url})`)
})
})
textBox.addEventListener("paste", async (e) => {
try {
const clipboardItems = await navigator.clipboard.read();

53
src/snek/view/channel.py Normal file
View File

@ -0,0 +1,53 @@
from snek.system.view import BaseView
import aiofiles
from aiohttp import web
import pathlib
class ChannelAttachmentView(BaseView):
async def get(self):
relative_path = self.request.match_info.get("relative_url")
channel_attachment = await self.services.channel_attachment.get(relative_url=relative_path)
response = web.FileResponse(channel_attachment["path"])
response.headers["Cache-Control"] = f"public, max-age={1337*420}"
response.headers["Content-Disposition"] = (
f'attachment; filename="{channel_attachment["name"]}"'
)
return response
async def post(self):
channel_uid = self.request.match_info.get("channel_uid")
user_uid = self.request.session.get("uid")
channel_member = await self.services.channel_member.get(user_uid=user_uid, channel_uid=channel_uid,deleted_at=None,is_banned=False)
if not channel_member:
return web.HTTPNotFound()
reader = await self.request.multipart()
attachments = []
while field := await reader.next():
filename = field.filename
if not filename:
continue
attachment = await self.services.channel_attachment.create_file(
channel_uid=channel_uid, name=filename,user_uid=user_uid
)
attachments.append(attachment)
pathlib.Path(attachment['path']).parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(attachment['path'], "wb") as f:
while chunk := await field.read_chunk():
await f.write(chunk)
return web.json_response(
{
"message": "Files uploaded successfully",
"files": [attachment.record for attachment in attachments],
"channel_uid": channel_uid,
}
)