feat: add /files/{file} GET and DELETE methods

This commit is contained in:
cătălin 2022-06-28 14:11:37 +02:00
commit 4a4a0a2149
No known key found for this signature in database
GPG key ID: C378F1E869F05A95
7 changed files with 252 additions and 60 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.env
.vscode/
.ash_history/
files/
.cache/
main

View file

@ -5,14 +5,14 @@ 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
&& 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
COPY go.mod .
COPY main.go .
COPY logger.go .
COPY *.go ./
RUN go build \
&& rm -r go.mod *.go

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# 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
```

View file

@ -7,7 +7,7 @@ servers:
- url: http://localhost:8080/api/v1/
description: local server
paths:
/:
/files:
get:
summary: "list all uploaded files"
description: "list all uploaded files"
@ -18,10 +18,8 @@ paths:
content:
text/plain:
schema:
type: array
items:
type: string
format: uri
type: string
format: html
500:
description: "internal server error"
@ -74,8 +72,9 @@ paths:
text/plain:
schema:
type: string
default: "internal server error"
/{file_name}:
/files/{file_name}:
get:
summary: "get file"
description: "get file"
@ -142,3 +141,23 @@ paths:
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:
description: "internal server error"
content:
text/plain:
schema:
type: string
default: "internal server error"

View file

@ -10,7 +10,18 @@ services:
- SFU_FILES_DIR=./files
volumes:
- ./:/opt/sfu
healthcheck:
test: "curl http://localhost:$SFU_PORT/health"
interval: 1s
timeout: 1s
retries: 3
start_period: 1s
deploy:
resources:
limits:
cpus: "0.5"
memory: "200M"
restart: always
proxy:
image: caddy
volumes:
@ -18,3 +29,6 @@ services:
- ./design:/usr/share/caddy/www/design
ports:
- '8080:80'
depends_on:
app:
condition: service_healthy

103
handlers.go Normal file
View file

@ -0,0 +1,103 @@
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>")
}

114
main.go
View file

@ -2,44 +2,17 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
)
var SFU_FILES_DIR string = get_envvar_or_fatal("SFU_FILES_DIR")
var SFU_PORT string = get_envvar_or_fatal("SFU_PORT")
var SFU_FILES_DIR string = getEnvvar("SFU_FILES_DIR")
var SFU_PORT string = getEnvvar("SFU_PORT")
func upload_file(w http.ResponseWriter, req *http.Request) {
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)
}
fmt.Fprint(w, "<html><body><ol>")
for _, f := range files {
fmt.Fprintf(w, "<li><a href='foo.barz'>%v</a></li>\n", f.Name())
}
fmt.Fprint(w, "</ol></body></html>")
}
func get_envvar_or_fatal(envvar_name string) string {
func getEnvvar(envvar_name string) string {
Info.Printf("getting envvar %v", envvar_name)
envvar_value, isSet := os.LookupEnv(envvar_name)
if !isSet {
Error.Println(fmt.Sprintf("%v is not set", envvar_name))
@ -48,31 +21,74 @@ func get_envvar_or_fatal(envvar_name string) string {
return envvar_value
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
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
func emptyArray(s []string) []string {
var r []string
for _, str := range s {
if str != "" {
r = append(r, str)
}
switch req.Method {
}
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:
list_uploaded_files(w, req)
listFiles(w, r)
case http.MethodPost:
upload_file(w, req)
uploadFile(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
Warning.Println(fmt.Sprintf("%v not allowed", req.Method))
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() {
mux := http.NewServeMux()
mux.HandleFunc("/", route)
port := fmt.Sprintf(":%v", SFU_PORT)
Info.Println(fmt.Sprintf("running SFU on port %v", port))
err := http.ListenAndServe(port, nil)
err := http.ListenAndServe(port, mux)
if err != nil {
Error.Println(fmt.Sprintf("%v port may not be available", port))
os.Exit(1)