From 6f9adfe67fd551dd99746c40bb55706a7ffcef3b Mon Sep 17 00:00:00 2001 From: retoor Date: Tue, 4 Feb 2025 23:38:13 +0100 Subject: [PATCH] Drive service. --- .gitignore | 2 ++ src/snek/app.py | 4 ++- src/snek/mapper/__init__.py | 4 +++ src/snek/mapper/drive.py | 7 ++++ src/snek/mapper/drive_item.py | 7 ++++ src/snek/model/drive.py | 7 ++++ src/snek/model/drive_item.py | 9 +++++ src/snek/service/__init__.py | 4 +++ src/snek/service/channel_message.py | 3 -- src/snek/service/drive.py | 21 +++++++++++ src/snek/service/drive_item.py | 18 ++++++++++ src/snek/static/app.js | 18 +++++----- src/snek/static/chat-input.js | 16 +++++++-- src/snek/static/chat-window.js | 6 ++-- src/snek/static/upload-button.js | 9 +++-- src/snek/view/upload.py | 56 +++++++++++++++++++++++++++++ 16 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 src/snek/mapper/drive.py create mode 100644 src/snek/mapper/drive_item.py create mode 100644 src/snek/model/drive.py create mode 100644 src/snek/model/drive_item.py create mode 100644 src/snek/service/drive.py create mode 100644 src/snek/service/drive_item.py create mode 100644 src/snek/view/upload.py diff --git a/.gitignore b/.gitignore index c1f3aef..ce8bd16 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ snek.d* *.zip *.db* cache +drive + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/src/snek/app.py b/src/snek/app.py index 137abf0..8cfed8f 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -26,8 +26,8 @@ from snek.view.register import RegisterView from snek.view.rpc import RPCView from snek.view.status import StatusView from snek.view.web import WebView +from snek.view.upload import UploadView -# base64.urlsafe_b64encode( SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34" @@ -81,6 +81,8 @@ class Application(BaseApplication): self.router.add_view("/login.json", LoginView) self.router.add_view("/register.html", RegisterView) self.router.add_view("/register.json", RegisterView) + self.router.add_view("/drive.bin", UploadView) + self.router.add_view("/drive.bin/{uid}", UploadView) 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) diff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py index 1841346..e4c67b0 100644 --- a/src/snek/mapper/__init__.py +++ b/src/snek/mapper/__init__.py @@ -5,6 +5,8 @@ from snek.mapper.channel_member import ChannelMemberMapper from snek.mapper.channel_message import ChannelMessageMapper from snek.mapper.notification import NotificationMapper from snek.mapper.user import UserMapper +from snek.mapper.drive import DriveMapper +from snek.mapper.drive_item import DriveItemMapper from snek.system.object import Object @@ -17,6 +19,8 @@ def get_mappers(app=None): "channel": ChannelMapper(app=app), "channel_message": ChannelMessageMapper(app=app), "notification": NotificationMapper(app=app), + "drive_item": DriveItemMapper(app=app), + "drive": DriveMapper(app=app), } ) diff --git a/src/snek/mapper/drive.py b/src/snek/mapper/drive.py new file mode 100644 index 0000000..970788a --- /dev/null +++ b/src/snek/mapper/drive.py @@ -0,0 +1,7 @@ +from snek.model.drive import DriveModel +from snek.system.mapper import BaseMapper + + +class DriveMapper(BaseMapper): + table_name = 'drive' + model_class = DriveModel diff --git a/src/snek/mapper/drive_item.py b/src/snek/mapper/drive_item.py new file mode 100644 index 0000000..c35afe1 --- /dev/null +++ b/src/snek/mapper/drive_item.py @@ -0,0 +1,7 @@ +from snek.system.mapper import BaseMapper +from snek.model.drive_item import DriveItemModel + +class DriveItemMapper(BaseMapper): + + model_class = DriveItemModel + table_name = 'drive_item' diff --git a/src/snek/model/drive.py b/src/snek/model/drive.py new file mode 100644 index 0000000..a310bbd --- /dev/null +++ b/src/snek/model/drive.py @@ -0,0 +1,7 @@ +from snek.system.model import BaseModel,ModelField + + +class DriveModel(BaseModel): + + user_uid = ModelField(name="user_uid", required=True) + diff --git a/src/snek/model/drive_item.py b/src/snek/model/drive_item.py new file mode 100644 index 0000000..74b8deb --- /dev/null +++ b/src/snek/model/drive_item.py @@ -0,0 +1,9 @@ +from snek.system.model import BaseModel,ModelField + + +class DriveItemModel(BaseModel): + drive_uid = ModelField(name="drive_uid", required=True,kind=str) + name = ModelField(name="name", required=True,kind=str) + path = ModelField(name="path", required=True,kind=str) + file_type = ModelField(name="file_type", required=True,kind=str) + file_size = ModelField(name="file_size", required=True,kind=int) diff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py index 97fbaae..e521c7b 100644 --- a/src/snek/service/__init__.py +++ b/src/snek/service/__init__.py @@ -8,6 +8,8 @@ from snek.service.notification import NotificationService from snek.service.socket import SocketService from snek.service.user import UserService from snek.service.util import UtilService +from snek.service.drive import DriveService +from snek.service.drive_item import DriveItemService from snek.system.object import Object @@ -23,6 +25,8 @@ def get_services(app): "socket": SocketService(app=app), "notification": NotificationService(app=app), "util": UtilService(app=app), + "drive": DriveService(app=app), + "drive_item": DriveItemService(app=app) } ) diff --git a/src/snek/service/channel_message.py b/src/snek/service/channel_message.py index 8765d53..dcc12d5 100644 --- a/src/snek/service/channel_message.py +++ b/src/snek/service/channel_message.py @@ -29,10 +29,7 @@ class ChannelMessageService(BaseService): model["html"] = template.render(**context) except Exception as ex: print(ex,flush=True) - print("RENDER",flush=True) - print("RECORD",context,flush=True) - print("AFTER RENDER",flush=True) if await self.save(model): return model raise Exception(f"Failed to create channel message: {model.errors}.") diff --git a/src/snek/service/drive.py b/src/snek/service/drive.py new file mode 100644 index 0000000..9d409a8 --- /dev/null +++ b/src/snek/service/drive.py @@ -0,0 +1,21 @@ +from snek.system.service import BaseService + + +class DriveService(BaseService): + + mapper_name = "drive" + + async def get_by_user(self, user_uid): + drives = [] + async for model in self.find(user_uid=user_uid): + drives.append(model) + return drives + + async def get_or_create(self, user_uid): + drives = await self.get_by_user(user_uid=user_uid) + if len(drives) == 0: + model = await self.new() + model['user_uid'] = user_uid + await self.save(model) + return model + return drives[0] diff --git a/src/snek/service/drive_item.py b/src/snek/service/drive_item.py new file mode 100644 index 0000000..058f55e --- /dev/null +++ b/src/snek/service/drive_item.py @@ -0,0 +1,18 @@ +from snek.system.service import BaseService + + +class DriveItemService(BaseService): + + mapper_name = "drive_item" + + async def create(self, drive_uid, name, path, type_,size): + model = await self.new() + model['drive_uid'] = drive_uid + model['name'] = name + model['path'] = str(path) + model['file_type'] = type_ + model['file_size'] = size + if await self.save(model): + return model + errors = await model.errors + raise Exception(f"Failed to create drive item: {errors}.") diff --git a/src/snek/static/app.js b/src/snek/static/app.js index 06fef95..a35f153 100644 --- a/src/snek/static/app.js +++ b/src/snek/static/app.js @@ -189,24 +189,24 @@ class Socket extends EventHandler { } this.isConnecting = true; return new Promise((resolve) => { - me.connectPromises.push(resolve); + this.connectPromises.push(resolve); console.debug("Connecting.."); - const ws = new WebSocket(me.url); + const ws = new WebSocket(this.url); ws.onopen = () => { - me.ws = ws; - me.isConnected = true; - me.isConnecting = false; + this.ws = ws; + this.isConnected = true; + this.isConnecting = false; ws.onmessage = (event) => { - me.onData(JSON.parse(event.data)); + this.onData(JSON.parse(event.data)); }; ws.onclose = () => { - me.onClose(); + this.onClose(); }; ws.onerror = () => { - me.onClose(); + this.onClose(); }; - me.connectPromises.forEach(resolver => resolver(me)); + this.connectPromises.forEach(resolver => resolver(this)); }; }); } diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 6a9353c..c1d767d 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -7,14 +7,23 @@ // MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License. class ChatInputElement extends HTMLElement { - + _chatWindow = null constructor() { super(); this.attachShadow({ mode: 'open' }); this.component = document.createElement('div'); this.shadowRoot.appendChild(this.component); } + set chatWindow(value){ + this._chatWindow = value + } + get chatWindow(){ + return this._chatWindow + } + get channelUid() { + return this.chatWindow.channel.uid + } connectedCallback() { const link = document.createElement('link'); link.rel = 'stylesheet'; @@ -28,7 +37,8 @@ class ChatInputElement extends HTMLElement { `; this.textBox = this.container.querySelector('textarea'); - + this.uploadButton = this.container.querySelector('upload-button'); + this.uploadButton.chatInput = this this.textBox.addEventListener('input', (e) => { this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true })); const message = e.target.value; @@ -56,4 +66,4 @@ class ChatInputElement extends HTMLElement { } } -customElements.define('chat-input', ChatInputElement); \ No newline at end of file +customElements.define('chat-input', ChatInputElement); diff --git a/src/snek/static/chat-window.js b/src/snek/static/chat-window.js index 2c2973a..0b341dc 100644 --- a/src/snek/static/chat-window.js +++ b/src/snek/static/chat-window.js @@ -13,7 +13,7 @@ class ChatWindowElement extends HTMLElement { receivedHistory = false; - + channel = null constructor() { super(); this.attachShadow({ mode: 'open' }); @@ -47,6 +47,7 @@ class ChatWindowElement extends HTMLElement { const channels = await app.rpc.getChannels(); const channel = channels[0]; + this.channel = channel; chatTitle.innerText = channel.name; const channelElement = document.createElement('message-list'); @@ -54,6 +55,7 @@ class ChatWindowElement extends HTMLElement { this.container.appendChild(channelElement); const chatInput = document.createElement('chat-input'); + chatInput.chatWindow = this; chatInput.addEventListener("submit", (e) => { app.rpc.sendMessage(channel.uid, e.detail); }); @@ -75,4 +77,4 @@ class ChatWindowElement extends HTMLElement { } } -customElements.define('chat-window', ChatWindowElement); \ No newline at end of file +customElements.define('chat-window', ChatWindowElement); diff --git a/src/snek/static/upload-button.js b/src/snek/static/upload-button.js index c6cfaa2..6e3052f 100644 --- a/src/snek/static/upload-button.js +++ b/src/snek/static/upload-button.js @@ -10,7 +10,7 @@ class UploadButtonElement extends HTMLElement { super(); this.attachShadow({ mode: 'open' }); } - + chatInput = null async uploadFiles() { const fileInput = this.container.querySelector('.file-input'); const uploadButton = this.container.querySelector('.upload-button'); @@ -21,12 +21,13 @@ class UploadButtonElement extends HTMLElement { const files = fileInput.files; const formData = new FormData(); + formData.append('channel_uid', this.chatInput.channelUid); for (let i = 0; i < files.length; i++) { formData.append('files[]', files[i]); } const request = new XMLHttpRequest(); - request.open('POST', '/upload', true); + request.open('POST', '/drive.bin', true); request.upload.onprogress = function (event) { if (event.lengthComputable) { @@ -37,7 +38,6 @@ class UploadButtonElement extends HTMLElement { request.onload = function () { if (request.status === 200) { - progressBar.style.width = '0%'; uploadButton.innerHTML = '📤'; } else { alert('Upload failed'); @@ -50,7 +50,7 @@ class UploadButtonElement extends HTMLElement { request.send(formData); } - + channelUid = null connectedCallback() { this.styleElement = document.createElement('style'); this.styleElement.innerHTML = ` @@ -95,7 +95,6 @@ class UploadButtonElement extends HTMLElement { } `; this.shadowRoot.appendChild(this.styleElement); - this.container = document.createElement('div'); this.container.innerHTML = `
diff --git a/src/snek/view/upload.py b/src/snek/view/upload.py new file mode 100644 index 0000000..ff208e9 --- /dev/null +++ b/src/snek/view/upload.py @@ -0,0 +1,56 @@ +from snek.system.view import BaseView +import aiofiles +import pathlib +from aiohttp import web +import uuid + +UPLOAD_DIR = pathlib.Path("./drive") + +class UploadView(BaseView): + + async def get(self): + uid = self.request.match_info.get("uid") + drive_item = await self.services.drive_item.get(uid) + + print(await drive_item.to_json(),flush=True) + return web.FileResponse(drive_item["path"]) + + async def post(self): + reader = await self.request.multipart() + files = [] + + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + + channel_uid = None + + drive = await self.services.drive.get_or_create(user_uid=self.request.session.get("uid")) + + print(str(drive),flush=True) + + while field := await reader.next(): + + if field.name == "channel_uid": + channel_uid = await field.text() + continue + + filename = field.filename + if not filename: + continue + + file_path = pathlib.Path(UPLOAD_DIR).joinpath(filename.strip("/").strip(".")) + files.append(file_path) + + async with aiofiles.open(str(file_path.absolute()), 'wb') as f: + while chunk := await field.read_chunk(): + await f.write(chunk) + + + drive_item = await self.services.drive_item.create(drive["uid"],filename,str(file_path.absolute()),file_path.stat().st_size,file_path.suffix) + + await self.services.chat.send(self.request.session.get("uid"),channel_uid,f"![{filename}](/drive.bin/{drive_item['uid']})") + print(drive_item,flush=True) + + return web.json_response({"message": "Files uploaded successfully", "files": [str(file) for file in files],"channel_uid":channel_uid}) + + +