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.upload import UploadView
from snek.view.user import UserView from snek.view.user import UserView
from snek.view.web import WebView from snek.view.web import WebView
from snek.view.channel import ChannelAttachmentView
from snek.webdav import WebdavApplication from snek.webdav import WebdavApplication
from snek.sgit import GitApplication 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-get", self.handle_http_get)
self.router.add_get("/http-photo", self.handle_http_photo) self.router.add_get("/http-photo", self.handle_http_photo)
self.router.add_get("/rpc.ws", RPCView) 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("/channel/{channel}.html", WebView)
self.router.add_view("/threads.html", ThreadsView) self.router.add_view("/threads.html", ThreadsView)
self.router.add_view("/terminal.ws", TerminalSocketView) 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 import UserMapper
from snek.mapper.user_property import UserPropertyMapper from snek.mapper.user_property import UserPropertyMapper
from snek.mapper.repository import RepositoryMapper from snek.mapper.repository import RepositoryMapper
from snek.mapper.channel_attachment import ChannelAttachmentMapper
from snek.system.object import Object from snek.system.object import Object
@ -25,6 +26,7 @@ def get_mappers(app=None):
"drive": DriveMapper(app=app), "drive": DriveMapper(app=app),
"user_property": UserPropertyMapper(app=app), "user_property": UserPropertyMapper(app=app),
"repository": RepositoryMapper(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 import UserModel
from snek.model.user_property import UserPropertyModel from snek.model.user_property import UserPropertyModel
from snek.model.repository import RepositoryModel from snek.model.repository import RepositoryModel
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.system.object import Object from snek.system.object import Object
@ -27,6 +28,7 @@ def get_models():
"notification": NotificationModel, "notification": NotificationModel,
"user_property": UserPropertyModel, "user_property": UserPropertyModel,
"repository": RepositoryModel, "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.user_property import UserPropertyService
from snek.service.util import UtilService from snek.service.util import UtilService
from snek.service.repository import RepositoryService from snek.service.repository import RepositoryService
from snek.service.channel_attachment import ChannelAttachmentService
from snek.system.object import Object from snek.system.object import Object
from snek.service.db import DBService from snek.service.db import DBService
@ -32,6 +33,7 @@ def get_services(app):
"user_property": UserPropertyService(app=app), "user_property": UserPropertyService(app=app),
"repository": RepositoryService(app=app), "repository": RepositoryService(app=app),
"db": DBService(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.model import now
from snek.system.service import BaseService from snek.system.service import BaseService
import pathlib
class ChannelService(BaseService): class ChannelService(BaseService):
mapper_name = "channel" 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): async def get(self, uid=None, **kwargs):
if uid: if uid:
kwargs["uid"] = 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 files = fileInput.files;
const formData = new FormData(); const formData = new FormData();
formData.append('channel_uid', this.channelUid);
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]); formData.append('files[]', files[i]);
} }
const request = new XMLHttpRequest(); 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) { request.upload.onprogress = function (event) {
if (event.lengthComputable) { if (event.lengthComputable) {
@ -35,9 +35,10 @@ class UploadButtonElement extends HTMLElement {
uploadButton.innerText = `${Math.round(percentComplete)}%`; uploadButton.innerText = `${Math.round(percentComplete)}%`;
} }
}; };
const me = this
request.onload = function () { request.onload = function () {
if (request.status === 200) { if (request.status === 200) {
me.dispatchEvent(new CustomEvent('uploaded', { detail: request.response }));
uploadButton.innerHTML = '📤'; uploadButton.innerHTML = '📤';
} else { } else {
alert('Upload failed'); alert('Upload failed');

View File

@ -47,6 +47,11 @@
document.querySelector("upload-button").addEventListener("upload",function(e){ document.querySelector("upload-button").addEventListener("upload",function(e){
getInputField().focus(); 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) => { textBox.addEventListener("paste", async (e) => {
try { try {
const clipboardItems = await navigator.clipboard.read(); 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,
}
)