Skip to content

Conversation

@coolwednesday
Copy link
Member

@coolwednesday coolwednesday commented Nov 28, 2025

Pull Request Template

Description:

package main

import (
	"fmt"
	"io"
	"os"
	"path"
	"strings"

	"gofr.dev/pkg/gofr"
	"gofr.dev/pkg/gofr/datasource/file/azure"
)

func main() {
	app := gofr.New()

	// Configure Azure File Storage
	accountName := app.Config.Get("AZURE_STORAGE_ACCOUNT_NAME")
	accountKey := app.Config.Get("AZURE_STORAGE_ACCOUNT_KEY")
	shareName := app.Config.Get("AZURE_FILE_SHARE_NAME")

	azureConfig := &azure.Config{
		AccountName: accountName,
		AccountKey:  accountKey,
		ShareName:   shareName,
	}

	// Create Azure File Storage filesystem
	azureFS, err := azure.New(azureConfig, app.Logger(), app.Metrics())
	if err != nil {
		app.Logger().Errorf("Failed to initialize Azure File Storage: %v", err)
		return
	}

	// Add Azure File Storage as the file store
	app.AddFileStore(azureFS)

	// File operations endpoints
	// Note: More specific routes (with path params) should be registered before less specific ones
	// to avoid routing conflicts
	// Use {filename:.*} pattern to match paths with slashes (e.g., dir1/testfile1)
	app.GET("/files/{filename:.*}/info", getFileInfo)
	app.GET("/files/{filename:.*}", readFile)
	app.POST("/files/{filename:.*}", createFile)
	app.PUT("/files/{filename:.*}", updateFile)
	app.DELETE("/files/{filename:.*}", deleteFile)
	app.GET("/files", listFiles) // Register /files last to avoid conflict with /files/{filename}
	
	// Directory operations endpoints
	// Use {dirname:.*} pattern to match paths with slashes (e.g., dir1/subdir)
	app.POST("/directories/{dirname:.*}", createDirectory)
	app.GET("/directories/{dirname:.*}", listDirectory)

	app.Run()
}

// listFiles lists all files in the root directory
func listFiles(c *gofr.Context) (any, error) {
	files, err := c.File.ReadDir("")
	if err != nil {
		return nil, err
	}

	result := make([]map[string]any, 0, len(files))
	for _, f := range files {
		result = append(result, map[string]any{
			"name":    f.Name(),
			"size":    f.Size(),
			"isDir":   f.IsDir(),
			"modTime": f.ModTime(),
			"mode":    f.Mode().String(),
		})
	}

	return result, nil
}

// readFile reads the content of a file
func readFile(c *gofr.Context) (any, error) {
	filename := c.PathParam("filename")

	file, err := c.File.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	// Read all content
	data, err := io.ReadAll(file)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	return map[string]any{
		"filename": filename,
		"content":  string(data),
		"size":     len(data),
	}, nil
}

// createFile creates a new file with content from request body
func createFile(c *gofr.Context) (any, error) {
	filename := c.PathParam("filename")

	// Read request body - accept JSON with content field or plain text
	type fileContent struct {
		Content string `json:"content"`
	}

	var fc fileContent
	if err := c.Bind(&fc); err != nil {
		return nil, fmt.Errorf("failed to read request body: %w", err)
	}

	body := []byte(fc.Content)

	// Create file (parent directories are automatically created by Azure implementation)
	file, err := c.File.Create(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to create file: %w", err)
	}
	defer file.Close()

	// Write content
	_, err = file.Write(body)
	if err != nil {
		return nil, fmt.Errorf("failed to write to file: %w", err)
	}

	return map[string]any{
		"message":  "File created successfully",
		"filename": filename,
		"size":     len(body),
	}, nil
}

// updateFile updates an existing file or creates it if it doesn't exist
func updateFile(c *gofr.Context) (any, error) {
	filename := c.PathParam("filename")

	// Read request body - accept JSON with content field
	type fileContent struct {
		Content string `json:"content"`
	}

	var fc fileContent
	if err := c.Bind(&fc); err != nil {
		return nil, fmt.Errorf("failed to read request body: %w", err)
	}

	body := []byte(fc.Content)

	// Open file with write permissions (create if not exists)
	// Parent directories are automatically created by Azure implementation
	file, err := c.File.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	// Write content
	_, err = file.Write(body)
	if err != nil {
		return nil, fmt.Errorf("failed to write to file: %w", err)
	}

	return map[string]any{
		"message":  "File updated successfully",
		"filename": filename,
		"size":     len(body),
	}, nil
}

// deleteFile deletes a file
func deleteFile(c *gofr.Context) (any, error) {
	filename := c.PathParam("filename")

	err := c.File.Remove(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to delete file: %w", err)
	}

	return map[string]any{
		"message":  "File deleted successfully",
		"filename": filename,
	}, nil
}

// getFileInfo retrieves metadata about a file
func getFileInfo(c *gofr.Context) (any, error) {
	filename := c.PathParam("filename")

	info, err := c.File.Stat(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to get file info: %w", err)
	}

	// Extract just the filename (not the full path) for consistency with directory listings
	// Stat returns the full path, but we want just the basename like in listFiles
	fileBaseName := path.Base(info.Name())

	return map[string]any{
		"name":    fileBaseName,
		"size":    info.Size(),
		"isDir":   info.IsDir(),
		"modTime": info.ModTime(),
		"mode":    info.Mode().String(),
	}, nil
}

// createDirectory creates a new directory
func createDirectory(c *gofr.Context) (any, error) {
	dirname := c.PathParam("dirname")

	err := c.File.MkdirAll(dirname, 0755)
	if err != nil {
		return nil, fmt.Errorf("failed to create directory: %w", err)
	}

	return map[string]any{
		"message": "Directory created successfully",
		"dirname": dirname,
	}, nil
}

// listDirectory lists files in a specific directory
func listDirectory(c *gofr.Context) (any, error) {
	dirname := c.PathParam("dirname")

	files, err := c.File.ReadDir(dirname)
	if err != nil {
		return nil, fmt.Errorf("failed to list directory: %w", err)
	}

	result := make([]map[string]any, 0, len(files))
	for _, f := range files {
		result = append(result, map[string]any{
			"name":    f.Name(),
			"size":    f.Size(),
			"isDir":   f.IsDir(),
			"modTime": f.ModTime(),
			"mode":    f.Mode().String(),
		})
	}

	return map[string]any{
		"directory": dirname,
		"files":     result,
		"count":     len(result),
	}, nil
}

Checklist:

  • I have formatted my code using goimport and golangci-lint.
  • All new code is covered by unit tests.
  • This PR does not decrease the overall code coverage.
  • I have reviewed the code comments and documentation for clarity.

Thank you for your contribution!

Copy link
Member

@Umang01-hash Umang01-hash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • In the provided example of main.go:
// Configure Azure File Storage
	accountName := app.Config.Get("AZURE_STORAGE_ACCOUNT_NAME")
	accountKey := app.Config.Get("AZURE_STORAGE_ACCOUNT_KEY")
	shareName := app.Config.Get("AZURE_FILE_SHARE_NAME")
	endpoint := app.Config.Get("AZURE_STORAGE_ENDPOINT")

	// Validate required configuration
	if accountName == "" {
		app.Logger().Error("AZURE_STORAGE_ACCOUNT_NAME is required")
		return
	}
	if accountKey == "" {
		app.Logger().Error("AZURE_STORAGE_ACCOUNT_KEY is required")
		return
	}
	if shareName == "" {
		app.Logger().Error("AZURE_FILE_SHARE_NAME is required")
		return
	}

	azureConfig := &azure.Config{
		AccountName: accountName,
		AccountKey:  accountKey,
		ShareName:   shareName,
		Endpoint:    endpoint,
	}

	// Create Azure File Storage filesystem
	azureFS, err := azure.New(azureConfig, app.Logger(), app.Metrics())
	if err != nil {
		app.Logger().Errorf("Failed to initialize Azure File Storage: %v", err)
		return
	}

This validation should be done internally in GoFr, and not by the user.

  • The coverage of file fs.go is very low ~58% can we try and improve it??

  • In the file fs_test.go, I commented the TestNew_WithEndpoint, TestNew_DefaultEndpoint and still got the same coverage. Please remove then also.

  • No if-else conditions in test, please create separate methods if needed.

@coolwednesday
Copy link
Member Author

@Umang01-hash, Refactored the tests as suggested. A few functions still have required conditional branches within the struct—these determine whether additional fields need to be asserted for specific use cases.

@coolwednesday coolwednesday merged commit 11ead52 into development Dec 5, 2025
20 checks passed
@coolwednesday coolwednesday deleted the en/azure branch December 5, 2025 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

azure blob storage abstraction for dealing with files stored there

4 participants