diff --git a/.gitignore b/.gitignore index e6b17a7..402782d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ .env .vscode/ -.ash_history/ files/ -.cache/ main \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cda8681..5b8fe3e 100644 --- a/Dockerfile +++ b/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 -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 ./ +COPY main.go . +COPY logger.go . + RUN go build \ && rm -r go.mod *.go - -FROM build AS run_prod -ENTRYPOINT [ "/bin/sh", "-c", "$APP_ROOT/main"] - -FROM base AS run_dev -ENTRYPOINT [ "/usr/local/go/bin/go", "run", "." ] +ENTRYPOINT [ "/bin/sh", "-c", "$APP_ROOT/main"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index c2a337c..0000000 --- a/README.md +++ /dev/null @@ -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 -``` \ No newline at end of file diff --git a/design/openapi.yml b/design/openapi.yml index 099b95a..9538001 100644 --- a/design/openapi.yml +++ b/design/openapi.yml @@ -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,9 +18,11 @@ paths: content: text/plain: schema: - type: string - format: html - + type: array + items: + type: string + format: uri + 500: description: "internal server error" content: @@ -72,9 +74,8 @@ paths: text/plain: schema: type: string - default: "internal server error" - /files/{file_name}: + /{file_name}: get: summary: "get file" description: "get file" @@ -134,26 +135,6 @@ 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: diff --git a/docker-compose.yml b/docker-compose.yml index a9346a4..c6b60e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,24 +4,11 @@ services: app: build: context: . - target: run_dev - environment: - - SFU_PORT=80 - - 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 + - "${PWD}/files:${SFU_FILES_DIR},z" + env_file: + - .env + proxy: image: caddy volumes: @@ -29,6 +16,3 @@ services: - ./design:/usr/share/caddy/www/design ports: - '8080:80' - depends_on: - app: - condition: service_healthy diff --git a/handlers.go b/handlers.go deleted file mode 100644 index 348a913..0000000 --- a/handlers.go +++ /dev/null @@ -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, "
    ") - for _, f := range files { - - fmt.Fprintf(w, "
  1. %v
  2. \n", f.Name(), f.Name()) - } - fmt.Fprint(w, "
") -} diff --git a/main.go b/main.go index b673228..e408be7 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,42 @@ package main import ( "fmt" + "io" + "io/ioutil" "net/http" "os" - "regexp" - "strings" ) -var SFU_FILES_DIR string = getEnvvar("SFU_FILES_DIR") -var SFU_PORT string = getEnvvar("SFU_PORT") +var SFU_FILES_DIR string = get_envvar_or_fatal("SFU_FILES_DIR") +var SFU_PORT string = get_envvar_or_fatal("SFU_PORT") -func getEnvvar(envvar_name string) string { - Info.Printf("getting envvar %v", envvar_name) +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) + } + 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) if !isSet { Error.Println(fmt.Sprintf("%v is not set", envvar_name)) @@ -21,74 +46,31 @@ func getEnvvar(envvar_name string) string { 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() { - mux := http.NewServeMux() - mux.HandleFunc("/", route) + 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 + } + 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) Info.Println(fmt.Sprintf("running SFU on port %v", port)) - err := http.ListenAndServe(port, mux) + err := http.ListenAndServe(port, nil) if err != nil { Error.Println(fmt.Sprintf("%v port may not be available", port)) os.Exit(1)