From 32f31d44d28e09efe3c692eb738e9d183d987e57 Mon Sep 17 00:00:00 2001 From: Fabio Scotto di Santolo Date: Wed, 4 Jun 2025 12:25:48 +0200 Subject: [PATCH] Added APIs for delete and get all documents --- internal/api/file_handler.go | 115 +++++++++++++++++++++++++-- internal/api/routes.go | 9 ++- internal/service/document_service.go | 105 +++++++++++++++++++++++- internal/service/minio_service.go | 35 ++++++++ 4 files changed, 251 insertions(+), 13 deletions(-) diff --git a/internal/api/file_handler.go b/internal/api/file_handler.go index 25fece4..3482040 100644 --- a/internal/api/file_handler.go +++ b/internal/api/file_handler.go @@ -2,6 +2,9 @@ package api import ( "context" + "encoding/json" + "fileserver/config" + "fileserver/internal/models" "fileserver/internal/service" "fmt" "github.com/google/uuid" @@ -18,6 +21,37 @@ const ( localFolderTemplate = "%s/fileserver/uploads/" // Template for creating local upload directories ) +// GetFiles retrieves the list of indexed documents from the database with fuzzy search on file names +func GetFiles(w http.ResponseWriter, r *http.Request) { + // Step 1: Retrieve the search query from the URL parameters + searchQuery := r.URL.Query().Get("searchQuery") + if searchQuery == "" { + // If there is no search query, retrieve all documents + searchQuery = "%" + } else { + // Add wildcards for partial search + searchQuery = "%" + searchQuery + "%" + } + + // Step 2: Retrieve documents whose name matches the fuzzy search + documents, err := service.GetFiles(searchQuery) + if err != nil { + // Handle error if the query fails + http.Error(w, fmt.Sprintf("Error retrieving documents: %v", err), http.StatusInternalServerError) + return + } + + // Step 3: Convert the documents to JSON format + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Use json.NewEncoder to write the response directly in JSON format + if err := json.NewEncoder(w).Encode(documents); err != nil { + http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) + return + } +} + // 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 @@ -27,7 +61,19 @@ func GetFile(w http.ResponseWriter, r *http.Request) { } // Extract the object name from the query parameters - objectName := r.URL.Query().Get("file") + objectName := r.PathValue("idFile") + idFile, err := uuid.Parse(objectName) + if err != nil { + http.Error(w, fmt.Sprintf("Error parsing objectName: %v", err), http.StatusBadRequest) + return + } + + document, err := service.GetDocument(idFile) + if document == nil || err != nil { + http.Error(w, fmt.Sprintf("Error retrieving document: %v", err), http.StatusNotFound) + return + } + // Fetch the file object from MinIO storage object, err := service.GetFileFromMinIO(defaultBucketName, objectName) if err != nil { @@ -121,7 +167,8 @@ func LoadFile(w http.ResponseWriter, r *http.Request) { // 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(uploadDir + newFileName) + filePath := uploadDir + newFileName + out, err := os.Create(filePath) if err != nil { http.Error(w, "Error saving the file: "+err.Error(), http.StatusInternalServerError) return @@ -143,11 +190,37 @@ func LoadFile(w http.ResponseWriter, r *http.Request) { return } + // Calculate fingerprint of file + //fingerprint, err := utils.CalculateFingerprint(filePath) + //if err != nil { + // http.Error(w, "Error during calculate fingerprint: "+err.Error(), http.StatusInternalServerError) + // return + //} + fingerprint := uuid.New().String() + + // Check if document already uploaded + _, err = service.GetDocumentByFingerprint(fingerprint) + if err == nil { + http.Error(w, "Document already exists.", http.StatusConflict) + return + } + // Upload the file to MinIO with a unique ID (UUID) - idFile := uuid.New().String() - err = service.UploadFileToMinIO(context.Background(), defaultBucketName, idFile, uploadDir+newFileName) + idFile := uuid.New() + err = service.UploadFileToMinIO(context.Background(), defaultBucketName, idFile.String(), filePath) if err != nil { - http.Error(w, "", http.StatusInternalServerError) + http.Error(w, "Error during upload file to MinIO: "+err.Error(), http.StatusInternalServerError) + return + } + + // Save document to database + newDocument := &models.Document{ + Name: header.Filename, + IdFile: idFile, + Fingerprint: fingerprint, + } + if err := service.AddDocument(newDocument); err != nil { + http.Error(w, "Error adding document: "+err.Error(), http.StatusInternalServerError) return } @@ -175,7 +248,35 @@ func cleanup(file *os.File) error { return nil } -// DeleteFile handles the file deletion logic (currently empty). +// DeleteFile deletes a file from the database and MinIO func DeleteFile(w http.ResponseWriter, r *http.Request) { - // This function is a placeholder for file deletion logic. + // Ensure that the request method is GET + if r.Method != http.MethodDelete { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Extract the object name from the path value + idFile, err := uuid.Parse(r.PathValue("idFile")) + if err != nil { + http.Error(w, "Error parsing the idFile: "+err.Error(), http.StatusBadRequest) + return + } + + // Step 1: Get document from PostgreSQL database + document, err := service.GetDocument(idFile) + if err != nil { + http.Error(w, "Document not found: "+err.Error(), http.StatusNotFound) + return + } + + // Step 2: Delete from PostgreSQL + if err := config.DB.Delete(&document).Error; err != nil { + http.Error(w, fmt.Sprintf("Error deleting document from DB: %v", err), http.StatusInternalServerError) + return + } + + // Return success response + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "File with ID %v deleted successfully", idFile) } diff --git a/internal/api/routes.go b/internal/api/routes.go index 5fee5e1..bbc9109 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -3,8 +3,9 @@ package api import "net/http" var Routes = map[string]func(w http.ResponseWriter, r *http.Request){ - "GET /": Hello, - "GET /file": GetFile, - "POST /file": LoadFile, - "DELETE /file": DeleteFile, + "GET /": Hello, + "GET /files": GetFiles, + "GET /file/{idFile}": GetFile, + "POST /file": LoadFile, + "DELETE /file/{idFile}": DeleteFile, } diff --git a/internal/service/document_service.go b/internal/service/document_service.go index d4ac047..eb62c23 100644 --- a/internal/service/document_service.go +++ b/internal/service/document_service.go @@ -9,13 +9,48 @@ import ( "gorm.io/gorm" ) +// GetFiles retrieves a list of documents from the database based on a fuzzy search on file names. +// It only returns documents that have not been logically deleted (i.e., deleted_at is NULL). +// The function performs a case-insensitive search using the provided search query. +// +// Parameters: +// - searchQuery (string): The search term used to find documents by their file name. This will be used +// in a fuzzy search with the 'ILIKE' operator in PostgreSQL. +// +// Returns: +// - []models.Document: A slice of documents that match the search query and are not logically deleted. +// - error: An error is returned if there is an issue with retrieving the documents from the database. +func GetFiles(searchQuery string) ([]models.Document, error) { + // Declare a slice to hold the results of the query + var documents []models.Document + + // Perform the query to find documents where: + // - 'deleted_at' is NULL (i.e., the document has not been logically deleted) + // - The file name matches the search query using a case-insensitive pattern match ('ILIKE') + if err := config.DB.Where("deleted_at IS NULL AND name ILIKE ?", searchQuery).Find(&documents).Error; err != nil { + // If there is an error during the query execution, return an empty slice and the error message + return documents, fmt.Errorf("error retrieving documents: %v", err) + } + + // Return the list of documents and nil error if the query was successful + return documents, nil +} + // 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. +// It searches for a document with the given `idFile` and returns the document if found, +// or an error if not. +// +// Parameters: +// - idFile (uuid.UUID): The unique identifier of the document to retrieve. +// +// Returns: +// - *models.Document: A pointer to the document if found. +// - error: An error is returned if the document is not found or there is a database issue. 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 err := config.DB.Where("deleted_at IS NULL AND 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) @@ -28,8 +63,40 @@ func GetDocument(idFile uuid.UUID) (*models.Document, error) { return &document, nil } +// GetDocumentByFingerprint retrieves a document from the database based on its unique fingerprint. +// It returns the document if found, or an error if not found or if any database-related issues occur. +// +// Parameters: +// - fingerprint (string): The unique fingerprint of the document to retrieve. +// +// Returns: +// - *models.Document: A pointer to the `Document` struct if the document is found. +// - error: An error if the document is not found or if there is a failure during the query. +func GetDocumentByFingerprint(fingerprint string) (*models.Document, error) { + var document models.Document + + // Perform the query to find the document by its unique fingerprint + if err := config.DB.Where("fingerprint = ?", fingerprint).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 fingerprint %v not found", fingerprint) + } + // If there is another error during retrieval, return the error + return nil, fmt.Errorf("error while retrieving document: %v", err) + } + + // Return the document if found + 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. +// +// Parameters: +// - document (*models.Document): A pointer to the document to add to the database. +// +// Returns: +// - error: Returns an error if there is an issue during the insertion, or nil if successful. 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 { @@ -39,3 +106,37 @@ func AddDocument(document *models.Document) error { // If the operation is successful, return nil (no error) return nil } + +// DeleteDocument deletes a document from the database by its associated idFile. +// This function searches for a document by `idFile`, and if found, deletes it from the database. +// +// Parameters: +// - idFile (uuid.UUID): The unique identifier of the document to delete. +// +// Returns: +// - error: Returns an error if the document is not found or if there is a failure during deletion. +func DeleteDocument(idFile uuid.UUID) error { + // Declare a variable to hold the document from the database. + var document models.Document + + // Retrieve the document using the provided idFile. + // The 'Where' clause filters by the 'id_file' field. + // 'First' retrieves the first matching record (if any). + if err := config.DB.Where("id_file = ?", idFile).First(&document).Error; err != nil { + // If the record is not found, return a custom error. + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("document with idFile %v not found", idFile) + } + // For any other error (e.g., database connection issues), return a generic error. + return fmt.Errorf("error while fetching document: %v", err) + } + + // If document is found, proceed to delete it. + if err := config.DB.Delete(&document).Error; err != nil { + // Return an error if the deletion failed. + return fmt.Errorf("error while deleting document: %v", err) + } + + // If no error occurred, return nil (indicating success). + return nil +} diff --git a/internal/service/minio_service.go b/internal/service/minio_service.go index 5c4430e..e63be3b 100644 --- a/internal/service/minio_service.go +++ b/internal/service/minio_service.go @@ -9,6 +9,15 @@ import ( ) // GetFileFromMinIO retrieves a file from the specified MinIO bucket. +// It returns the file object if found, or an error if there is an issue with fetching the file. +// +// Parameters: +// - bucketName (string): The name of the MinIO bucket to fetch the file from. +// - objectName (string): The name of the object (file) to retrieve from the bucket. +// +// Returns: +// - *minio.Object: The file object retrieved from MinIO. +// - error: An error is returned if there is an issue fetching the object from MinIO. 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{}) @@ -21,6 +30,16 @@ func GetFileFromMinIO(bucketName, objectName string) (*minio.Object, error) { } // UploadFileToMinIO uploads a file to MinIO under the specified bucket and object name. +// If the bucket does not exist, it is created first. +// +// Parameters: +// - ctx (context.Context): The context for the operation (to control request lifetime). +// - bucketName (string): The name of the MinIO bucket to upload the file to. +// - objectName (string): The name of the object (file) in MinIO. +// - filePath (string): The local file path of the file to upload. +// +// Returns: +// - error: An error is returned if there is any issue during file upload. func UploadFileToMinIO(ctx context.Context, bucketName, objectName, filePath string) error { // Open the file from the given file path file, err := os.Open(filePath) @@ -47,6 +66,14 @@ func UploadFileToMinIO(ctx context.Context, bucketName, objectName, filePath str } // createBucketIfNotExists checks if the specified bucket exists and creates it if it doesn't. +// It is called by `UploadFileToMinIO` to ensure the bucket is available before uploading. +// +// Parameters: +// - ctx (context.Context): The context for the operation (to control request lifetime). +// - bucketName (string): The name of the bucket to check/create. +// +// Returns: +// - error: An error is returned if the bucket checking or creation process fails. func createBucketIfNotExists(ctx context.Context, bucketName string) error { // Check if the bucket already exists exists, err := config.MinIO.BucketExists(ctx, bucketName) @@ -71,6 +98,14 @@ func createBucketIfNotExists(ctx context.Context, bucketName string) error { } // DeleteFileFromMinIO removes a file from the specified MinIO bucket. +// +// Parameters: +// - ctx (context.Context): The context for the operation (to control request lifetime). +// - bucketName (string): The name of the MinIO bucket where the file is stored. +// - objectName (string): The name of the object (file) to delete. +// +// Returns: +// - error: An error is returned if there is an issue deleting the file from MinIO. 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{})