feat: add /files/{file} GET and DELETE methods
This commit is contained in:
parent
7ca416e43e
commit
4a4a0a2149
7 changed files with 252 additions and 60 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
.env
|
.env
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.ash_history/
|
||||||
files/
|
files/
|
||||||
.cache/
|
.cache/
|
||||||
main
|
main
|
||||||
|
|
@ -5,14 +5,14 @@ ARG gid=1000
|
||||||
ARG uid=1000
|
ARG uid=1000
|
||||||
RUN mkdir -p "$APP_ROOT" \
|
RUN mkdir -p "$APP_ROOT" \
|
||||||
&& addgroup --system sfu -g $gid \
|
&& 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"
|
WORKDIR "$APP_ROOT"
|
||||||
USER sfu:sfu
|
USER sfu:sfu
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY go.mod .
|
COPY go.mod .
|
||||||
COPY main.go .
|
COPY *.go ./
|
||||||
COPY logger.go .
|
|
||||||
RUN go build \
|
RUN go build \
|
||||||
&& rm -r go.mod *.go
|
&& rm -r go.mod *.go
|
||||||
|
|
||||||
|
|
|
||||||
39
README.md
Normal file
39
README.md
Normal 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
|
||||||
|
```
|
||||||
|
|
@ -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,10 +18,8 @@ paths:
|
||||||
content:
|
content:
|
||||||
text/plain:
|
text/plain:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: html
|
||||||
|
|
||||||
500:
|
500:
|
||||||
description: "internal server error"
|
description: "internal server error"
|
||||||
|
|
@ -74,8 +72,9 @@ paths:
|
||||||
text/plain:
|
text/plain:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
default: "internal server error"
|
default: "internal server error"
|
||||||
/{file_name}:
|
/files/{file_name}:
|
||||||
get:
|
get:
|
||||||
summary: "get file"
|
summary: "get file"
|
||||||
description: "get file"
|
description: "get file"
|
||||||
|
|
@ -142,3 +141,23 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: "internal server error"
|
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"
|
||||||
|
|
@ -10,7 +10,18 @@ services:
|
||||||
- SFU_FILES_DIR=./files
|
- SFU_FILES_DIR=./files
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/opt/sfu
|
- ./:/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:
|
proxy:
|
||||||
image: caddy
|
image: caddy
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -18,3 +29,6 @@ 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
Normal file
103
handlers.go
Normal 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
114
main.go
|
|
@ -2,44 +2,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SFU_FILES_DIR string = get_envvar_or_fatal("SFU_FILES_DIR")
|
var SFU_FILES_DIR string = getEnvvar("SFU_FILES_DIR")
|
||||||
var SFU_PORT string = get_envvar_or_fatal("SFU_PORT")
|
var SFU_PORT string = getEnvvar("SFU_PORT")
|
||||||
|
|
||||||
func upload_file(w http.ResponseWriter, req *http.Request) {
|
func getEnvvar(envvar_name string) string {
|
||||||
file, fileHeader, err := req.FormFile("file")
|
Info.Printf("getting envvar %v", envvar_name)
|
||||||
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 {
|
|
||||||
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))
|
||||||
|
|
@ -48,31 +21,74 @@ func get_envvar_or_fatal(envvar_name string) string {
|
||||||
return envvar_value
|
return envvar_value
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func emptyArray(s []string) []string {
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
var r []string
|
||||||
path := req.URL.Path
|
for _, str := range s {
|
||||||
Info.Println(fmt.Sprintf("received %v on %v", req.Method, path))
|
if str != "" {
|
||||||
if path != "/" {
|
r = append(r, str)
|
||||||
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 {
|
}
|
||||||
|
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:
|
case http.MethodGet:
|
||||||
list_uploaded_files(w, req)
|
listFiles(w, r)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
upload_file(w, req)
|
uploadFile(w, r)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
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))
|
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)
|
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, nil)
|
err := http.ListenAndServe(port, mux)
|
||||||
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