diff --git a/.gitignore b/.gitignore index 402782d..e6b17a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .env .vscode/ +.ash_history/ files/ +.cache/ main \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5b8fe3e..cda8681 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,23 @@ -FROM golang:1.18.3-alpine3.16 +FROM golang:1.18.3-alpine3.16 AS base ENV APP_ROOT /opt/sfu - -RUN mkdir -p "$APP_ROOT" +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 COPY go.mod . -COPY main.go . -COPY logger.go . - +COPY *.go ./ RUN go build \ && rm -r go.mod *.go -ENTRYPOINT [ "/bin/sh", "-c", "$APP_ROOT/main"] \ No newline at end of file + +FROM build AS run_prod +ENTRYPOINT [ "/bin/sh", "-c", "$APP_ROOT/main"] + +FROM base AS run_dev +ENTRYPOINT [ "/usr/local/go/bin/go", "run", "." ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2a337c --- /dev/null +++ b/README.md @@ -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 +``` \ No newline at end of file diff --git a/design/openapi.yml b/design/openapi.yml index 9538001..099b95a 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,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: diff --git a/docker-compose.yml b/docker-compose.yml index c6b60e7..a9346a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,11 +4,24 @@ services: app: build: context: . + target: run_dev + environment: + - SFU_PORT=80 + - SFU_FILES_DIR=./files volumes: - - "${PWD}/files:${SFU_FILES_DIR},z" - env_file: - - .env - + - ./:/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: @@ -16,3 +29,6 @@ services: - ./design:/usr/share/caddy/www/design ports: - '8080:80' + depends_on: + app: + condition: service_healthy diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..348a913 --- /dev/null +++ b/handlers.go @@ -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, "