diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..573ef88 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,99 @@ +### JetBrains+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 +.idea/ +*.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 + +### Git template +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt +.git/ +.gitignore + +## Docker Compose +docker-compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f38c6f9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM golang:1.24.1-alpine3.21 AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +# Specifica il path del main package +RUN CGO_ENABLED=0 GOOS=linux go build -o api ./cmd/fileserver + +FROM alpine:latest + +WORKDIR /app + +COPY --from=builder /app/api . +COPY --from=builder /app/config/*.json ./config/ + +RUN ls -laR . + +RUN apk --no-cache add ca-certificates + +EXPOSE 8080 + +CMD ["./api"] diff --git a/cmd/fileserver/main.go b/cmd/fileserver/main.go index e7c6777..0890aef 100644 --- a/cmd/fileserver/main.go +++ b/cmd/fileserver/main.go @@ -3,55 +3,56 @@ package main import ( "fileserver/config" "fileserver/internal/api" + "fileserver/internal/utils" "fmt" "log" "net/http" "os" - "reflect" - "runtime" ) func main() { + // Defer a function to catch any runtime panics and log them. + // This helps in recovering from unexpected fatal errors. defer func() { if r := recover(); r != nil { - log.Fatalf("Catch fatal error: %v\n", r) + log.Fatalf("Catch fatal error: %v\n", r) // Log the fatal error if panic occurs } }() - profile := defaultValue(os.Getenv("APP_PROFILE"), "prod") + + // Get the application profile from environment variables or default to "prod" if not set. + profile := utils.DefaultValue(os.Getenv("APP_PROFILE"), "prod") + + // Initialize the configuration for the application based on the profile. if err := config.Initialize(profile); err != nil { + // If an error occurs during initialization, log the error and terminate the application. log.Fatalf("Error to read %s configuration: %v\n", profile, err) } + + // Log the profile that is being used to start the application. log.Printf("Application starting with profile: %s", profile) - // Register all routes in the new ServeMux + + // Create a new HTTP request multiplexer (ServeMux) to register routes. mux := http.NewServeMux() log.Printf("Register all routes\n") + + // Iterate through the routes defined in the API package and register them. for url, handler := range api.Routes { - log.Printf("Register route %s for %v", url, getFunctionName(handler)) + // For each route, log the URL and corresponding handler function name. + log.Printf("Register route %s for %v", url, utils.GetFunctionName(handler)) + // Register the route and associate it with the handler function. mux.HandleFunc(url, handler) } + + // Get the server configuration from the app's config settings. server := config.App.Server + // Format the server's host and port into a string for the URL. url := fmt.Sprintf("%s:%d", server.Host, server.Port) + // Log the server's URL where it will be listening. log.Printf("Start server on %s\n", url) + + // Start the HTTP server using the specified host and port, and pass in the mux for routing. + // If an error occurs while starting the server, log it and terminate the program. 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 index e61ddd9..cb5af9d 100644 --- a/config/application-dev.json +++ b/config/application-dev.json @@ -2,5 +2,18 @@ "server": { "host": "localhost", "port": 8081 + }, + "database": { + "driver": "postgres", + "host": "localhost", + "port": 5432, + "name": "fileserver", + "username": "postgres", + "password": "postgres" + }, + "minio": { + "url": "localhost:9000", + "username": "minioadmin", + "password": "minioadmin" } } diff --git a/config/application.json b/config/application.json index f6787bc..af91c3e 100644 --- a/config/application.json +++ b/config/application.json @@ -2,5 +2,17 @@ "server": { "host": "localhost", "port": 8080 + }, + "database": { + "driver": "postgres", + "url": "pgfileserver", + "name": "fileserver", + "username": "postgres", + "password": "postgres" + }, + "minio": { + "url": "miniofs", + "username": "minioadmin", + "password": "minioadmin" } } diff --git a/config/config.go b/config/config.go index 43f5b87..9c674ad 100644 --- a/config/config.go +++ b/config/config.go @@ -2,40 +2,107 @@ package config import ( "encoding/json" + "fileserver/internal/utils" "fmt" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" "os" ) +// Application represents the top-level structure of the application's configuration. type Application struct { - Server Server `json:"server"` + Server *Server `json:"server"` // Server configuration + Database *Database `json:"database"` // Database configuration + Minio *Minio `json:"minio"` // MinIO configuration } +// Server holds the configuration related to the web server (e.g., host, port). type Server struct { - Host string `json:"host"` - Port int `json:"port"` + Host string `json:"host"` // Hostname or IP address for the server + Port int `json:"port"` // Port number on which the server will run } -var App Application +// Database holds the configuration for connecting to a database (e.g., Postgres or SQLite). +type Database struct { + Url string `json:"url"` // Database URL (used in case of SQLite) + Driver string `json:"driver"` // Database driver (e.g., "postgres" or "sqlite") + Host string `json:"host"` // Hostname of the database server (used in case of PostgreSQL) + Port int `json:"port"` // Port number for the database connection + Name string `json:"name"` // Database name + Username string `json:"username"` // Database username + Password string `json:"password"` // Database password + SSLMode bool `json:"ssl-mode"` // Whether SSL is enabled for the connection + Timezone string `json:"timezone"` // Timezone for the database connection +} -const configDir = "config" +// Minio holds the configuration for connecting to a MinIO server. +type Minio struct { + Url string `json:"url"` // MinIO server URL + Username string `json:"username"` // MinIO username + Password string `json:"password"` // MinIO password + Token string `json:"token"` // Optional token for MinIO authentication + Secure bool `json:"secure"` // Whether the connection is secure (HTTPS) + Region string `json:"region"` // MinIO server region + BucketLookup int `json:"bucketLookup"` // Bucket lookup strategy +} +// Global variables for the application configuration and clients. +var ( + App Application // Application-level configuration + DB *gorm.DB // Database client (GORM) + MinIO *minio.Client // MinIO client +) + +const ( + configDir = "config" // Directory where the configuration files are stored +) + +// Initialize reads the configuration file based on the profile (dev, test, prod), +// and initializes the MinIO and database clients based on the configuration. func Initialize(profile string) error { + // Get the file path based on the profile filename, err := checkProfileAndGetFilePath(profile) if err != nil { - return fmt.Errorf("errore checking profile: %v", err) + return fmt.Errorf("error checking profile: %v", err) } + + // Read the configuration file content, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("error reading file: %v", err) } + + // Unmarshal the JSON content into the Application structure if err = json.Unmarshal(content, &App); err != nil { return fmt.Errorf("error unmarshaling JSON: %v", err) } + + // Initialize MinIO if MinIO configuration is provided + if App.Minio != nil { + if err := initializeMinIO(App.Minio); err != nil { + return fmt.Errorf("error initializing MinIO: %v", err) + } + fmt.Println("MinIO initialized") + } + + // Initialize database if database configuration is provided + if App.Database != nil { + if err := initializeDatabase(App.Database); err != nil { + return fmt.Errorf("error initializing database: %v", err) + } + fmt.Println("Database initialized") + } + return nil } +// checkProfileAndGetFilePath returns the correct configuration file path based on the profile (dev, test, prod). func checkProfileAndGetFilePath(profile string) (string, error) { var filename string + // Match the profile to its respective configuration file switch { case profile == "dev": filename = fmt.Sprintf("%s/application-dev.json", configDir) @@ -44,7 +111,93 @@ func checkProfileAndGetFilePath(profile string) (string, error) { case profile == "prod": filename = fmt.Sprintf("%s/application.json", configDir) default: - return "", fmt.Errorf("profile %s is not valid value", profile) + return "", fmt.Errorf("profile %s is not valid", profile) } return filename, nil } + +// initializeMinIO initializes the MinIO client using the provided configuration. +func initializeMinIO(minioConfig *Minio) error { + // Create a MinIO client with the given credentials and options + client, err := minio.New(minioConfig.Url, &minio.Options{ + Creds: credentials.NewStaticV4(minioConfig.Username, minioConfig.Password, minioConfig.Token), + Secure: minioConfig.Secure, + Region: minioConfig.Region, + BucketLookup: getBucketLookup(minioConfig.BucketLookup), + }) + if err != nil { + return fmt.Errorf("cannot connect to MinIO %s: %v", minioConfig.Url, err) + } + MinIO = client + return nil +} + +// getBucketLookup maps the integer value to the appropriate MinIO bucket lookup type. +func getBucketLookup(value int) minio.BucketLookupType { + switch value { + case 0: + return minio.BucketLookupAuto + case 1: + return minio.BucketLookupDNS + case 2: + return minio.BucketLookupPath + default: + return minio.BucketLookupAuto + } +} + +// initializeDatabase initializes the database client based on the provided configuration. +func initializeDatabase(dbConfig *Database) error { + // Generate the database connection string based on the driver + switch dbConfig.Driver { + case "postgres": + var url string + if dbConfig.Url != "" { + url = fmt.Sprintf( + "postgres://%s:%s@%s/%s?sslmode=%s&TimeZone=%s", + dbConfig.Username, + dbConfig.Password, + dbConfig.Url, + utils.DefaultValue(dbConfig.Name, "postgres"), + getSSLModeValue(dbConfig.SSLMode), + utils.DefaultValue(dbConfig.Timezone, "UTC"), + ) + } else { + url = fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s", + dbConfig.Host, + dbConfig.Username, + dbConfig.Password, + utils.DefaultValue(dbConfig.Name, "postgres"), + dbConfig.Port, + getSSLModeValue(dbConfig.SSLMode), + utils.DefaultValue(dbConfig.Timezone, "UTC"), + ) + } + + // Open PostgreSQL connection with GORM + db, err := gorm.Open(postgres.Open(url), &gorm.Config{}) + if err != nil { + return fmt.Errorf("cannot connect to database %s@%s:%d", dbConfig.Username, dbConfig.Host, dbConfig.Port) + } + DB = db + case "sqlite": + // Open SQLite connection with GORM + db, err := gorm.Open(sqlite.Open(dbConfig.Url), &gorm.Config{}) + if err != nil { + return fmt.Errorf("cannot connect to database %s", dbConfig.Url) + } + DB = db + default: + return fmt.Errorf("database type is not supported") + } + return nil +} + +// getSSLModeValue returns "enable" or "disable" based on the boolean value for SSL mode. +func getSSLModeValue(mode bool) string { + if !mode { + return "disable" + } + return "enable" +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..fc1f9aa --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,50 @@ +version: '3.9' + +services: + postgres: + image: postgres:15 + container_name: pgfileserver + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: fileserver + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + networks: + - backend_net + + minio: + image: minio/minio:latest + container_name: miniofs + restart: unless-stopped + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + command: server /data --console-address ":9001" + ports: + - "9000:9000" # API + - "9001:9001" # Console + volumes: + - minio_data:/data + networks: + - backend_net + + fileserver: + image: fileserver:latest + container_name: fileserver + restart: always + ports: + - "8080:8081" + networks: + - backend_net + +volumes: + pg_data: + minio_data: + +networks: + backend_net: + driver: bridge diff --git a/go.mod b/go.mod index ca5fa2d..461e6c9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,36 @@ module fileserver go 1.24 + +require ( + github.com/google/uuid v1.6.0 + github.com/minio/minio-go/v7 v7.0.92 + gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.30.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.5 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/minio/crc64nvme v1.0.2 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2743355 --- /dev/null +++ b/go.sum @@ -0,0 +1,69 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg= +github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw= +github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/internal/api/file_handler.go b/internal/api/file_handler.go index 11cb01b..25fece4 100644 --- a/internal/api/file_handler.go +++ b/internal/api/file_handler.go @@ -1,30 +1,104 @@ package api import ( + "context" + "fileserver/internal/service" "fmt" + "github.com/google/uuid" "io" "mime/multipart" "net/http" "os" - "strings" "time" ) +// Constants for default bucket name and folder path for uploaded files +const ( + defaultBucketName = "documents" // Default bucket name in MinIO + localFolderTemplate = "%s/fileserver/uploads/" // Template for creating local upload directories +) + +// GetFile handles the request to fetch a file from MinIO and serve it to the user. +func GetFile(w http.ResponseWriter, r *http.Request) { + // Ensure that the request method is GET + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Extract the object name from the query parameters + objectName := r.URL.Query().Get("file") + // Fetch the file object from MinIO storage + object, err := service.GetFileFromMinIO(defaultBucketName, objectName) + if err != nil { + return + } + defer object.Close() // Ensure that the file object is closed after use + + // Create the upload directory if it doesn't exist + uploadDir := fmt.Sprintf(localFolderTemplate, os.TempDir()) + err = os.MkdirAll(uploadDir, os.ModePerm) + if err != nil { + http.Error(w, "Error creating the uploads folder", http.StatusInternalServerError) + return + } + + // Create a new file locally with a unique name (using a timestamp) + newFileName := fmt.Sprintf("%d_%s", time.Now().Unix(), objectName) + file, err := os.Create(uploadDir + newFileName) + if err != nil { + http.Error(w, "Error saving the file: "+err.Error(), http.StatusInternalServerError) + return + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + http.Error(w, "Error closing file: "+err.Error(), http.StatusInternalServerError) + } + }(file) + + // Copy the file content from MinIO to the local file + _, err = io.Copy(file, object) + if err != nil { + fmt.Println("Error saving object to file:", err) + return + } + + // Get the file's information (size, name, etc.) + fileInfo, err := file.Stat() + if err != nil { + http.Error(w, "Could not get file information", http.StatusInternalServerError) + return + } + + // Set headers for file download (name, content type, and length) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; objectName=%s", fileInfo.Name())) + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) + + // Log the successful file retrieval + fmt.Printf("Sending file: %s (Size: %d bytes)\n", fileInfo.Name(), fileInfo.Size()) + + // Serve the file content as a download + http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file) +} + +// LoadFile handles file uploads from a client and stores them locally and on MinIO. func LoadFile(w http.ResponseWriter, r *http.Request) { - // Make sure the request is a POST and is of type multipart/form-data + // Ensure that the request method is POST and that it is a multipart form if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - // Limit the maximum request size (e.g., 10 MB) + // Parse the multipart form data with a maximum file size of 10MB 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 + // Retrieve the uploaded file from the form file, header, err := r.FormFile("file") if err != nil { http.Error(w, "Error retrieving the file: "+err.Error(), http.StatusInternalServerError) @@ -37,22 +111,17 @@ func LoadFile(w http.ResponseWriter, r *http.Request) { } }(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) + // Create the upload directory if it doesn't exist + uploadDir := fmt.Sprintf(localFolderTemplate, os.TempDir()) + err = os.MkdirAll(uploadDir, os.ModePerm) if err != nil { http.Error(w, "Error creating the uploads folder", http.StatusInternalServerError) return } - // Use a unique name for the file (timestamp) + // Use a unique file name based on timestamp and the original file name newFileName := fmt.Sprintf("%d_%s", time.Now().Unix(), header.Filename) - out, err := os.Create("./uploads/" + newFileName) + out, err := os.Create(uploadDir + newFileName) if err != nil { http.Error(w, "Error saving the file: "+err.Error(), http.StatusInternalServerError) return @@ -62,15 +131,51 @@ func LoadFile(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, "Error closing the output file: "+err.Error(), http.StatusInternalServerError) } + if err := cleanup(out); err != nil { + http.Error(w, "Error remove the output file: "+err.Error(), http.StatusInternalServerError) + } }(out) - // Copy the file contents from the request to the saved file + // Copy the file content from the request to the local 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) + // Upload the file to MinIO with a unique ID (UUID) + idFile := uuid.New().String() + err = service.UploadFileToMinIO(context.Background(), defaultBucketName, idFile, uploadDir+newFileName) + if err != nil { + http.Error(w, "", http.StatusInternalServerError) + return + } + + // Respond to the client with a success message + _, err = fmt.Fprintf(w, "File %s uploaded successfully!\n", newFileName) + if err != nil { + return + } +} + +// cleanup removes a file from the local file system after use. +func cleanup(file *os.File) error { + if _, err := os.Stat(file.Name()); err == nil { + err := os.Remove(file.Name()) + if err != nil { + return fmt.Errorf("Error removing file: %v", err) + } else { + fmt.Println("File removed successfully:", file.Name()) + } + } else if os.IsNotExist(err) { + return fmt.Errorf("File does not exist: %v", file.Name()) + } else { + fmt.Println("Error checking file:", err) + } + return nil +} + +// DeleteFile handles the file deletion logic (currently empty). +func DeleteFile(w http.ResponseWriter, r *http.Request) { + // This function is a placeholder for file deletion logic. } diff --git a/internal/api/routes.go b/internal/api/routes.go index 8a80716..5fee5e1 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -3,6 +3,8 @@ package api import "net/http" var Routes = map[string]func(w http.ResponseWriter, r *http.Request){ - "/": Hello, - "/file": LoadFile, + "GET /": Hello, + "GET /file": GetFile, + "POST /file": LoadFile, + "DELETE /file": DeleteFile, } diff --git a/internal/models/document.go b/internal/models/document.go new file mode 100644 index 0000000..235ca93 --- /dev/null +++ b/internal/models/document.go @@ -0,0 +1,24 @@ +package models + +import ( + "github.com/google/uuid" + "gorm.io/gorm" + "time" +) + +// Document represents the structure of the documents table in the database. +type Document struct { + ID uint `gorm:"primaryKey"` // Primary key for the document + Name string `gorm:"column:name"` // Name of the document + IdFile uuid.UUID `gorm:"type:uuid;column:id_file;unique"` // Unique identifier for the document's file + Fingerprint string `gorm:"column:fingerprint;unique"` // Unique fingerprint (hash) for the document + CreatedAt time.Time `gorm:"column:created_at"` // Timestamp of when the document was created + UpdatedAt time.Time `gorm:"column:updated_at"` // Timestamp of when the document was last updated + DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at"` // Timestamp for soft deletion (if applicable) +} + +// TableName overrides the default table name used by GORM. +func (Document) TableName() string { + // Returns the name of the table where documents are stored + return "documents" +} diff --git a/internal/service/document_service.go b/internal/service/document_service.go new file mode 100644 index 0000000..d4ac047 --- /dev/null +++ b/internal/service/document_service.go @@ -0,0 +1,41 @@ +package service + +import ( + "errors" + "fileserver/config" + "fileserver/internal/models" + "fmt" + "github.com/google/uuid" + "gorm.io/gorm" +) + +// GetDocument retrieves a document from the database based on its `idFile` field. +// It searches for a document with the given `idFile` and returns the document if found, or an error if not. +func GetDocument(idFile uuid.UUID) (*models.Document, error) { + var document models.Document + + // Perform the query to find the document by its unique `idFile` field + if err := config.DB.Where("id_file = ?", idFile).First(&document).Error; err != nil { + // If no record is found, return a descriptive error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("document with idFile %v not found", idFile) + } + // If there is another error during retrieval, return the error + return nil, fmt.Errorf("error while retrieving document: %v", err) + } + + // Return the document found in the database + return &document, nil +} + +// AddDocument adds a new document to the database. +// The function receives a pointer to a `Document` struct and attempts to insert it into the database. +func AddDocument(document *models.Document) error { + // Create a new record for the document in the database + if err := config.DB.Create(document).Error; err != nil { + // If an error occurs during the insert, return the error + return fmt.Errorf("error while adding document: %v", err) + } + // If the operation is successful, return nil (no error) + return nil +} diff --git a/internal/service/minio_service.go b/internal/service/minio_service.go new file mode 100644 index 0000000..5c4430e --- /dev/null +++ b/internal/service/minio_service.go @@ -0,0 +1,85 @@ +package service + +import ( + "context" + "fileserver/config" + "fmt" + "github.com/minio/minio-go/v7" + "os" +) + +// GetFileFromMinIO retrieves a file from the specified MinIO bucket. +func GetFileFromMinIO(bucketName, objectName string) (*minio.Object, error) { + // Fetch the object from MinIO using the provided bucket name and object name + object, err := config.MinIO.GetObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{}) + if err != nil { + // Return error if there is any issue in fetching the object + return nil, fmt.Errorf("error getting object from MinIO: %v", err) + } + // Return the fetched object if successful + return object, nil +} + +// UploadFileToMinIO uploads a file to MinIO under the specified bucket and object name. +func UploadFileToMinIO(ctx context.Context, bucketName, objectName, filePath string) error { + // Open the file from the given file path + file, err := os.Open(filePath) + if err != nil { + // Return error if unable to open the file + return fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() // Ensure file is closed after use + + // Check if the bucket exists, create it if not + if err = createBucketIfNotExists(ctx, bucketName); err != nil { + // Return error if bucket creation fails + return fmt.Errorf("failed to create bucket: %v", err) + } + + // Upload the file to MinIO + _, err = config.MinIO.PutObject(ctx, bucketName, objectName, file, -1, minio.PutObjectOptions{ContentType: "application/octet-stream"}) + if err != nil { + // Return error if uploading the file fails + return fmt.Errorf("failed to upload file: %v", err) + } + // Return nil if the file is successfully uploaded + return nil +} + +// createBucketIfNotExists checks if the specified bucket exists and creates it if it doesn't. +func createBucketIfNotExists(ctx context.Context, bucketName string) error { + // Check if the bucket already exists + exists, err := config.MinIO.BucketExists(ctx, bucketName) + if err != nil { + // Return error if checking the bucket existence fails + return fmt.Errorf("failed to check if bucket exists: %v", err) + } + + // If the bucket doesn't exist, create it + if !exists { + fmt.Println("Bucket does not exist. Creating bucket...") + // Create the bucket with the specified region + err = config.MinIO.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: "us-east-1"}) + if err != nil { + // Return error if bucket creation fails + return fmt.Errorf("failed to create bucket: %v", err) + } + fmt.Println("Bucket created successfully!") + } + // Return nil if bucket already exists or is created successfully + return nil +} + +// DeleteFileFromMinIO removes a file from the specified MinIO bucket. +func DeleteFileFromMinIO(ctx context.Context, bucketName, objectName string) error { + // Remove the object from the MinIO bucket + err := config.MinIO.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{}) + if err != nil { + // Return error if deleting the object fails + return fmt.Errorf("error deleting object from MinIO: %v", err) + } + // Log success message after deletion + fmt.Println("File deleted successfully") + // Return nil if file is deleted successfully + return nil +} diff --git a/internal/utils/fingerprint.go b/internal/utils/fingerprint.go new file mode 100644 index 0000000..d5b22a4 --- /dev/null +++ b/internal/utils/fingerprint.go @@ -0,0 +1,57 @@ +package utils + +import ( + "crypto/sha1" + "fmt" + "io" + "log" + "os" +) + +// CalculateFingerprint calculates the fingerprint (SHA-1 hash) of a file at a given path. +// +// This function computes a SHA-1 hash for the contents of a file. It opens the file, reads it in chunks +// to avoid loading the entire file into memory, and then calculates the hash using the SHA-1 algorithm. +// Finally, it returns the resulting hash as a hexadecimal string. +// +// Parameters: +// - filePath (string): The path to the file whose fingerprint (hash) is to be calculated. +// +// Returns: +// - string: The SHA-1 hash of the file, represented as a hexadecimal string. +// - error: Any error encountered while opening the file, reading it, or calculating the hash. +// If no error occurred, it returns nil. +// +// Example usage: +// +// fingerprint, err := utils.CalculateFingerprint("/path/to/file.txt") +// if err != nil { +// log.Fatalf("Error calculating fingerprint: %v", err) +// } +// fmt.Printf("Fingerprint: %s\n", fingerprint) +func CalculateFingerprint(filePath string) (string, error) { + // Open the file at the specified file path. + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("failed to open file: %v", err) + } + // Ensure that the file is closed after processing (using a defer statement). + defer func(file *os.File) { + err := file.Close() + if err != nil { + log.Fatalf("failed to close file: %v", err) + } + }(file) + + // Create a new SHA-1 hash object. + hash := sha1.New() + + // Read the file and calculate the hash while reading. The entire file is not loaded into memory. + _, err = io.Copy(hash, file) + if err != nil { + return "", fmt.Errorf("failed to calculate hash: %v", err) + } + + // Return the final hash as a hexadecimal string. + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} diff --git a/internal/utils/strings.go b/internal/utils/strings.go new file mode 100644 index 0000000..979fd8a --- /dev/null +++ b/internal/utils/strings.go @@ -0,0 +1,55 @@ +package utils + +import ( + "reflect" + "runtime" +) + +// GetFunctionName returns the name of a function from its reference. +// +// This function uses reflection to get the function's pointer and +// retrieves its name using the runtime package. It can be used to +// dynamically obtain the name of a function at runtime. +// +// Parameters: +// - fn (any): A reference to the function whose name you want to retrieve. +// +// Returns: +// - string: The name of the function, typically in the form of "packageName.funcName". +// +// Example usage: +// +// func example() {} +// name := utils.GetFunctionName(example) // Returns "main.example" +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() +} + +// DefaultValue checks if a given value is non-empty and returns it. +// If the value is empty, it returns a fallback (default) value. +// +// Parameters: +// - value (string): The value to check for non-emptiness. +// - other (string): The fallback value to return if the input `value` is empty. +// +// Returns: +// - string: The `value` if it is non-empty, otherwise the `other` value. +// +// Example usage: +// +// result := utils.DefaultValue("", "default") // Returns "default" +// result := utils.DefaultValue("custom", "default") // Returns "custom" +func DefaultValue(value string, other string) string { + // Return `value` if it is not empty, otherwise return `other` + if value != "" { + return value + } + return other +} diff --git a/scripts/database/db.sql b/scripts/database/db.sql new file mode 100644 index 0000000..84a58d9 --- /dev/null +++ b/scripts/database/db.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS documents +( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + id_file UUID UNIQUE NOT NULL, + fingerprint TEXT UNIQUE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITHOUT TIME ZONE +); + +-- Crea un indice su deleted_at per il supporto soft delete +CREATE INDEX IF NOT EXISTS idx_documents_deleted_at ON documents (deleted_at);