Added db configurations and Dockerfile
This commit is contained in:
99
.dockerignore
Normal file
99
.dockerignore
Normal 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
26
Dockerfile
Normal 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"]
|
||||||
@@ -3,55 +3,56 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fileserver/config"
|
"fileserver/config"
|
||||||
"fileserver/internal/api"
|
"fileserver/internal/api"
|
||||||
|
"fileserver/internal/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Defer a function to catch any runtime panics and log them.
|
||||||
|
// This helps in recovering from unexpected fatal errors.
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
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 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.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)
|
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()
|
mux := http.NewServeMux()
|
||||||
log.Printf("Register all routes\n")
|
log.Printf("Register all routes\n")
|
||||||
|
|
||||||
|
// Iterate through the routes defined in the API package and register them.
|
||||||
for url, handler := range api.Routes {
|
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)
|
mux.HandleFunc(url, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the server configuration from the app's config settings.
|
||||||
server := config.App.Server
|
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)
|
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)
|
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 {
|
if err := http.ListenAndServe(url, mux); err != nil {
|
||||||
log.Fatalf("%v\n", err)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,5 +2,18 @@
|
|||||||
"server": {
|
"server": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 8081
|
"port": 8081
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"driver": "postgres",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"name": "fileserver",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "postgres"
|
||||||
|
},
|
||||||
|
"minio": {
|
||||||
|
"url": "localhost:9000",
|
||||||
|
"username": "minioadmin",
|
||||||
|
"password": "minioadmin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,17 @@
|
|||||||
"server": {
|
"server": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 8080
|
"port": 8080
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"driver": "postgres",
|
||||||
|
"url": "pgfileserver",
|
||||||
|
"name": "fileserver",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "postgres"
|
||||||
|
},
|
||||||
|
"minio": {
|
||||||
|
"url": "miniofs",
|
||||||
|
"username": "minioadmin",
|
||||||
|
"password": "minioadmin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
167
config/config.go
167
config/config.go
@@ -2,40 +2,107 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fileserver/internal/utils"
|
||||||
"fmt"
|
"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"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Application represents the top-level structure of the application's configuration.
|
||||||
type Application struct {
|
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 {
|
type Server struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"` // Hostname or IP address for the server
|
||||||
Port int `json:"port"`
|
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 {
|
func Initialize(profile string) error {
|
||||||
|
// Get the file path based on the profile
|
||||||
filename, err := checkProfileAndGetFilePath(profile)
|
filename, err := checkProfileAndGetFilePath(profile)
|
||||||
if err != nil {
|
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)
|
content, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error reading file: %v", err)
|
return fmt.Errorf("error reading file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unmarshal the JSON content into the Application structure
|
||||||
if err = json.Unmarshal(content, &App); err != nil {
|
if err = json.Unmarshal(content, &App); err != nil {
|
||||||
return fmt.Errorf("error unmarshaling JSON: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkProfileAndGetFilePath returns the correct configuration file path based on the profile (dev, test, prod).
|
||||||
func checkProfileAndGetFilePath(profile string) (string, error) {
|
func checkProfileAndGetFilePath(profile string) (string, error) {
|
||||||
var filename string
|
var filename string
|
||||||
|
// Match the profile to its respective configuration file
|
||||||
switch {
|
switch {
|
||||||
case profile == "dev":
|
case profile == "dev":
|
||||||
filename = fmt.Sprintf("%s/application-dev.json", configDir)
|
filename = fmt.Sprintf("%s/application-dev.json", configDir)
|
||||||
@@ -44,7 +111,93 @@ func checkProfileAndGetFilePath(profile string) (string, error) {
|
|||||||
case profile == "prod":
|
case profile == "prod":
|
||||||
filename = fmt.Sprintf("%s/application.json", configDir)
|
filename = fmt.Sprintf("%s/application.json", configDir)
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("profile %s is not valid value", profile)
|
return "", fmt.Errorf("profile %s is not valid", profile)
|
||||||
}
|
}
|
||||||
return filename, nil
|
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
50
docker-compose.yaml
Normal 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
33
go.mod
@@ -1,3 +1,36 @@
|
|||||||
module fileserver
|
module fileserver
|
||||||
|
|
||||||
go 1.24
|
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
69
go.sum
Normal 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=
|
||||||
@@ -1,30 +1,104 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fileserver/internal/service"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"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) {
|
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 {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
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
|
err := r.ParseMultipartForm(10 << 20) // 10 MB
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error parsing the request", http.StatusBadRequest)
|
http.Error(w, "Error parsing the request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the file from the 'file' field of the form
|
// Retrieve the uploaded file from the form
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error retrieving the file: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Error retrieving the file: "+err.Error(), http.StatusInternalServerError)
|
||||||
@@ -37,22 +111,17 @@ func LoadFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}(file)
|
}(file)
|
||||||
|
|
||||||
// Check the file extension (example)
|
// Create the upload directory if it doesn't exist
|
||||||
if !strings.HasSuffix(header.Filename, ".txt") {
|
uploadDir := fmt.Sprintf(localFolderTemplate, os.TempDir())
|
||||||
http.Error(w, "Unsupported file type", http.StatusBadRequest)
|
err = os.MkdirAll(uploadDir, os.ModePerm)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create uploads folder if it doesn't exist
|
|
||||||
err = os.MkdirAll("./uploads", os.ModePerm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error creating the uploads folder", http.StatusInternalServerError)
|
http.Error(w, "Error creating the uploads folder", http.StatusInternalServerError)
|
||||||
return
|
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)
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "Error saving the file: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Error saving the file: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -62,15 +131,51 @@ func LoadFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error closing the output file: "+err.Error(), http.StatusInternalServerError)
|
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)
|
}(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)
|
_, err = io.Copy(out, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error copying the file", http.StatusInternalServerError)
|
http.Error(w, "Error copying the file", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respond with a success message
|
// Upload the file to MinIO with a unique ID (UUID)
|
||||||
fmt.Fprintf(w, "File %s uploaded successfully!", newFileName)
|
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.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package api
|
|||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
var Routes = map[string]func(w http.ResponseWriter, r *http.Request){
|
var Routes = map[string]func(w http.ResponseWriter, r *http.Request){
|
||||||
"/": Hello,
|
"GET /": Hello,
|
||||||
"/file": LoadFile,
|
"GET /file": GetFile,
|
||||||
|
"POST /file": LoadFile,
|
||||||
|
"DELETE /file": DeleteFile,
|
||||||
}
|
}
|
||||||
|
|||||||
24
internal/models/document.go
Normal file
24
internal/models/document.go
Normal 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"
|
||||||
|
}
|
||||||
41
internal/service/document_service.go
Normal file
41
internal/service/document_service.go
Normal 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
|
||||||
|
}
|
||||||
85
internal/service/minio_service.go
Normal file
85
internal/service/minio_service.go
Normal 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
|
||||||
|
}
|
||||||
57
internal/utils/fingerprint.go
Normal file
57
internal/utils/fingerprint.go
Normal 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
55
internal/utils/strings.go
Normal 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
13
scripts/database/db.sql
Normal 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);
|
||||||
Reference in New Issue
Block a user