Compare commits
25 Commits
212bf7ca75
...
f8cc04f18e
Author | SHA1 | Date | |
---|---|---|---|
f8cc04f18e | |||
bc26af8a97 | |||
dd0e1b63cf | |||
8b1337bdc1 | |||
8c1ab0b53b | |||
2c1342a20e | |||
66fd12473e | |||
e0b51e3c96 | |||
79b65dfb34 | |||
8e9ffa2405 | |||
7beb2ae538 | |||
59104bcb12 | |||
c60056d7ce | |||
8113202f5f | |||
16761d4382 | |||
c2e09996a2 | |||
d68add7803 | |||
912f0049af | |||
34f04d3380 | |||
929df87147 | |||
2edeff726d | |||
cd4592d257 | |||
677ac4f427 | |||
6240310747 | |||
08df1ee3dc |
@ -1,4 +0,0 @@
|
||||
__pycache__
|
||||
upload
|
||||
venv
|
||||
.git
|
@ -1 +0,0 @@
|
||||
UPLOAD_ONLY=1 # Enable upload only for the instance
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
__pycache__
|
||||
upload
|
||||
env
|
||||
.env
|
||||
files
|
||||
|
13
Dockerfile
13
Dockerfile
@ -1,13 +0,0 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
RUN apk add --no-cache --update libmagic \
|
||||
&& pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips='*'"]
|
106
README.md
106
README.md
@ -1,68 +1,92 @@
|
||||
# UpFast
|
||||
|
||||
Simple tool for uploading and sharing files that you can self-host.
|
||||
The new and improwed version of upfast, now writen in GO!
|
||||
|
||||
## Settings
|
||||
This one is gonna be a lot easier to deploy since all you need to do is download the upfast binary and run it in a directory.
|
||||
|
||||
All settings are controled by using a .env file, an example is in the repo.
|
||||
To change the port or the ip adress to listen for you can use `-p` and `-a` options respectivelly.
|
||||
|
||||
- Upload only: You can make the instance to be upload only. In that case you need to manually delete the files from your hosting machine.
|
||||
|
||||
## How to host
|
||||
|
||||
### Regular system
|
||||
|
||||
To run on a regular system I recommend to use a virtual environment to install the dependencies and run the project from there.
|
||||
|
||||
We will also be setting up an specific user to run the app as safe as possible with a systemd service file for startup
|
||||
|
||||
#### Installing
|
||||
> example
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
./upfast -p 8080 -a 0.0.0.0
|
||||
```
|
||||
|
||||
#### Running
|
||||
By default upfast listen's on 127.0.0.1 adress so if you wan't to access it outside your network you will either need to listen to 0.0.0.0 or use a reverse proxy. I recommend the usage of a reverse proxy.
|
||||
|
||||
## Note for the users of the python version
|
||||
|
||||
You will need to rename the `upload` folder to `files`, since I changed the naming a bit.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
systemctl start upfast.service
|
||||
# Create upfast user and home directory
|
||||
useradd --shell /bin/sh --system --home-dir /usr/local/upfast upfast
|
||||
mkdir -p /usr/local/upfast # dodge copying of skeletons
|
||||
chown upfast:upfast /usr/local/upfast
|
||||
chmod 700 /usr/local/upfast
|
||||
|
||||
# change to upfast user
|
||||
su upfast
|
||||
# go to upfast's user's home and curl the upfast binary
|
||||
cd ~
|
||||
curl -O binary
|
||||
```
|
||||
|
||||
#### Start on boot
|
||||
## Updating
|
||||
|
||||
When new update of upfast come's out all you will need to change to upfast user and curl the new binary in it's home directory overwritting the old one like this.
|
||||
|
||||
```bash
|
||||
systemctl enable upfast.service
|
||||
su upfast
|
||||
cd ~
|
||||
curl -O binary
|
||||
```
|
||||
|
||||
### Docker
|
||||
## Startup automaticallly on system restart
|
||||
|
||||
In the repo there is an included dockerfile to generate an image from the latest version of everything, to generate an image just run `docker build . -t upfast` (You need root privileges or to be in the docker group).
|
||||
If you use the old version of upfast you just need to change values in the systemd service located `/etc/systemd/system/upfast.service`, otherwise create a file in that path with this content.
|
||||
|
||||
To run the docker container check the container id with `docker images` command.
|
||||
> /etc/systemd/system/upfast.service
|
||||
|
||||
> sample docker run command
|
||||
```systemd-service
|
||||
[Unit]
|
||||
Description=UpFast service
|
||||
Documentation=https://code.cronyakatsuki.xyz/crony/upfast
|
||||
|
||||
```bash
|
||||
sudo docker run -p 8000:8000 -v ./upload:/usr/src/app/upload CONTAINER_ID
|
||||
[Service]
|
||||
User=upfast
|
||||
Group=upfast
|
||||
WorkingDirectory=/usr/local/upfast/
|
||||
ExecStart=/usr/local/upfast/upfast -p 8000 -a 127.0.0.1
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
The sample command will need an upload directory, you can replace `./upload` with a different path to save uploaded stuff.
|
||||
Remember to change the port if you need to or the listening adress.
|
||||
|
||||
### Nginx Proxy setup
|
||||
After that run `systemctl daemon-reload` as root or with sudo.
|
||||
|
||||
This is an example nginx proxy config for http
|
||||
## Nginx proxy setup
|
||||
|
||||
```bash
|
||||
cp ./upfast-nginx /etc/nginx/sites-available/upfast
|
||||
ln -sf /etc/nginx/sites-available/upfast /etc/nginx/sites-enabled/
|
||||
To setup nginx as the reverse proxy you will need to add this server block to either the main nginx config file or in a separate file depending on your version of nginx.
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name upfast.example.xyz;
|
||||
|
||||
location / {
|
||||
proxy_pass https://127.0.0.1:8000;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Load config
|
||||
|
||||
```bash
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
# Contributions
|
||||
|
||||
Thanks [TLasT](https://craftmenners.men) on his help with testing and documentation.
|
||||
|
19
go.mod
Normal file
19
go.mod
Normal file
@ -0,0 +1,19 @@
|
||||
module cronyakatsuki/upfast
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require github.com/labstack/echo/v4 v4.11.3
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
)
|
47
go.sum
Normal file
47
go.sum
Normal file
@ -0,0 +1,47 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
||||
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
26
install.sh
26
install.sh
@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# quick install (and run) script for upfast using systemd
|
||||
|
||||
# Check if script is run by root
|
||||
[ $EUID -ne 0 ] && printf '%s\n' "Please run as root" && exit 1
|
||||
|
||||
apt-get install python3-pip
|
||||
|
||||
useradd --shell /bin/sh --system --home-dir /usr/local/upfast upfast
|
||||
mkdir -p /usr/local/upfast # dodge copying of skeletons
|
||||
|
||||
chown upfast:upfast /usr/local/upfast
|
||||
chmod 700 /usr/local/upfast
|
||||
|
||||
su upfast -c'
|
||||
cd
|
||||
git clone https://code.cronyakatsuki.xyz/crony/upfast .
|
||||
|
||||
python3 -m venv env
|
||||
. ./env/bin/activate
|
||||
pip install -r requirements.txt
|
||||
mkdir upload'
|
||||
|
||||
cp /usr/local/upfast/upfast.service /etc/systemd/system/upfast.service
|
||||
systemctl daemon-reload
|
225
main.go
Normal file
225
main.go
Normal file
@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
//go:embed all:public/views/*.html
|
||||
var templates embed.FS
|
||||
|
||||
//go:embed all:static/*
|
||||
var static embed.FS
|
||||
|
||||
func main() {
|
||||
t := &Template{
|
||||
templates: template.Must(template.ParseFS(templates, "public/views/*.html")),
|
||||
}
|
||||
|
||||
port := flag.Int("p", 1323, "upfast port to listen on.")
|
||||
adress := flag.String("a", "127.0.0.1", "upfast ip to listen to")
|
||||
flag.Parse()
|
||||
|
||||
host := *adress + ":" + strconv.Itoa(*port)
|
||||
|
||||
e := echo.New()
|
||||
|
||||
e.Renderer = t
|
||||
e.Use(middleware.Logger())
|
||||
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Root: "static",
|
||||
Browse: false,
|
||||
HTML5: true,
|
||||
Filesystem: http.FS(static),
|
||||
}))
|
||||
|
||||
files := "files"
|
||||
|
||||
if _, err := os.Stat(files); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(files, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
e.Static("/files", files)
|
||||
|
||||
e.GET("/", Index)
|
||||
|
||||
e.POST("/", Upload)
|
||||
|
||||
e.GET("/files/", Files)
|
||||
|
||||
e.DELETE("/files/:file", Delete)
|
||||
|
||||
e.Logger.Fatal(e.Start(host))
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Name string
|
||||
FileType string
|
||||
Content string
|
||||
}
|
||||
|
||||
type FilesData struct {
|
||||
Files []File
|
||||
}
|
||||
|
||||
type IndexData struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func Index(c echo.Context) error {
|
||||
data := IndexData{
|
||||
Host: c.Request().Host,
|
||||
}
|
||||
return c.Render(http.StatusOK, "index", data)
|
||||
}
|
||||
|
||||
func GetFileContentType(ouput *os.File) (string, error) {
|
||||
buf := make([]byte, 512)
|
||||
|
||||
_, err := ouput.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contentType := http.DetectContentType(buf)
|
||||
|
||||
return contentType, nil
|
||||
}
|
||||
|
||||
func Files(c echo.Context) error {
|
||||
var files FilesData
|
||||
|
||||
filelist, err := os.ReadDir("./files/")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var Type string
|
||||
var Content string
|
||||
|
||||
ImageMatch := regexp.MustCompile("^image/.*")
|
||||
VideoMatch := regexp.MustCompile("^video/.*")
|
||||
JsonMatch := regexp.MustCompile("application/json")
|
||||
TextMatch := regexp.MustCompile("^text/.*|application/octet-stream")
|
||||
|
||||
for _, f := range filelist {
|
||||
filePath := "files/" + f.Name()
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
contentType, err := GetFileContentType(file)
|
||||
|
||||
switch {
|
||||
case ImageMatch.MatchString(contentType):
|
||||
Type = "image"
|
||||
Content = ""
|
||||
case VideoMatch.MatchString(contentType):
|
||||
Type = "video"
|
||||
Content = ""
|
||||
case JsonMatch.MatchString(contentType):
|
||||
Type = "text"
|
||||
b, _ := os.ReadFile(filePath)
|
||||
Content = string(b)
|
||||
case TextMatch.MatchString(contentType):
|
||||
Type = "text"
|
||||
b, _ := os.ReadFile(filePath)
|
||||
Content = string(b)
|
||||
default:
|
||||
Type = "else"
|
||||
Content = ""
|
||||
}
|
||||
|
||||
log.Print(contentType)
|
||||
files.Files = append(
|
||||
files.Files,
|
||||
File{Name: f.Name(), FileType: Type, Content: Content},
|
||||
)
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "files", files)
|
||||
}
|
||||
|
||||
func Upload(c echo.Context) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat("files/" + file.Filename); err == nil {
|
||||
return c.String(http.StatusOK, "A file with the same name already exist's on the server.\n")
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst, err := os.Create("files/" + file.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err = io.Copy(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileUrl := c.Request().Host + "/files/" + file.Filename + "\n"
|
||||
|
||||
UserAgent := c.Request().UserAgent()
|
||||
|
||||
log.Print(UserAgent)
|
||||
|
||||
match, err := regexp.MatchString("^curl/.*", UserAgent)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if match {
|
||||
return c.String(http.StatusOK, fileUrl)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusMovedPermanently, "/files/"+file.Filename)
|
||||
}
|
||||
|
||||
func Delete(c echo.Context) error {
|
||||
file := c.Param("file")
|
||||
filePath := "files/" + file
|
||||
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return c.String(http.StatusOK, "That file doesn't exist on the server\n")
|
||||
}
|
||||
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return c.String(http.StatusOK, "Error while deleting "+file+"\n")
|
||||
}
|
||||
return c.String(http.StatusOK, "Deleted file from server\n")
|
||||
}
|
148
main.py
148
main.py
@ -1,148 +0,0 @@
|
||||
import shutil
|
||||
import magic
|
||||
import re
|
||||
from typing import Annotated, Union
|
||||
from os import listdir, remove
|
||||
from os.path import abspath, dirname, exists
|
||||
from fastapi import FastAPI, Request, UploadFile, File, Header
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
# Settings
|
||||
class Settings(BaseSettings):
|
||||
upload_only: bool = False
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
|
||||
# file class
|
||||
class _File:
|
||||
def __init__(self, name: str, fileType: str, content=None):
|
||||
self.name = name
|
||||
self.fileType = fileType
|
||||
self.content = content if content is not None else ""
|
||||
|
||||
|
||||
# File list generator with filetype assignemt and content reading for previews
|
||||
def file_list_generator(path: str, file_list: list[str]):
|
||||
files = []
|
||||
for file in file_list:
|
||||
file_type = magic.from_file(f"{path}{file}", mime=True)
|
||||
if re.search("^image/.*", file_type):
|
||||
_file = _File(file, "image")
|
||||
elif re.search("^video/.*", file_type):
|
||||
_file = _File(file, "video")
|
||||
elif re.search("^text/.*", file_type):
|
||||
with open(f"{path}{file}") as f:
|
||||
content = f.read()
|
||||
_file = _File(file, "text", content)
|
||||
elif file_type == "application/json":
|
||||
with open(f"{path}{file}") as f:
|
||||
content = f.read()
|
||||
_file = _File(file, "text", content)
|
||||
else:
|
||||
_file = _File(file, "else")
|
||||
files.append(_file)
|
||||
return files
|
||||
|
||||
|
||||
# Load jinja2 for templating
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
# Create settings
|
||||
settings = Settings()
|
||||
# Create fastapi template
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# Mount all uploaded files at the /files path
|
||||
app.mount("/files", StaticFiles(directory="upload"), name="upload")
|
||||
|
||||
|
||||
# Mount static files like css
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
# show the homepage when just getting the website
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(
|
||||
request: Request, user_agent: Annotated[Union[str, None], Header()] = None
|
||||
):
|
||||
if re.search("^curl/.*", str(user_agent)):
|
||||
return PlainTextResponse("It fucking works!\n")
|
||||
else:
|
||||
context = {
|
||||
"request": request,
|
||||
"user_agent": user_agent,
|
||||
"upload_only": settings.upload_only,
|
||||
}
|
||||
return templates.TemplateResponse("index.html", context)
|
||||
|
||||
|
||||
# get the file user want's to upload and save it
|
||||
@app.post("/")
|
||||
async def upload(
|
||||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
user_agent: Annotated[Union[str, None], Header()] = None,
|
||||
):
|
||||
file_location = f"upload/{file.filename}"
|
||||
if exists(file_location):
|
||||
context = f"""File: "{request.url._url}files/{file.filename}" already exists on the server!
|
||||
Please rename the file or delete the one on the server\n"""
|
||||
return PlainTextResponse(context)
|
||||
with open(file_location, "wb+") as file_object:
|
||||
shutil.copyfileobj(file.file, file_object)
|
||||
if re.search("^curl/.*", str(user_agent)):
|
||||
return PlainTextResponse(f"{request.url._url}files/{file.filename}\n")
|
||||
else:
|
||||
return {
|
||||
"filename": file.filename,
|
||||
"path": f"{request.url._url}files/{file.filename}",
|
||||
}
|
||||
|
||||
|
||||
# show the files page with the list of all files or if run with curl list all files in url format
|
||||
@app.get("/files")
|
||||
async def files(
|
||||
request: Request, user_agent: Annotated[Union[str, None], Header()] = None
|
||||
):
|
||||
path = f"{dirname(abspath(__file__))}/upload/"
|
||||
file_list = listdir(path)
|
||||
files = file_list_generator(path, file_list)
|
||||
context = {"request": request, "files": files, "upload_only": settings.upload_only}
|
||||
if re.search("^curl/.*", str(user_agent)):
|
||||
response = ""
|
||||
for file in files:
|
||||
response += f"{request.url._url}/{file.name}\n"
|
||||
return PlainTextResponse(f"{response}")
|
||||
else:
|
||||
return templates.TemplateResponse("files.html", context)
|
||||
|
||||
|
||||
# delete specific file when getting this request
|
||||
@app.get("/delete/{file}")
|
||||
async def delete(
|
||||
request: Request,
|
||||
file: str,
|
||||
user_agent: Annotated[Union[str, None], Header()] = None,
|
||||
):
|
||||
if settings.upload_only:
|
||||
return PlainTextResponse(
|
||||
"This api endpoint is not available on upload only instance. \
|
||||
If you wan't to delete a file ask the hoster of the instance!!"
|
||||
)
|
||||
else:
|
||||
file_path = f"{dirname(abspath(__file__))}/upload/{file}"
|
||||
if exists(file_path):
|
||||
remove(file_path)
|
||||
if re.search("^curl/.*", str(user_agent)):
|
||||
return PlainTextResponse(f"file {file} deleted from the server\n")
|
||||
return RedirectResponse(request.url_for("files"))
|
||||
if re.search("^curl/.*", str(user_agent)):
|
||||
return PlainTextResponse(f"file {file} doesn't exist on the server\n")
|
||||
return RedirectResponse(request.url_for("files"))
|
32
public/views/files.html
Normal file
32
public/views/files.html
Normal file
@ -0,0 +1,32 @@
|
||||
{{define "files"}}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Files - UpFast</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="/css/files.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>File List</h1>
|
||||
{{range .Files}}
|
||||
<hr />
|
||||
<div class="file">
|
||||
{{if eq .FileType "image"}}
|
||||
<img src="/files/{{.Name}}" alt="{{.Name}}" />
|
||||
{{else if eq .FileType "video"}}
|
||||
<video controls src="/files/{{.Name}}" />
|
||||
{{else if eq .FileType "text"}}
|
||||
<pre>{{.Content}}</pre>
|
||||
{{end}}
|
||||
<div class="info">
|
||||
<a href="/files/{{.Name}}" download>{{.Name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
@ -1,12 +1,12 @@
|
||||
{{define "index"}}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="{{ url_for('static', path='/css/style.css') }}" rel="stylesheet" />
|
||||
<title>UpFast</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="/css/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -19,21 +19,22 @@
|
||||
|
||||
<p>
|
||||
To upload files use curl:
|
||||
<code>curl -F "file=@/path/to/file" "{{ request.url }}"</code>
|
||||
<code>curl -F "file=@/path/to/file" "{{.Host}}"</code>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Or for <a href="https://poggerer.xyz/">Tulg</a> on a windows pc ;)
|
||||
|
||||
<form action="/" method="post" enctype="multipart/form-data">
|
||||
File: <input type="file" name="file"> <input type="submit" value="submit">
|
||||
</form>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To delete a file using curl:
|
||||
<code>curl "{{ request.url }}delete/file_name"</code>
|
||||
<code>curl -X DELETE "{{.Host}}/files/:file"</code>
|
||||
</p>
|
||||
|
||||
{% if upload_only %}
|
||||
<p>
|
||||
NOTE: This is an upload only instance, if you wan't a file on the instance
|
||||
deleted ask the hoster to delete the file.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
To get of all files hosted on the instance check
|
||||
<a href="/files">files</a>
|
||||
@ -51,3 +52,4 @@
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
@ -1,18 +0,0 @@
|
||||
annotated-types==0.6.0
|
||||
anyio==3.7.1
|
||||
click==8.1.7
|
||||
fastapi==0.104.0
|
||||
h11==0.14.0
|
||||
idna==3.4
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
pydantic==2.4.2
|
||||
pydantic-settings==2.0.3
|
||||
pydantic_core==2.10.1
|
||||
python-dotenv==1.0.0
|
||||
python-magic==0.4.27
|
||||
python-multipart==0.0.6
|
||||
sniffio==1.3.0
|
||||
starlette==0.27.0
|
||||
typing_extensions==4.8.0
|
||||
uvicorn==0.23.2
|
@ -1,66 +1,66 @@
|
||||
html {
|
||||
padding-bottom: 10%;
|
||||
padding-bottom: 10%;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
background: #303446;
|
||||
color: #c6d0f5;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
background: #303446;
|
||||
color: #c6d0f5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 250%;
|
||||
text-align: center;
|
||||
font-size: 250%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 40px;
|
||||
color: #626880;
|
||||
margin: 40px;
|
||||
color: #626880;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
max-height: 800px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
max-height: 800px;
|
||||
}
|
||||
|
||||
video {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
max-height: 800px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
max-height: 800px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #f2d5cf;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
color: #f2d5cf;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #292c3c;
|
||||
font-size: 1am;
|
||||
padding: 10px;
|
||||
max-width: 800;
|
||||
max-height: 10rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
background: #292c3c;
|
||||
font-size: 1am;
|
||||
padding: 10px;
|
||||
max-width: 800;
|
||||
max-height: 10rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.file {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: block;
|
||||
text-align: center;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -1,30 +1,29 @@
|
||||
body {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
background: #303446;
|
||||
color: #c6d0f5;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
background: #303446;
|
||||
color: #c6d0f5;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 250%;
|
||||
text-align: center;
|
||||
font-size: 250%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #f2d5cf;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
color: #f2d5cf;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #414559;
|
||||
border-radius: 10px;
|
||||
font-size: 105%;
|
||||
padding: 2px;
|
||||
background: #414559;
|
||||
border-radius: 10px;
|
||||
font-size: 105%;
|
||||
padding: 2px;
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="{{ url_for('static', path='/css/files.css') }}" rel="stylesheet" />
|
||||
<title>UpFast: File List</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>File List</h1>
|
||||
|
||||
{% for file in files %}
|
||||
<hr />
|
||||
<div class="file">
|
||||
{% if file.fileType == "image" %}
|
||||
<img src="/files/{{ file.name }}" />
|
||||
{% elif file.fileType == "video" %}
|
||||
<video controls src="/files/{{ file.name }}"></video>
|
||||
{% elif file.fileType == "text" %}
|
||||
<pre>{{ file.content }}</pre>
|
||||
{% endif %}
|
||||
<div class="info">
|
||||
<a href="/files/{{ file.name }}" download>{{ file.name }}</a>
|
||||
{% if not upload_only %}
|
||||
<a href="/delete/{{ file.name }}" onclick="return confirm('Are you sure?')">delete!</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</body>
|
||||
|
||||
</html>
|
15
upfast-nginx
15
upfast-nginx
@ -1,15 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name upfast.example.com ;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=UpFast service
|
||||
Documentation=https://code.cronyakatsuki.xyz/crony/upfast
|
||||
|
||||
[Service]
|
||||
User=upfast
|
||||
Group=upfast
|
||||
WorkingDirectory=/usr/local/upfast/
|
||||
ExecStart=/usr/local/upfast/env/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips='*'
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Loading…
Reference in New Issue
Block a user