Storage
Store and retrieve files with Velocity's unified storage interface for local filesystem and Amazon S3.
Velocity provides a unified storage interface for file operations across different backends including local filesystem and Amazon S3. The storage system uses a driver-based architecture that allows you to switch storage backends through configuration without changing your code.
Quick Start
.env file. No setup required!import "github.com/velocitykode/velocity/pkg/storage"
func uploadAvatar(userID int, imageData []byte) error {
// Store file using default disk
path := fmt.Sprintf("avatars/user-%d.jpg", userID)
return storage.Put(path, imageData)
}
func getAvatar(userID int) ([]byte, error) {
path := fmt.Sprintf("avatars/user-%d.jpg", userID)
return storage.Get(path)
}
func deleteAvatar(userID int) error {
path := fmt.Sprintf("avatars/user-%d.jpg", userID)
return storage.Delete(path)
}import (
"os"
"github.com/velocitykode/velocity/pkg/storage"
)
func uploadVideo(videoPath string) error {
// Open file for streaming
file, err := os.Open(videoPath)
if err != nil {
return err
}
defer file.Close()
// Stream large files efficiently
return storage.PutStream("videos/intro.mp4", file)
}
func downloadVideo(outputPath string) error {
// Get file as stream
stream, err := storage.GetStream("videos/intro.mp4")
if err != nil {
return err
}
defer stream.Close()
// Write to file
file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, stream)
return err
}import (
"time"
"github.com/velocitykode/velocity/pkg/storage"
)
func getPublicURL(filePath string) string {
// Get permanent public URL
return storage.URL(filePath)
}
func getTemporaryURL(filePath string) (string, error) {
// Get temporary signed URL (expires in 1 hour)
return storage.TemporaryURL(filePath, 1*time.Hour)
}
// Example in HTTP handler
func handleDownload(c *http.Context) error {
reportPath := "reports/monthly-report.pdf"
// Generate signed URL valid for 30 minutes
url, err := storage.TemporaryURL(reportPath, 30*time.Minute)
if err != nil {
return err
}
return c.JSON(200, map[string]string{
"download_url": url,
})
}Configuration
Configure storage through environment variables in your .env file:
# Default disk
FILESYSTEM_DISK=local # Options: local, s3, public
# Local disk configuration
FILESYSTEM_LOCAL_ROOT=storage/app
FILESYSTEM_LOCAL_URL=http://localhost:8090/storage
FILESYSTEM_LOCAL_VISIBILITY=private
# Public disk configuration
FILESYSTEM_PUBLIC_ROOT=storage/app/public
FILESYSTEM_PUBLIC_URL=http://localhost:8090/storage
APP_URL=http://localhost:8090
# S3 disk configuration
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=my-app-bucket
AWS_URL=https://my-bucket.s3.amazonaws.com
AWS_VISIBILITY=private
# Testing (automatically uses memory driver)
APP_ENV=testingDrivers
Local Driver
The local driver stores files on your server’s filesystem:
- Storage location: Configured via
FILESYSTEM_LOCAL_ROOT(default:storage/app) - Public access: Files can be served via configured URL
- Thread-safe: Concurrent writes are properly synchronized
- Cross-platform: Handles path separators automatically
Use cases:
- Development and testing
- Small-scale deployments
- Files that don’t need distributed storage
S3 Driver
The S3 driver stores files on Amazon S3:
- Scalable: Handle unlimited file storage
- Distributed: Access from anywhere
- Secure: Supports private files with signed URLs
- Efficient: Uses multipart uploads for large files
- Reliable: Automatic retry with exponential backoff
Use cases:
- Production deployments
- Large file storage
- Global content delivery
- Backup and archival
Memory Driver
The memory driver stores files in memory (for testing):
- Fast: No disk I/O overhead
- Clean: No filesystem pollution during tests
- Isolated: Each test gets a clean state
- Limited: Configurable size limit
Use cases:
- Unit testing
- Integration testing
- Development with mock data
API Reference
File Operations
Put
Store file contents at a given path:
// Store byte array
imageData := []byte{...}
err := storage.Put("uploads/image.jpg", imageData)
// Using specific disk
s3 := storage.Disk("s3")
err := s3.Put("backups/data.json", jsonData)PutStream
Store a file from an io.Reader (efficient for large files):
file, _ := os.Open("large-video.mp4")
defer file.Close()
err := storage.PutStream("videos/tutorial.mp4", file)Get
Retrieve file contents as byte array:
contents, err := storage.Get("uploads/document.pdf")
if err != nil {
if errors.Is(err, storage.ErrFileNotFound) {
// Handle missing file
}
return err
}GetStream
Retrieve file as io.ReadCloser (efficient for large files):
stream, err := storage.GetStream("videos/tutorial.mp4")
if err != nil {
return err
}
defer stream.Close()
// Process stream
_, err = io.Copy(outputFile, stream)Exists
Check if a file exists:
if storage.Exists("uploads/avatar.jpg") {
// File exists
}Delete
Delete one or more files:
// Delete single file
err := storage.Delete("uploads/temp.txt")
// Delete multiple files
err := storage.Delete(
"uploads/file1.txt",
"uploads/file2.txt",
"uploads/file3.txt",
)File Management
Copy
Copy a file to a new location:
err := storage.Copy("uploads/original.jpg", "uploads/copy.jpg")Move
Move a file to a new location:
err := storage.Move("uploads/temp.jpg", "uploads/final.jpg")Size
Get the size of a file in bytes:
size, err := storage.Size("uploads/video.mp4")
fmt.Printf("File size: %d bytes\n", size)LastModified
Get the last modified time:
modTime, err := storage.LastModified("uploads/document.pdf")
fmt.Printf("Last modified: %s\n", modTime.Format(time.RFC3339))MimeType
Get the MIME type of a file:
mimeType, err := storage.MimeType("uploads/image.jpg")
// Returns: "image/jpeg"Directory Operations
Files
List files in a directory (non-recursive):
files, err := storage.Files("uploads")
// Returns: ["file1.txt", "file2.pdf"]AllFiles
List all files recursively:
files, err := storage.AllFiles("uploads")
// Returns: ["file1.txt", "2024/file2.pdf", "2024/01/file3.jpg"]Directories
List subdirectories (non-recursive):
dirs, err := storage.Directories("uploads")
// Returns: ["2024", "temp"]AllDirectories
List all subdirectories recursively:
dirs, err := storage.AllDirectories("uploads")
// Returns: ["2024", "2024/01", "2024/02", "temp"]MakeDirectory
Create a directory:
err := storage.MakeDirectory("uploads/2024/12")DeleteDirectory
Delete a directory and all its contents:
err := storage.DeleteDirectory("uploads/temp")URL Operations
URL
Get a permanent public URL for a file:
// Local disk: returns configured base URL + path
url := storage.URL("public/logo.png")
// Returns: "http://localhost:8090/storage/public/logo.png"
// S3 disk: returns S3 URL
s3 := storage.Disk("s3")
url := s3.URL("images/banner.jpg")
// Returns: "https://my-bucket.s3.amazonaws.com/images/banner.jpg"TemporaryURL
Get a temporary signed URL (S3 only):
// Generate URL that expires in 15 minutes
url, err := storage.TemporaryURL("private/report.pdf", 15*time.Minute)
if err != nil {
return err
}
// Share the URL - it will expire automatically
fmt.Println("Download link:", url)Working with Multiple Disks
Using Specific Disks
// Get a specific disk
s3 := storage.Disk("s3")
local := storage.Disk("local")
public := storage.Disk("public")
// Use the disk
s3.Put("backups/data.json", jsonData)
local.Put("logs/app.log", logData)
public.Put("images/logo.png", imageData)Copying Between Disks
// Read from one disk, write to another
data, err := storage.Disk("local").Get("uploads/file.pdf")
if err != nil {
return err
}
err = storage.Disk("s3").Put("backups/file.pdf", data)
if err != nil {
return err
}Custom Disk Configuration
import (
"github.com/velocitykode/velocity/pkg/storage"
)
func setupCustomStorage() error {
config := storage.Config{
Default: "s3",
Disks: map[string]storage.DiskConfig{
"local": {
Driver: "local",
Root: "./storage/app",
URL: "http://localhost:8090/storage",
},
"s3": {
Driver: "s3",
Key: "YOUR_AWS_KEY",
Secret: "YOUR_AWS_SECRET",
Region: "us-east-1",
Bucket: "my-bucket",
},
"backup": {
Driver: "s3",
Key: "YOUR_AWS_KEY",
Secret: "YOUR_AWS_SECRET",
Region: "us-west-2",
Bucket: "my-backup-bucket",
},
},
}
return storage.Configure(config)
}Best Practices
1. Use UUIDs for Uploaded Files
Prevent filename collisions and path traversal attacks:
import "github.com/google/uuid"
func handleFileUpload(originalName string, data []byte) error {
// Generate unique filename
ext := filepath.Ext(originalName)
filename := uuid.New().String() + ext
path := fmt.Sprintf("uploads/%s", filename)
return storage.Put(path, data)
}2. Stream Large Files
Don’t load large files into memory:
// Good: Stream the file
file, _ := os.Open("large-file.mp4")
defer file.Close()
storage.PutStream("videos/video.mp4", file)
// Bad: Load entire file into memory
data, _ := os.ReadFile("large-file.mp4")
storage.Put("videos/video.mp4", data)3. Use Appropriate Visibility
Store sensitive files as private:
// Private files (default on S3)
storage.Disk("s3").Put("invoices/invoice-123.pdf", invoiceData)
// Public files
storage.Disk("public").Put("images/logo.png", logoData)
// Generate temporary access to private files
url, _ := storage.TemporaryURL("invoices/invoice-123.pdf", 1*time.Hour)4. Handle Errors Properly
import "errors"
func getFile(path string) ([]byte, error) {
data, err := storage.Get(path)
if err != nil {
if errors.Is(err, storage.ErrFileNotFound) {
// Handle missing file specifically
return nil, fmt.Errorf("file not found: %s", path)
}
// Handle other errors
return nil, fmt.Errorf("failed to read file: %w", err)
}
return data, nil
}5. Organize Files with Directory Structure
// Good: Organized structure
storage.Put(fmt.Sprintf("uploads/%d/%s/avatar.jpg", year, month), data)
storage.Put(fmt.Sprintf("users/%d/documents/%s.pdf", userID, docType), data)
// Bad: Flat structure
storage.Put(fmt.Sprintf("avatar-%d.jpg", userID), data)6. Clean Up Temporary Files
// Upload temporary file
tempPath := fmt.Sprintf("temp/%s.jpg", uuid.New().String())
storage.Put(tempPath, imageData)
// Process the file
processedData := processImage(tempPath)
// Clean up
defer storage.Delete(tempPath)
// Save final result
storage.Put("images/final.jpg", processedData)Testing
Using Memory Driver
func TestFileUpload(t *testing.T) {
// Memory driver is automatically used when APP_ENV=testing
testData := []byte("test content")
err := storage.Put("test/file.txt", testData)
assert.NoError(t, err)
// Verify file was stored
retrieved, err := storage.Get("test/file.txt")
assert.NoError(t, err)
assert.Equal(t, testData, retrieved)
}Testing with Fake Storage
import "github.com/velocitykode/velocity/pkg/storage/testing"
func TestUserAvatar(t *testing.T) {
// Create fake storage
fake := testing.NewFake()
storage.SetGlobalManager(storage.NewManager(storage.Config{
Default: "fake",
Disks: map[string]storage.DiskConfig{
"fake": {Driver: "memory"},
},
}))
// Test your code
UploadAvatar(123, testImage)
// Assert file was stored
fake.AssertExists("avatars/user-123.jpg")
fake.AssertMissing("avatars/user-456.jpg")
}Complete Example
Here’s a complete example of a file upload handler:
package handlers
import (
"fmt"
"io"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/velocitykode/velocity/pkg/http"
"github.com/velocitykode/velocity/pkg/storage"
)
type FileHandler struct{}
// Upload handles file uploads
func (fc *FileHandler) Upload(c *http.Context) error {
// Parse multipart form (32 MB max)
err := c.Request.ParseMultipartForm(32 << 20)
if err != nil {
return c.JSON(400, map[string]string{
"error": "Invalid file upload",
})
}
// Get the file
file, header, err := c.Request.FormFile("file")
if err != nil {
return c.JSON(400, map[string]string{
"error": "No file provided",
})
}
defer file.Close()
// Validate file size (10 MB max)
if header.Size > 10*1024*1024 {
return c.JSON(400, map[string]string{
"error": "File too large (max 10MB)",
})
}
// Generate unique filename
ext := filepath.Ext(header.Filename)
filename := uuid.New().String() + ext
path := fmt.Sprintf("uploads/%s", filename)
// Store the file
err = storage.PutStream(path, file)
if err != nil {
return c.JSON(500, map[string]string{
"error": "Failed to store file",
})
}
// Generate public URL
url := storage.URL(path)
return c.JSON(200, map[string]interface{}{
"message": "File uploaded successfully",
"path": path,
"url": url,
"size": header.Size,
})
}
// Download generates a temporary download link
func (fc *FileHandler) Download(c *http.Context) error {
filePath := c.Param("path")
// Check if file exists
if !storage.Exists(filePath) {
return c.JSON(404, map[string]string{
"error": "File not found",
})
}
// Generate temporary URL (valid for 1 hour)
url, err := storage.TemporaryURL(filePath, 1*time.Hour)
if err != nil {
return c.JSON(500, map[string]string{
"error": "Failed to generate download link",
})
}
return c.JSON(200, map[string]string{
"download_url": url,
"expires_in": "1 hour",
})
}
// Delete removes a file
func (fc *FileHandler) Delete(c *http.Context) error {
filePath := c.Param("path")
// Check if file exists
if !storage.Exists(filePath) {
return c.JSON(404, map[string]string{
"error": "File not found",
})
}
// Delete the file
err := storage.Delete(filePath)
if err != nil {
return c.JSON(500, map[string]string{
"error": "Failed to delete file",
})
}
return c.JSON(200, map[string]string{
"message": "File deleted successfully",
})
}
