Compare commits
No commits in common. "4a4a0a2149508573aeaf3c8d5e96080d42816594" and "02ed6be314c2b50a6987a5171467b7a5465f237a" have entirely different histories.
4a4a0a2149
...
02ed6be314
7 changed files with 65 additions and 271 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,6 +1,4 @@
|
||||||
.env
|
.env
|
||||||
.vscode/
|
.vscode/
|
||||||
.ash_history/
|
|
||||||
files/
|
files/
|
||||||
.cache/
|
|
||||||
main
|
main
|
||||||
25
Dockerfile
25
Dockerfile
|
|
@ -1,23 +1,14 @@
|
||||||
FROM golang:1.18.3-alpine3.16 AS base
|
FROM golang:1.18.3-alpine3.16
|
||||||
|
|
||||||
ENV APP_ROOT /opt/sfu
|
ENV APP_ROOT /opt/sfu
|
||||||
ARG gid=1000
|
|
||||||
ARG uid=1000
|
|
||||||
RUN mkdir -p "$APP_ROOT" \
|
|
||||||
&& addgroup --system sfu -g $gid \
|
|
||||||
&& adduser -h "$APP_ROOT" --disabled-password --system -u $uid --ingroup sfu sfu \
|
|
||||||
&& apk add curl~=7
|
|
||||||
WORKDIR "$APP_ROOT"
|
|
||||||
USER sfu:sfu
|
|
||||||
|
|
||||||
FROM base AS build
|
RUN mkdir -p "$APP_ROOT"
|
||||||
|
WORKDIR "$APP_ROOT"
|
||||||
|
|
||||||
COPY go.mod .
|
COPY go.mod .
|
||||||
COPY *.go ./
|
COPY main.go .
|
||||||
|
COPY logger.go .
|
||||||
|
|
||||||
RUN go build \
|
RUN go build \
|
||||||
&& rm -r go.mod *.go
|
&& rm -r go.mod *.go
|
||||||
|
ENTRYPOINT [ "/bin/sh", "-c", "$APP_ROOT/main"]
|
||||||
FROM build AS run_prod
|
|
||||||
ENTRYPOINT [ "/bin/sh", "-c", "$APP_ROOT/main"]
|
|
||||||
|
|
||||||
FROM base AS run_dev
|
|
||||||
ENTRYPOINT [ "/usr/local/go/bin/go", "run", "." ]
|
|
||||||
39
README.md
39
README.md
|
|
@ -1,39 +0,0 @@
|
||||||
# sfu
|
|
||||||
|
|
||||||
simple requirementsless, authenticationless file upload server
|
|
||||||
|
|
||||||
## prod version
|
|
||||||
|
|
||||||
WIP
|
|
||||||
|
|
||||||
## dev version
|
|
||||||
|
|
||||||
### using docker
|
|
||||||
|
|
||||||
1. take a look at [docker-compose](docker-compose.yml) and modify the envvars accordingly. the default values **should work** as long as you're ok with a `files/` folder being created and **your user UID and GID are 1000**. if for some reason you need other ids, please add them like this:
|
|
||||||
|
|
||||||
```docker-compose
|
|
||||||
...
|
|
||||||
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
target: run_dev
|
|
||||||
environment:
|
|
||||||
- SFU_PORT=80
|
|
||||||
- SFU_FILES_DIR=./files
|
|
||||||
- gid=1234
|
|
||||||
- uid=1234
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### dockerless
|
|
||||||
|
|
||||||
1. sfu needs two envvars to be set
|
|
||||||
- `SFU_PORT`: the port to listen on
|
|
||||||
- `SFU_FILES_DIR`: the directory to store files in
|
|
||||||
2. run the server
|
|
||||||
```shell
|
|
||||||
go build
|
|
||||||
SFU_PORT=80 SFU_FILES_DIR=./files ./main
|
|
||||||
```
|
|
||||||
|
|
@ -7,7 +7,7 @@ servers:
|
||||||
- url: http://localhost:8080/api/v1/
|
- url: http://localhost:8080/api/v1/
|
||||||
description: local server
|
description: local server
|
||||||
paths:
|
paths:
|
||||||
/files:
|
/:
|
||||||
get:
|
get:
|
||||||
summary: "list all uploaded files"
|
summary: "list all uploaded files"
|
||||||
description: "list all uploaded files"
|
description: "list all uploaded files"
|
||||||
|
|
@ -18,9 +18,11 @@ paths:
|
||||||
content:
|
content:
|
||||||
text/plain:
|
text/plain:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: array
|
||||||
format: html
|
items:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
|
||||||
500:
|
500:
|
||||||
description: "internal server error"
|
description: "internal server error"
|
||||||
content:
|
content:
|
||||||
|
|
@ -72,9 +74,8 @@ paths:
|
||||||
text/plain:
|
text/plain:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
default: "internal server error"
|
default: "internal server error"
|
||||||
/files/{file_name}:
|
/{file_name}:
|
||||||
get:
|
get:
|
||||||
summary: "get file"
|
summary: "get file"
|
||||||
description: "get file"
|
description: "get file"
|
||||||
|
|
@ -134,26 +135,6 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: "file not found"
|
default: "file not found"
|
||||||
500:
|
|
||||||
description: "internal server error"
|
|
||||||
content:
|
|
||||||
text/plain:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: "internal server error"
|
|
||||||
/health:
|
|
||||||
get:
|
|
||||||
summary: "health check"
|
|
||||||
description: "health check"
|
|
||||||
operationId: "health_check"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "health check successful"
|
|
||||||
content:
|
|
||||||
text/plain:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: "health check successful"
|
|
||||||
500:
|
500:
|
||||||
description: "internal server error"
|
description: "internal server error"
|
||||||
content:
|
content:
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,11 @@ services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: run_dev
|
|
||||||
environment:
|
|
||||||
- SFU_PORT=80
|
|
||||||
- SFU_FILES_DIR=./files
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/opt/sfu
|
- "${PWD}/files:${SFU_FILES_DIR},z"
|
||||||
healthcheck:
|
env_file:
|
||||||
test: "curl http://localhost:$SFU_PORT/health"
|
- .env
|
||||||
interval: 1s
|
|
||||||
timeout: 1s
|
|
||||||
retries: 3
|
|
||||||
start_period: 1s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "0.5"
|
|
||||||
memory: "200M"
|
|
||||||
restart: always
|
|
||||||
proxy:
|
proxy:
|
||||||
image: caddy
|
image: caddy
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -29,6 +16,3 @@ services:
|
||||||
- ./design:/usr/share/caddy/www/design
|
- ./design:/usr/share/caddy/www/design
|
||||||
ports:
|
ports:
|
||||||
- '8080:80'
|
- '8080:80'
|
||||||
depends_on:
|
|
||||||
app:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
|
||||||
103
handlers.go
103
handlers.go
|
|
@ -1,103 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func downloadFile(w http.ResponseWriter, req *http.Request, fileName string) {
|
|
||||||
Info.Println(fmt.Sprintf("server: will download %v", fileName))
|
|
||||||
file, err := os.Open(fmt.Sprintf("%v/%v", SFU_FILES_DIR, fileName))
|
|
||||||
if err != nil {
|
|
||||||
Error.Println(err)
|
|
||||||
http.Error(w, "File not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%v", fileName))
|
|
||||||
http.ServeContent(w, req, fileName, time.Now(), file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(fileName string) bool {
|
|
||||||
_, err := os.Stat(fmt.Sprintf("%v/%v", SFU_FILES_DIR, fileName))
|
|
||||||
if err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteFile(w http.ResponseWriter, req *http.Request, fileName string) {
|
|
||||||
Info.Println(fmt.Sprintf("server: will delete %v", fileName))
|
|
||||||
if !fileExists(fileName) {
|
|
||||||
Error.Println(fmt.Sprintf("%v does not exist", fileName))
|
|
||||||
http.Error(w, "File not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := os.Remove(fmt.Sprintf("%v/%v", SFU_FILES_DIR, fileName))
|
|
||||||
if err != nil {
|
|
||||||
Error.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("file deleted"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func uploadFile(w http.ResponseWriter, req *http.Request) {
|
|
||||||
file, fileHeader, err := req.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
Error.Println(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
fileExists := fileExists(fileHeader.Filename)
|
|
||||||
forceOverwrite := req.FormValue("force")
|
|
||||||
if fileExists && forceOverwrite != "true" {
|
|
||||||
Error.Println(fmt.Sprintf("file %v already exists", fileHeader.Filename))
|
|
||||||
http.Error(w, "File already exists", http.StatusConflict)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !fileExists || (forceOverwrite == "true" && fileExists) {
|
|
||||||
Info.Println(fmt.Sprintf("server: will upload %v", fileHeader.Filename))
|
|
||||||
out, err := os.Create(fmt.Sprintf("%v/%v", SFU_FILES_DIR, fileHeader.Filename))
|
|
||||||
if err != nil {
|
|
||||||
Error.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
_, err = io.Copy(out, file)
|
|
||||||
if err != nil {
|
|
||||||
Error.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
w.Write([]byte("file uploaded"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func listFiles(w http.ResponseWriter, req *http.Request) {
|
|
||||||
Info.Println(fmt.Sprintf("server: will list uploaded files on %v", SFU_FILES_DIR))
|
|
||||||
files, err := ioutil.ReadDir(SFU_FILES_DIR)
|
|
||||||
if err != nil {
|
|
||||||
Warning.Println(fmt.Sprintf("%v does not exist", SFU_FILES_DIR))
|
|
||||||
Info.Println(fmt.Sprintf("will create %v", SFU_FILES_DIR))
|
|
||||||
_ = os.Mkdir(SFU_FILES_DIR, os.ModePerm)
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "<html><body><ol>")
|
|
||||||
for _, f := range files {
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "<li><a href=\"%v\">%v</a></li>\n", f.Name(), f.Name())
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "</ol></body></html>")
|
|
||||||
}
|
|
||||||
124
main.go
124
main.go
|
|
@ -2,17 +2,42 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var SFU_FILES_DIR string = getEnvvar("SFU_FILES_DIR")
|
var SFU_FILES_DIR string = get_envvar_or_fatal("SFU_FILES_DIR")
|
||||||
var SFU_PORT string = getEnvvar("SFU_PORT")
|
var SFU_PORT string = get_envvar_or_fatal("SFU_PORT")
|
||||||
|
|
||||||
func getEnvvar(envvar_name string) string {
|
func upload_file(w http.ResponseWriter, req *http.Request) {
|
||||||
Info.Printf("getting envvar %v", envvar_name)
|
file, fileHeader, err := req.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
Error.Println(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
f, err := os.OpenFile(fmt.Sprintf("%v/%v", SFU_FILES_DIR, fileHeader.Filename), os.O_WRONLY|os.O_CREATE, 0666)
|
||||||
|
defer f.Close()
|
||||||
|
io.Copy(f, file)
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func list_uploaded_files(w http.ResponseWriter, req *http.Request) {
|
||||||
|
Info.Println(fmt.Sprintf("server: will list uploaded files on %v", SFU_FILES_DIR))
|
||||||
|
files, err := ioutil.ReadDir(SFU_FILES_DIR)
|
||||||
|
if err != nil {
|
||||||
|
Warning.Println(fmt.Sprintf("%v does not exist", SFU_FILES_DIR))
|
||||||
|
Info.Println(fmt.Sprintf("will create %v", SFU_FILES_DIR))
|
||||||
|
_ = os.Mkdir(SFU_FILES_DIR, os.ModePerm)
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
fmt.Fprintf(w, "%v\n", f.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_envvar_or_fatal(envvar_name string) string {
|
||||||
envvar_value, isSet := os.LookupEnv(envvar_name)
|
envvar_value, isSet := os.LookupEnv(envvar_name)
|
||||||
if !isSet {
|
if !isSet {
|
||||||
Error.Println(fmt.Sprintf("%v is not set", envvar_name))
|
Error.Println(fmt.Sprintf("%v is not set", envvar_name))
|
||||||
|
|
@ -21,74 +46,31 @@ func getEnvvar(envvar_name string) string {
|
||||||
return envvar_value
|
return envvar_value
|
||||||
}
|
}
|
||||||
|
|
||||||
func emptyArray(s []string) []string {
|
|
||||||
var r []string
|
|
||||||
for _, str := range s {
|
|
||||||
if str != "" {
|
|
||||||
r = append(r, str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func routeFiles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path
|
|
||||||
Info.Println(fmt.Sprintf("received %v on %v", r.Method, path))
|
|
||||||
paths := emptyArray(strings.Split(path, "/"))
|
|
||||||
Info.Println(fmt.Sprintf("paths %v", paths))
|
|
||||||
switch len(paths) {
|
|
||||||
case 1:
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
listFiles(w, r)
|
|
||||||
case http.MethodPost:
|
|
||||||
uploadFile(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
Warning.Println(fmt.Sprintf("%v not allowed", r.Method))
|
|
||||||
Error.Println(fmt.Sprintf("will return %v", http.StatusMethodNotAllowed))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case 2:
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
downloadFile(w, r, paths[1])
|
|
||||||
case http.MethodDelete:
|
|
||||||
deleteFile(w, r, paths[1])
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
Warning.Println(fmt.Sprintf("%v not allowed", r.Method))
|
|
||||||
Error.Println(fmt.Sprintf("will return %v", http.StatusMethodNotAllowed))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
http.Error(w, "Not found", http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func route(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path
|
|
||||||
switch {
|
|
||||||
case path == "/":
|
|
||||||
fmt.Fprintf(w, "Hello, world!")
|
|
||||||
case path == "/health":
|
|
||||||
Info.Println("health check OK")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("OK"))
|
|
||||||
case regexp.MustCompile("^/files").MatchString(path):
|
|
||||||
Info.Println("received request for files, will call files router")
|
|
||||||
routeFiles(w, r)
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
mux := http.NewServeMux()
|
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
mux.HandleFunc("/", route)
|
path := req.URL.Path
|
||||||
|
Info.Println(fmt.Sprintf("received %v on %v", req.Method, path))
|
||||||
|
if path != "/" {
|
||||||
|
err_msg := fmt.Sprintf("path %v does not exist", path)
|
||||||
|
http.Error(w, err_msg, http.StatusNotFound)
|
||||||
|
Warning.Println(err_msg)
|
||||||
|
Error.Println(fmt.Sprintf("will return %v", http.StatusNotFound))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch req.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
list_uploaded_files(w, req)
|
||||||
|
case http.MethodPost:
|
||||||
|
upload_file(w, req)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
Warning.Println(fmt.Sprintf("%v not allowed", req.Method))
|
||||||
|
Error.Println(fmt.Sprintf("will return %v", http.StatusMethodNotAllowed))
|
||||||
|
}
|
||||||
|
})
|
||||||
port := fmt.Sprintf(":%v", SFU_PORT)
|
port := fmt.Sprintf(":%v", SFU_PORT)
|
||||||
Info.Println(fmt.Sprintf("running SFU on port %v", port))
|
Info.Println(fmt.Sprintf("running SFU on port %v", port))
|
||||||
err := http.ListenAndServe(port, mux)
|
err := http.ListenAndServe(port, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error.Println(fmt.Sprintf("%v port may not be available", port))
|
Error.Println(fmt.Sprintf("%v port may not be available", port))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue