diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef40fab --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ +### GoLand+all template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..18d7b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..c2621b8 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..df5f35d --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9715c22 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d4c7d02 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cmd/fileserver/main.go b/cmd/fileserver/main.go new file mode 100644 index 0000000..3f062a0 --- /dev/null +++ b/cmd/fileserver/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fileserver/config" + "fileserver/internal/api" + "fmt" + "log" + "net/http" + "os" + "reflect" + "runtime" +) + +func main() { + defer func() { + if r := recover(); r != nil { + log.Fatalf("Catch fatal error: %v\n", r) + } + }() + profile := defaultValue(os.Getenv("APP_PROFILE"), "prod") + if err := config.Initialize(profile); err != nil { + log.Fatalf("Error to read %s configuration: %v\n", profile, err) + } + log.Printf("Application starting with profile: %s", profile) + server := config.App.Server + mux := http.NewServeMux() + // Register all routes in the new ServeMux + log.Printf("Register all routes\n") + for url, handler := range api.Routes { + log.Printf("Register route %s for %v", url, getFunctionName(handler)) + mux.HandleFunc(url, handler) + } + url := fmt.Sprintf("%s:%d", server.Host, server.Port) + log.Printf("Start server on %s\n", url) + if err := http.ListenAndServe(url, mux); err != nil { + log.Fatalf("%v\n", err) + } +} + +// Function to get the name of a function from its reference +func getFunctionName(fn any) string { + // Get the pointer to the function using reflection + pc := reflect.ValueOf(fn).Pointer() + + // Use the pointer to get the function object + funcObj := runtime.FuncForPC(pc) + + // Return the name of the function + return funcObj.Name() +} + +func defaultValue(value string, other string) string { + if value != "" { + return value + } + return other +} diff --git a/config/application-dev.json b/config/application-dev.json new file mode 100644 index 0000000..f6787bc --- /dev/null +++ b/config/application-dev.json @@ -0,0 +1,6 @@ +{ + "server": { + "host": "localhost", + "port": 8080 + } +} diff --git a/config/application.json b/config/application.json new file mode 100644 index 0000000..26decab --- /dev/null +++ b/config/application.json @@ -0,0 +1,6 @@ +{ + "server": { + "host": "localhost", + "port": 8089 + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..7f09972 --- /dev/null +++ b/config/config.go @@ -0,0 +1,49 @@ +package config + +import ( + "encoding/json" + "fmt" + "log" + "os" +) + +type Application struct { + Server Server `json:"server"` +} + +type Server struct { + Host string `json:"host"` + Port int `json:"port"` +} + +var App Application + +const configDir = "config" + +func Initialize(profile string) error { + filename := checkProfileAndGetFilePath(profile) + content, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("error reading file: %v", err) + } + + if err = json.Unmarshal(content, &App); err != nil { + return fmt.Errorf("error unmarshaling JSON: %v", err) + } + return nil +} + +func checkProfileAndGetFilePath(profile string) string { + var filename string + switch { + case profile == "dev": + filename = fmt.Sprintf("%s/application-dev.json", configDir) + case profile == "test": + filename = fmt.Sprintf("%s/application-test.json", configDir) + case profile == "prod": + filename = fmt.Sprintf("%s/application.json", configDir) + default: + log.Fatalf("Profile %s is not valid value", profile) + } + return filename +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca5fa2d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module fileserver + +go 1.24 diff --git a/internal/api/file_handler.go b/internal/api/file_handler.go new file mode 100644 index 0000000..11cb01b --- /dev/null +++ b/internal/api/file_handler.go @@ -0,0 +1,76 @@ +package api + +import ( + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "strings" + "time" +) + +func LoadFile(w http.ResponseWriter, r *http.Request) { + // Make sure the request is a POST and is of type multipart/form-data + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Limit the maximum request size (e.g., 10 MB) + err := r.ParseMultipartForm(10 << 20) // 10 MB + if err != nil { + http.Error(w, "Error parsing the request", http.StatusBadRequest) + return + } + + // Retrieve the file from the 'file' field of the form + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "Error retrieving the file: "+err.Error(), http.StatusInternalServerError) + return + } + defer func(file multipart.File) { + err := file.Close() + if err != nil { + http.Error(w, "Error closing the input file: "+err.Error(), http.StatusInternalServerError) + } + }(file) + + // Check the file extension (example) + if !strings.HasSuffix(header.Filename, ".txt") { + http.Error(w, "Unsupported file type", http.StatusBadRequest) + return + } + + // Create uploads folder if it doesn't exist + err = os.MkdirAll("./uploads", os.ModePerm) + if err != nil { + http.Error(w, "Error creating the uploads folder", http.StatusInternalServerError) + return + } + + // Use a unique name for the file (timestamp) + newFileName := fmt.Sprintf("%d_%s", time.Now().Unix(), header.Filename) + out, err := os.Create("./uploads/" + newFileName) + if err != nil { + http.Error(w, "Error saving the file: "+err.Error(), http.StatusInternalServerError) + return + } + defer func(out *os.File) { + err := out.Close() + if err != nil { + http.Error(w, "Error closing the output file: "+err.Error(), http.StatusInternalServerError) + } + }(out) + + // Copy the file contents from the request to the saved file + _, err = io.Copy(out, file) + if err != nil { + http.Error(w, "Error copying the file", http.StatusInternalServerError) + return + } + + // Respond with a success message + fmt.Fprintf(w, "File %s uploaded successfully!", newFileName) +} diff --git a/internal/api/routes.go b/internal/api/routes.go new file mode 100644 index 0000000..8a80716 --- /dev/null +++ b/internal/api/routes.go @@ -0,0 +1,8 @@ +package api + +import "net/http" + +var Routes = map[string]func(w http.ResponseWriter, r *http.Request){ + "/": Hello, + "/file": LoadFile, +} diff --git a/internal/api/welcome.go b/internal/api/welcome.go new file mode 100644 index 0000000..fede4df --- /dev/null +++ b/internal/api/welcome.go @@ -0,0 +1,10 @@ +package api + +import "net/http" + +func Hello(w http.ResponseWriter, r *http.Request) { + if _, err := w.Write([]byte("Welcome to my homepage")); err != nil { + return + } + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/.gitkeep b/pkg/.gitkeep new file mode 100644 index 0000000..e69de29