Added db configurations and Dockerfile

This commit is contained in:
Fabio Scotto di Santolo
2025-06-03 18:17:47 +02:00
parent 0c17486015
commit eab9c25885
17 changed files with 889 additions and 51 deletions

99
.dockerignore Normal file
View File

@@ -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

26
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}

50
docker-compose.yaml Normal file
View File

@@ -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

33
go.mod
View File

@@ -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
)

69
go.sum Normal file
View File

@@ -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=

View File

@@ -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.
}

View File

@@ -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,
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

55
internal/utils/strings.go Normal file
View File

@@ -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
}

13
scripts/database/db.sql Normal file
View File

@@ -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);