Skip to main content

Overview

DefDrive provides a secure file management system with per-user directory isolation, file ownership tracking, and metadata storage. Files are stored on disk with database records tracking ownership, size, and access permissions.

User Isolation

Each user has a separate directory

Ownership Tracking

Files are linked to user accounts

Metadata Storage

Size, hash, and location tracked

Public/Private

Per-file visibility control

File Model

The File model stores metadata about uploaded files and their relationship to users.

Model Definition

// From models/file.go:7-19
type File struct {
    gorm.Model
    Name     string
    Location string
    Size     int64
    Hash     string
    Public   bool `gorm:"default:false"`
    
    UserID   uint `gorm:"index"`
    User     User `gorm:"foreignKey:UserID;references:ID"`
    
    Accesses []Access `gorm:"foreignKey:FileID;references:ID"`
}
The gorm.Model includes standard fields: ID, CreatedAt, UpdatedAt, and DeletedAt (for soft deletes).

Field Descriptions

FieldTypeDescription
NamestringOriginal filename as uploaded by user
LocationstringRelative storage path (username/filename)
Sizeint64File size in bytes
HashstringFile hash for integrity verification
PublicboolVisibility flag (defaults to private)
UserIDuintForeign key to owner’s User record (indexed)
UserUserBelongs-to relationship with User
Accesses[]AccessHas-many relationship with Access records

File Upload Process

The upload process enforces user limits, creates directory structure, and stores file metadata.

Upload Handler

// From controllers/file.go:24-30
func (fc *FileController) Upload(c *gin.Context) {
    // Get current user ID from context (set by auth middleware)
    userID, exists := c.Get("userID")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
        return
    }
    // ...
}
The upload endpoint requires authentication. The user ID is extracted from the JWT token by the AuthRequired() middleware.

Step 1: Retrieve Uploaded File

// From controllers/file.go:32-37
file, err := c.FormFile("file")
if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
    return
}
Files must be submitted as multipart form data with the field name file.

Step 2: Check File Count Limit

// From controllers/file.go:46-61
// Check current file count
var currentFileCount int64
if err := fc.DB.Model(&models.File{}).Where("user_id = ?", userID).Count(&currentFileCount).Error; err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check current file count"})
    return
}

// Check file limit
if currentFileCount >= int64(user.MaxFiles) {
    c.JSON(http.StatusForbidden, gin.H{
        "error":         "File limit exceeded",
        "current_files": currentFileCount,
        "max_files":     user.MaxFiles,
    })
    return
}
See User Limits for detailed information about how file count and storage limits are enforced.

Step 3: Check Storage Limit

// From controllers/file.go:63-79
// Check current storage usage
var currentStorage int64
if err := fc.DB.Model(&models.File{}).Where("user_id = ?", userID).Select("COALESCE(SUM(size), 0)").Scan(&currentStorage).Error; err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check current storage usage"})
    return
}

// Check storage limit
if currentStorage+file.Size > user.MaxStorage {
    c.JSON(http.StatusForbidden, gin.H{
        "error":           "Storage limit exceeded",
        "current_storage": currentStorage,
        "max_storage":     user.MaxStorage,
        "file_size":       file.Size,
    })
    return
}
Storage checks use COALESCE(SUM(size), 0) to handle users with no files, which would otherwise return NULL.

Step 4: Create User Directory

// From controllers/file.go:81-86
// Create user directory if it doesn't exist
userDir := filepath.Join("/app/data/uploads", user.Username)
if err := os.MkdirAll(userDir, 0755); err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user directory"})
    return
}

Directory Structure

/app/data/uploads/username/

Permissions

0755 (rwxr-xr-x)

Step 5: Check for Duplicate Filenames

// From controllers/file.go:88-93
// Check if file already exists
filePath := filepath.Join(userDir, filepath.Base(file.Filename))
if _, err := os.Stat(filePath); err == nil {
    c.JSON(http.StatusConflict, gin.H{"error": "A file with this name already exists in your folder"})
    return
}
DefDrive prevents duplicate filenames within a user’s directory. To upload a file with the same name, the existing file must be deleted first.

Step 6: Save File to Disk

// From controllers/file.go:95-99
if err := c.SaveUploadedFile(file, filePath); err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
    return
}

Step 7: Store File Record in Database

// From controllers/file.go:101-116
// Store only the relative path (username/filename) in the database
relativePath := filepath.Join(user.Username, filepath.Base(file.Filename))

// Create file record in database
fileRecord := models.File{
    Name:     file.Filename,
    Location: relativePath,
    UserID:   userID.(uint),
    Size:     file.Size,
    Public:   false, // Default to private
}

if result := fc.DB.Create(&fileRecord); result.Error != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to record file in database"})
    return
}
Files default to private (Public = false). Use the TogglePublicAccess endpoint to make files public.

Storage Paths

DefDrive uses a structured file storage system:

Absolute vs Relative Paths

// From controllers/file.go:82 and 102
userDir := filepath.Join("/app/data/uploads", user.Username)  // Absolute path for disk operations
relativePath := filepath.Join(user.Username, filepath.Base(file.Filename))  // Relative path for database
Example:
  • Absolute disk path: /app/data/uploads/johndoe/document.pdf
  • Relative database path: johndoe/document.pdf
Storing relative paths in the database makes the system portable and allows the base upload directory to be changed without database migrations.

File Ownership

Every file is owned by exactly one user via the UserID foreign key.

Ownership Verification

// From controllers/file.go:162-166
// Check if user owns the file
if file.UserID != userID.(uint) {
    c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to modify this file"})
    return
}
All file modification operations (toggle public, delete) verify ownership before proceeding.

User-File Relationship

// From models/user.go:17
Files []File `gorm:"foreignKey:UserID;references:ID"`

// From models/file.go:15-16
UserID   uint `gorm:"index"`
User     User `gorm:"foreignKey:UserID;references:ID"`
The UserID field is indexed for efficient querying when listing a user’s files or calculating storage usage.

Listing Files

Retrieve all files owned by the authenticated user:
// From controllers/file.go:125-139
func (fc *FileController) ListFiles(c *gin.Context) {
    userID, exists := c.Get("userID")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
        return
    }

    var files []models.File
    result := fc.DB.Where("user_id = ?", userID).Find(&files)
    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve files"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"files": files})
}
Reference: controllers/file.go:125-139

Public/Private Access

Control file visibility with the Public flag.

Toggle Public Access

// From controllers/file.go:143-187
func (fc *FileController) TogglePublicAccess(c *gin.Context) {
    // ... ownership verification ...

    // Parse request body
    var requestBody struct {
        Public bool `json:"public"`
    }
    if err := c.ShouldBindJSON(&requestBody); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
        return
    }

    // Update public status
    file.Public = requestBody.Public
    if err := fc.DB.Save(&file).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update file"})
        return
    }
}
API Example:
PATCH /api/files/:fileID/public
Authorization: Bearer <token>
Content-Type: application/json

{
  "public": true
}
Reference: controllers/file.go:143-187
Making a file public does NOT automatically make it accessible. Access links must also be public. See Access Control for details.

File Deletion

Deleting a file removes both the database record and the physical file.

Deletion Process

// From controllers/file.go:191-238
func (fc *FileController) DeleteFile(c *gin.Context) {
    // ... ownership verification ...

    // Delete any associated accesses first (cascade delete)
    if err := fc.DB.Where("file_id = ?", uint(fileID)).Delete(&models.Access{}).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file accesses"})
        return
    }

    // Reconstruct the full file path to delete the physical file
    fullPath := filepath.Join("/app/data/uploads", file.Location)

    // Delete the file record
    if err := fc.DB.Delete(&file).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file record"})
        return
    }

    // Delete the physical file from the host system
    if err := os.Remove(fullPath); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete the physical file"})
        return
    }
}
Reference: controllers/file.go:191-238

Deletion Order

  1. Verify ownership
  2. Delete associated Access records (cascade)
  3. Delete database File record
  4. Delete physical file from disk
Access links must be deleted before the File record to maintain referential integrity. The File record is deleted before the physical file to ensure the database reflects the true state.

File Statistics

Retrieve detailed statistics about file usage:
// From controllers/file.go:241-291
func (fc *FileController) GetUserStats(c *gin.Context) {
    // Get current file count
    var fileCount int64
    fc.DB.Model(&models.File{}).Where("user_id = ?", userID).Count(&fileCount)

    // Get current storage usage
    var totalSize int64
    fc.DB.Model(&models.File{}).Where("user_id = ?", userID).Select("COALESCE(SUM(size), 0)").Scan(&totalSize)

    // Get file type breakdown
    var fileStats []struct {
        Extension string `json:"extension"`
        Count     int64  `json:"count"`
        TotalSize int64  `json:"total_size"`
    }

    fc.DB.Raw(`
        SELECT 
            CASE 
                WHEN name ~ '\.' THEN LOWER(RIGHT(name, LENGTH(name) - POSITION('.' IN REVERSE(name))))
                ELSE 'no_extension'
            END as extension,
            COUNT(*) as count,
            COALESCE(SUM(size), 0) as total_size
        FROM files 
        WHERE user_id = ? AND deleted_at IS NULL
        GROUP BY extension
        ORDER BY total_size DESC
    `, userID).Scan(&fileStats)
    // ...
}
Response Example:
{
  "file_count": 15,
  "total_storage": 52428800,
  "file_types": [
    {
      "extension": "pdf",
      "count": 8,
      "total_size": 31457280
    },
    {
      "extension": "jpg",
      "count": 5,
      "total_size": 15728640
    },
    {
      "extension": "txt",
      "count": 2,
      "total_size": 5242880
    }
  ]
}
Reference: controllers/file.go:241-291

API Endpoints Summary

Upload File

POST /api/files
Multipart form data with file field

List Files

GET /api/files
Returns all files for authenticated user

Toggle Public

PATCH /api/files/:fileID/public
Set file visibility

Delete File

DELETE /api/files/:fileID
Remove file and all access links

Get Statistics

GET /api/files/stats
File count, storage, and type breakdown

User Limits

Understanding file and storage quotas

Access Control

Creating shareable links with restrictions

Authentication

JWT authentication for file operations

Build docs developers (and LLMs) love