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