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
|
||||
.vscode/
|
||||
.ash_history/
|
||||
files/
|
||||
.cache/
|
||||
main
|
||||
|
|
@ -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
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/
|
||||
description: local server
|
||||
paths:
|
||||
/:
|
||||
/files:
|
||||
get:
|
||||
summary: "list all uploaded files"
|
||||
description: "list all uploaded files"
|
||||
|
|
@ -18,11 +18,9 @@ paths:
|
|||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uri
|
||||
|
||||
type: string
|
||||
format: html
|
||||
|
||||
500:
|
||||
description: "internal server error"
|
||||
content:
|
||||
|
|
@ -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"
|
||||
|
|
@ -135,6 +134,26 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
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:
|
||||
description: "internal server error"
|
||||
content:
|
||||
|
|
|
|||
|
|
@ -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
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 (
|
||||
"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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue