Added db configurations and Dockerfile
This commit is contained in:
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user