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.14.0a6-bookworm
|
||||||
FROM python:3.12.8-alpine3.21
|
RUN mkdir -p /code
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
ENV FLASK_APP=app.py
|
RUN apt update && apt install build-essential docker -y
|
||||||
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
|
|
||||||
COPY pyproject.toml pyproject.toml
|
COPY pyproject.toml pyproject.toml
|
||||||
COPY src src
|
COPY src src
|
||||||
|
RUN mkdir /drive
|
||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
RUN pip install -e .
|
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"
|
- "8081:8081"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/code
|
- ./:/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
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /media/storage/snek/molodetz.nl/drive:/drive
|
|
||||||
environment:
|
environment:
|
||||||
- PYTHONDONTWRITEBYTECODE=1
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
entrypoint: ["gunicorn", "-w", "1", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
|
entrypoint: ["gunicorn", "-w", "1", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
|
||||||
#entrypoint: ["python","-m","snek.app"]
|
#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