Drive service.

This commit is contained in:
retoor 2025-02-04 23:38:13 +01:00
parent 084f8dba20
commit 6f9adfe67f
16 changed files with 168 additions and 23 deletions

2
.gitignore vendored
View File

@ -8,6 +8,8 @@ snek.d*
*.zip *.zip
*.db* *.db*
cache cache
drive
# ---> Python # ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

View File

@ -26,8 +26,8 @@ from snek.view.register import RegisterView
from snek.view.rpc import RPCView from snek.view.rpc import RPCView
from snek.view.status import StatusView from snek.view.status import StatusView
from snek.view.web import WebView from snek.view.web import WebView
from snek.view.upload import UploadView
# base64.urlsafe_b64encode(
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34" SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@ -81,6 +81,8 @@ class Application(BaseApplication):
self.router.add_view("/login.json", LoginView) self.router.add_view("/login.json", LoginView)
self.router.add_view("/register.html", RegisterView) self.router.add_view("/register.html", RegisterView)
self.router.add_view("/register.json", 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-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)

View File

@ -5,6 +5,8 @@ from snek.mapper.channel_member import ChannelMemberMapper
from snek.mapper.channel_message import ChannelMessageMapper from snek.mapper.channel_message import ChannelMessageMapper
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.drive import DriveMapper
from snek.mapper.drive_item import DriveItemMapper
from snek.system.object import Object from snek.system.object import Object
@ -17,6 +19,8 @@ def get_mappers(app=None):
"channel": ChannelMapper(app=app), "channel": ChannelMapper(app=app),
"channel_message": ChannelMessageMapper(app=app), "channel_message": ChannelMessageMapper(app=app),
"notification": NotificationMapper(app=app), "notification": NotificationMapper(app=app),
"drive_item": DriveItemMapper(app=app),
"drive": DriveMapper(app=app),
} }
) )

7
src/snek/mapper/drive.py Normal file
View File

@ -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

View File

@ -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'

7
src/snek/model/drive.py Normal file
View File

@ -0,0 +1,7 @@
from snek.system.model import BaseModel,ModelField
class DriveModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True)

View File

@ -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)

View File

@ -8,6 +8,8 @@ from snek.service.notification import NotificationService
from snek.service.socket import SocketService from snek.service.socket import SocketService
from snek.service.user import UserService from snek.service.user import UserService
from snek.service.util import UtilService 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 from snek.system.object import Object
@ -23,6 +25,8 @@ def get_services(app):
"socket": SocketService(app=app), "socket": SocketService(app=app),
"notification": NotificationService(app=app), "notification": NotificationService(app=app),
"util": UtilService(app=app), "util": UtilService(app=app),
"drive": DriveService(app=app),
"drive_item": DriveItemService(app=app)
} }
) )

View File

@ -29,10 +29,7 @@ class ChannelMessageService(BaseService):
model["html"] = template.render(**context) model["html"] = template.render(**context)
except Exception as ex: except Exception as ex:
print(ex,flush=True) print(ex,flush=True)
print("RENDER",flush=True)
print("RECORD",context,flush=True)
print("AFTER RENDER",flush=True)
if await self.save(model): if await self.save(model):
return model return model
raise Exception(f"Failed to create channel message: {model.errors}.") raise Exception(f"Failed to create channel message: {model.errors}.")

21
src/snek/service/drive.py Normal file
View File

@ -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]

View File

@ -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}.")

View File

@ -189,24 +189,24 @@ class Socket extends EventHandler {
} }
this.isConnecting = true; this.isConnecting = true;
return new Promise((resolve) => { return new Promise((resolve) => {
me.connectPromises.push(resolve); this.connectPromises.push(resolve);
console.debug("Connecting.."); console.debug("Connecting..");
const ws = new WebSocket(me.url); const ws = new WebSocket(this.url);
ws.onopen = () => { ws.onopen = () => {
me.ws = ws; this.ws = ws;
me.isConnected = true; this.isConnected = true;
me.isConnecting = false; this.isConnecting = false;
ws.onmessage = (event) => { ws.onmessage = (event) => {
me.onData(JSON.parse(event.data)); this.onData(JSON.parse(event.data));
}; };
ws.onclose = () => { ws.onclose = () => {
me.onClose(); this.onClose();
}; };
ws.onerror = () => { ws.onerror = () => {
me.onClose(); this.onClose();
}; };
me.connectPromises.forEach(resolver => resolver(me)); this.connectPromises.forEach(resolver => resolver(this));
}; };
}); });
} }

View File

@ -7,14 +7,23 @@
// MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License. // MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License.
class ChatInputElement extends HTMLElement { class ChatInputElement extends HTMLElement {
_chatWindow = null
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.component = document.createElement('div'); this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
set chatWindow(value){
this._chatWindow = value
}
get chatWindow(){
return this._chatWindow
}
get channelUid() {
return this.chatWindow.channel.uid
}
connectedCallback() { connectedCallback() {
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
@ -28,7 +37,8 @@ class ChatInputElement extends HTMLElement {
<upload-button></upload-button> <upload-button></upload-button>
`; `;
this.textBox = this.container.querySelector('textarea'); this.textBox = this.container.querySelector('textarea');
this.uploadButton = this.container.querySelector('upload-button');
this.uploadButton.chatInput = this
this.textBox.addEventListener('input', (e) => { this.textBox.addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true })); this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));
const message = e.target.value; const message = e.target.value;

View File

@ -13,7 +13,7 @@
class ChatWindowElement extends HTMLElement { class ChatWindowElement extends HTMLElement {
receivedHistory = false; receivedHistory = false;
channel = null
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
@ -47,6 +47,7 @@ class ChatWindowElement extends HTMLElement {
const channels = await app.rpc.getChannels(); const channels = await app.rpc.getChannels();
const channel = channels[0]; const channel = channels[0];
this.channel = channel;
chatTitle.innerText = channel.name; chatTitle.innerText = channel.name;
const channelElement = document.createElement('message-list'); const channelElement = document.createElement('message-list');
@ -54,6 +55,7 @@ class ChatWindowElement extends HTMLElement {
this.container.appendChild(channelElement); this.container.appendChild(channelElement);
const chatInput = document.createElement('chat-input'); const chatInput = document.createElement('chat-input');
chatInput.chatWindow = this;
chatInput.addEventListener("submit", (e) => { chatInput.addEventListener("submit", (e) => {
app.rpc.sendMessage(channel.uid, e.detail); app.rpc.sendMessage(channel.uid, e.detail);
}); });

View File

@ -10,7 +10,7 @@ class UploadButtonElement extends HTMLElement {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
} }
chatInput = null
async uploadFiles() { async uploadFiles() {
const fileInput = this.container.querySelector('.file-input'); const fileInput = this.container.querySelector('.file-input');
const uploadButton = this.container.querySelector('.upload-button'); const uploadButton = this.container.querySelector('.upload-button');
@ -21,12 +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.chatInput.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', '/upload', true); request.open('POST', '/drive.bin', true);
request.upload.onprogress = function (event) { request.upload.onprogress = function (event) {
if (event.lengthComputable) { if (event.lengthComputable) {
@ -37,7 +38,6 @@ class UploadButtonElement extends HTMLElement {
request.onload = function () { request.onload = function () {
if (request.status === 200) { if (request.status === 200) {
progressBar.style.width = '0%';
uploadButton.innerHTML = '📤'; uploadButton.innerHTML = '📤';
} else { } else {
alert('Upload failed'); alert('Upload failed');
@ -50,7 +50,7 @@ class UploadButtonElement extends HTMLElement {
request.send(formData); request.send(formData);
} }
channelUid = null
connectedCallback() { connectedCallback() {
this.styleElement = document.createElement('style'); this.styleElement = document.createElement('style');
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
@ -95,7 +95,6 @@ class UploadButtonElement extends HTMLElement {
} }
`; `;
this.shadowRoot.appendChild(this.styleElement); this.shadowRoot.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.innerHTML = ` this.container.innerHTML = `
<div class="upload-container"> <div class="upload-container">

56
src/snek/view/upload.py Normal file
View File

@ -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})