Drive access.
This commit is contained in:
parent
7dcabde2ed
commit
013d4adce5
39
Dockerfile
39
Dockerfile
@ -1,41 +1,10 @@
|
||||
FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf
|
||||
FROM python:3.12.8-alpine3.21
|
||||
FROM python:3.14.0a6-bookworm
|
||||
RUN mkdir -p /code
|
||||
WORKDIR /code
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
RUN apk add --no-cache gcc musl-dev linux-headers git
|
||||
|
||||
#WKHTMLTOPDFNEEDS
|
||||
RUN apk add --no-cache \
|
||||
libstdc++ \
|
||||
libx11 \
|
||||
libxrender \
|
||||
libxext \
|
||||
libssl3 \
|
||||
ca-certificates \
|
||||
docker \
|
||||
fontconfig \
|
||||
freetype \
|
||||
ttf-dejavu \
|
||||
ttf-droid \
|
||||
ttf-freefont \
|
||||
ttf-liberation \
|
||||
# more fonts
|
||||
&& apk add --no-cache --virtual .build-deps \
|
||||
msttcorefonts-installer \
|
||||
# Install microsoft fonts
|
||||
&& update-ms-fonts \
|
||||
&& fc-cache -f \
|
||||
# Clean up when done
|
||||
&& rm -rf /tmp/* \
|
||||
&& apk del .build-deps
|
||||
COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf
|
||||
COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage
|
||||
RUN apt update && apt install build-essential docker -y
|
||||
COPY pyproject.toml pyproject.toml
|
||||
COPY src src
|
||||
RUN mkdir /drive
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install -e .
|
||||
EXPOSE 8081
|
||||
|
||||
CMD ["python","-m","snek.app"]
|
||||
#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
|
||||
|
18
compose.yml
18
compose.yml
@ -7,26 +7,12 @@ services:
|
||||
- "8081:8081"
|
||||
volumes:
|
||||
- ./:/code
|
||||
- /media/storage/snek/molodetz.nl/drive:/code/drive
|
||||
- /media/storage/snek.molodetz.nl/drive:/code/drive
|
||||
- /media/storage/snek.molodetz.nl/drive:/drive
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /media/storage/snek/molodetz.nl/drive:/drive
|
||||
environment:
|
||||
- PYTHONDONTWRITEBYTECODE=1
|
||||
- PYTHONUNBUFFERED=1
|
||||
entrypoint: ["gunicorn", "-w", "1", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
|
||||
#entrypoint: ["python","-m","snek.app"]
|
||||
snecssh:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: DockerfileDrive
|
||||
restart: always
|
||||
ports:
|
||||
- "2225:2225"
|
||||
volumes:
|
||||
- ./:/code
|
||||
environment:
|
||||
- PYTHONDONTWRITEBYTECODE="1"
|
||||
- PYTHONUNBUFFERED="1"
|
||||
entrypoint: ["python","-m","snekssh.app2"]
|
||||
#["python","-m","snek.app"]
|
||||
|
||||
|
54
src/snek/scripts/chat.js
Normal file
54
src/snek/scripts/chat.js
Normal file
@ -0,0 +1,54 @@
|
||||
const channelUid = "{{ channel.uid.value }}";
|
||||
|
||||
function initInputField(textBox) {
|
||||
textBox.addEventListener('change', (e) => {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
|
||||
});
|
||||
|
||||
textBox.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const message = e.target.value.trim();
|
||||
if (message) {
|
||||
app.rpc.sendMessage(channelUid, message);
|
||||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
textBox.focus();
|
||||
}
|
||||
|
||||
function updateTimes() {
|
||||
document.querySelectorAll(".time").forEach((time) => {
|
||||
time.innerText = app.timeDescription(time.dataset.created_at);
|
||||
});
|
||||
}
|
||||
|
||||
function isElementVisible(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
const messagesContainer = document.querySelector(".chat-messages");
|
||||
|
||||
let isLoadingExtra = false;
|
||||
|
||||
messagesContainer.addEventListener("scroll", () => {
|
||||
loadExtra();
|
||||
});
|
||||
|
||||
setInterval(updateTimes, 1000);
|
||||
|
||||
app.addEventListener("channel-message", (data) => {
|
||||
if (data.channel_uid !== channelUid) {
|
||||
if(!isMentionForSomeoneElse(data.message)){
|
||||
channelSidebar.notify(data);
|
||||
}
|
||||
}
|
||||
});
|
48
src/snek/system/terminal.py
Normal file
48
src/snek/system/terminal.py
Normal file
@ -0,0 +1,48 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import aiohttp.web
|
||||
import os
|
||||
import pty
|
||||
import shlex
|
||||
import subprocess
|
||||
import pathlib
|
||||
|
||||
commands = {
|
||||
'alpine': 'docker run -it alpine /bin/sh',
|
||||
'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',
|
||||
}
|
||||
|
||||
class TerminalSession:
|
||||
def __init__(self,command):
|
||||
self.master, self.slave = pty.openpty()
|
||||
self.sockets =[]
|
||||
self.process = subprocess.Popen(
|
||||
command.split(" "),
|
||||
stdin=self.slave,
|
||||
stdout=self.slave,
|
||||
stderr=self.slave,
|
||||
bufsize=0,
|
||||
universal_newlines=True
|
||||
)
|
||||
|
||||
async def read_output(self, ws):
|
||||
loop = asyncio.get_event_loop()
|
||||
self.sockets.append(ws)
|
||||
if len(self.sockets) > 1:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
data = await loop.run_in_executor(None, os.read, self.master, 1024)
|
||||
if not data:
|
||||
break
|
||||
try:
|
||||
for ws in self.sockets: await ws.send_bytes(data) # Send raw bytes for ANSI support
|
||||
except:
|
||||
self.sockets.remove(ws)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
async def write_input(self, data):
|
||||
os.write(self.master, data.encode())
|
||||
|
||||
|
1
src/snek/templates/static
Symbolic link
1
src/snek/templates/static
Symbolic link
@ -0,0 +1 @@
|
||||
../static/
|
47
src/snek/templates/terminal.html
Normal file
47
src/snek/templates/terminal.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends "app.html" %}
|
||||
|
||||
{% block sidebar %}
|
||||
Reboot
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
||||
<style>
|
||||
#terminal { width: 100%; height: 480px; overflow-y: none; }
|
||||
</style>
|
||||
|
||||
<div class="container" id="terminal"></div>
|
||||
|
||||
<script>
|
||||
const term = new Terminal({ cursorBlink: true });
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById("terminal"));
|
||||
fitAddon.fit();
|
||||
|
||||
window.addEventListener("resize", () => fitAddon.fit());
|
||||
|
||||
const schema = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const hostname = window.location.host;
|
||||
const url = `${schema}://${hostname}/terminal.ws`;
|
||||
|
||||
const socket = new WebSocket(url);
|
||||
socket.binaryType = "arraybuffer"; // Support binary data
|
||||
|
||||
socket.onopen = () => term.write("\x1b[32mConnected to Molodetz\x1b[0m\r\n");
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const data = new Uint8Array(event.data);
|
||||
term.write(new TextDecoder().decode(data));
|
||||
};
|
||||
|
||||
term.onData(data => socket.send(new TextEncoder().encode(data)));
|
||||
|
||||
socket.onclose = () => term.write("\r\n\x1b[31mConnection closed\x1b[0m\r\n");
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% endblock main %}
|
56
src/snek/view/terminal.py
Normal file
56
src/snek/view/terminal.py
Normal file
@ -0,0 +1,56 @@
|
||||
from snek.system.view import BaseView
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from snek.system.terminal import TerminalSession
|
||||
import pathlib
|
||||
|
||||
class TerminalSocketView(BaseView):
|
||||
|
||||
login_required = True
|
||||
|
||||
user_sessions = {}
|
||||
|
||||
async def prepare_drive(self):
|
||||
user = await self.services.user.get(uid=self.session.get("uid"))
|
||||
root = pathlib.Path("drive").joinpath(user["uid"])
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
terminal_folder = pathlib.Path("terminal")
|
||||
for path in terminal_folder.iterdir():
|
||||
destination_path = root.joinpath(path.name)
|
||||
if not destination_path.exists():
|
||||
if not path.is_dir():
|
||||
destination_path.write_bytes(path.read_bytes())
|
||||
return root
|
||||
|
||||
async def get(self):
|
||||
|
||||
|
||||
|
||||
ws = aiohttp.web.WebSocketResponse()
|
||||
await ws.prepare(self.request)
|
||||
user = await self.services.user.get(uid=self.session.get("uid"))
|
||||
root = await self.prepare_drive()
|
||||
|
||||
command = f"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash"
|
||||
print(command)
|
||||
|
||||
session = self.user_sessions.get(user["uid"])
|
||||
if not session:
|
||||
self.user_sessions[user["uid"]] = TerminalSession(command=command)
|
||||
session = self.user_sessions[user["uid"]]
|
||||
asyncio.create_task(session.read_output(ws))
|
||||
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.BINARY:
|
||||
await session.write_input(msg.data.decode())
|
||||
|
||||
|
||||
return ws
|
||||
|
||||
class TerminalView(BaseView):
|
||||
|
||||
login_required = True
|
||||
|
||||
async def get(self):
|
||||
request = self.request
|
||||
return await self.request.app.render_template('terminal.html',self.request)
|
108
terminal/.bashrc
Normal file
108
terminal/.bashrc
Normal file
@ -0,0 +1,108 @@
|
||||
# ~/.bashrc: executed by bash(1) for non-login shells.
|
||||
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
|
||||
# for examples
|
||||
|
||||
# If not running interactively, don't do anything
|
||||
[ -z "$PS1" ] && return
|
||||
|
||||
# don't put duplicate lines in the history. See bash(1) for more options
|
||||
# ... or force ignoredups and ignorespace
|
||||
HISTCONTROL=ignoredups:ignorespace
|
||||
|
||||
# append to the history file, don't overwrite it
|
||||
shopt -s histappend
|
||||
|
||||
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
|
||||
HISTSIZE=1000
|
||||
HISTFILESIZE=2000
|
||||
|
||||
# check the window size after each command and, if necessary,
|
||||
# update the values of LINES and COLUMNS.
|
||||
shopt -s checkwinsize
|
||||
|
||||
# make less more friendly for non-text input files, see lesspipe(1)
|
||||
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
|
||||
|
||||
# set variable identifying the chroot you work in (used in the prompt below)
|
||||
if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then
|
||||
debian_chroot=$(cat /etc/debian_chroot)
|
||||
fi
|
||||
|
||||
# set a fancy prompt (non-color, unless we know we "want" color)
|
||||
case "$TERM" in
|
||||
xterm-color) color_prompt=yes;;
|
||||
esac
|
||||
|
||||
# uncomment for a colored prompt, if the terminal has the capability; turned
|
||||
# off by default to not distract the user: the focus in a terminal window
|
||||
# should be on the output of commands, not on the prompt
|
||||
#force_color_prompt=yes
|
||||
|
||||
if [ -n "$force_color_prompt" ]; then
|
||||
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
|
||||
# We have color support; assume it's compliant with Ecma-48
|
||||
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
|
||||
# a case would tend to support setf rather than setaf.)
|
||||
color_prompt=yes
|
||||
else
|
||||
color_prompt=
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$color_prompt" = yes ]; then
|
||||
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
|
||||
else
|
||||
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
|
||||
fi
|
||||
unset color_prompt force_color_prompt
|
||||
|
||||
# If this is an xterm set the title to user@host:dir
|
||||
case "$TERM" in
|
||||
xterm*|rxvt*)
|
||||
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
# enable color support of ls and also add handy aliases
|
||||
if [ -x /usr/bin/dircolors ]; then
|
||||
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
|
||||
alias ls='ls --color=auto'
|
||||
#alias dir='dir --color=auto'
|
||||
#alias vdir='vdir --color=auto'
|
||||
|
||||
alias grep='grep --color=auto'
|
||||
alias fgrep='fgrep --color=auto'
|
||||
alias egrep='egrep --color=auto'
|
||||
fi
|
||||
|
||||
# some more ls aliases
|
||||
alias ll='ls -alF'
|
||||
alias la='ls -A'
|
||||
alias l='ls -CF'
|
||||
|
||||
# Alias definitions.
|
||||
# You may want to put all your additions into a separate file like
|
||||
# ~/.bash_aliases, instead of adding them here directly.
|
||||
# See /usr/share/doc/bash-doc/examples in the bash-doc package.
|
||||
|
||||
if [ -f ~/.bash_aliases ]; then
|
||||
. ~/.bash_aliases
|
||||
fi
|
||||
|
||||
|
||||
cp ~/r /usr/local/bin
|
||||
|
||||
chmod -x /usr/local/bin/*
|
||||
|
||||
apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv -y
|
||||
|
||||
echo "r is installed."
|
||||
|
||||
# enable programmable completion features (you don't need to enable
|
||||
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
|
||||
# sources /etc/bash.bashrc).
|
||||
#if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
|
||||
# . /etc/bash_completion
|
||||
#fi
|
9
terminal/.profile
Normal file
9
terminal/.profile
Normal file
@ -0,0 +1,9 @@
|
||||
# ~/.profile: executed by Bourne-compatible login shells.
|
||||
|
||||
if [ "$BASH" ]; then
|
||||
if [ -f ~/.bashrc ]; then
|
||||
. ~/.bashrc
|
||||
fi
|
||||
fi
|
||||
|
||||
mesg n 2> /dev/null || true
|
BIN
terminal/r
Executable file
BIN
terminal/r
Executable file
Binary file not shown.
Loading…
Reference in New Issue
Block a user