package model import ( "fmt" "strings" "time" "unicode" "gorm.io/gorm" ) // FileBlob represents a physical file stored in MinIO, deduplicated by SHA256. type FileBlob struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` SHA256 string `gorm:"uniqueIndex;size:64;not null" json:"sha256"` MinioKey string `gorm:"size:255;not null" json:"minio_key"` FileSize int64 `gorm:"not null" json:"file_size"` MimeType string `gorm:"size:255" json:"mime_type"` RefCount int `gorm:"not null;default:0" json:"ref_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (FileBlob) TableName() string { return "hpc_file_blobs" } // File represents a logical file visible to users, backed by a FileBlob. type File struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` Name string `gorm:"size:255;not null" json:"name"` FolderID *int64 `gorm:"index" json:"folder_id,omitempty"` BlobSHA256 string `gorm:"size:64;not null" json:"blob_sha256"` UserID *int64 `gorm:"index" json:"user_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` } func (File) TableName() string { return "hpc_files" } // Folder represents a directory in the virtual file system. type Folder struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` Name string `gorm:"size:255;not null" json:"name"` ParentID *int64 `gorm:"index" json:"parent_id,omitempty"` Path string `gorm:"uniqueIndex;size:768;not null" json:"path"` UserID *int64 `gorm:"index" json:"user_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` } func (Folder) TableName() string { return "hpc_folders" } // UploadSession represents an in-progress chunked upload. // State transitions: pending→uploading, pending→completed(zero-byte), uploading→merging, // uploading→cancelled, merging→completed, merging→failed, any→expired type UploadSession struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` FileName string `gorm:"size:255;not null" json:"file_name"` FileSize int64 `gorm:"not null" json:"file_size"` ChunkSize int64 `gorm:"not null" json:"chunk_size"` TotalChunks int `gorm:"not null" json:"total_chunks"` SHA256 string `gorm:"size:64;not null" json:"sha256"` FolderID *int64 `gorm:"index" json:"folder_id,omitempty"` Status string `gorm:"size:20;not null;default:pending" json:"status"` MinioPrefix string `gorm:"size:255;not null" json:"minio_prefix"` MimeType string `gorm:"size:255;default:'application/octet-stream'" json:"mime_type"` UserID *int64 `gorm:"index" json:"user_id,omitempty"` ExpiresAt time.Time `gorm:"not null" json:"expires_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (UploadSession) TableName() string { return "hpc_upload_sessions" } // UploadChunk represents a single chunk of an upload session. type UploadChunk struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` SessionID int64 `gorm:"not null;uniqueIndex:idx_session_chunk" json:"session_id"` ChunkIndex int `gorm:"not null;uniqueIndex:idx_session_chunk" json:"chunk_index"` MinioKey string `gorm:"size:255;not null" json:"minio_key"` SHA256 string `gorm:"size:64" json:"sha256,omitempty"` Size int64 `gorm:"not null" json:"size"` Status string `gorm:"size:20;not null;default:pending" json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (UploadChunk) TableName() string { return "hpc_upload_chunks" } // InitUploadRequest is the DTO for initiating a chunked upload. type InitUploadRequest struct { FileName string `json:"file_name" binding:"required"` FileSize int64 `json:"file_size" binding:"required"` SHA256 string `json:"sha256" binding:"required"` FolderID *int64 `json:"folder_id,omitempty"` ChunkSize *int64 `json:"chunk_size,omitempty"` MimeType string `json:"mime_type,omitempty"` } // CreateFolderRequest is the DTO for creating a new folder. type CreateFolderRequest struct { Name string `json:"name" binding:"required"` ParentID *int64 `json:"parent_id,omitempty"` } // UploadSessionResponse is the DTO returned when creating/querying an upload session. type UploadSessionResponse struct { ID int64 `json:"id"` FileName string `json:"file_name"` FileSize int64 `json:"file_size"` ChunkSize int64 `json:"chunk_size"` TotalChunks int `json:"total_chunks"` SHA256 string `json:"sha256"` Status string `json:"status"` UploadedChunks []int `json:"uploaded_chunks"` ExpiresAt time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` } // FileResponse is the DTO for a file in API responses. type FileResponse struct { ID int64 `json:"id"` Name string `json:"name"` FolderID *int64 `json:"folder_id,omitempty"` Size int64 `json:"size"` MimeType string `json:"mime_type"` SHA256 string `json:"sha256"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // FolderResponse is the DTO for a folder in API responses. type FolderResponse struct { ID int64 `json:"id"` Name string `json:"name"` ParentID *int64 `json:"parent_id,omitempty"` Path string `json:"path"` FileCount int64 `json:"file_count"` SubFolderCount int64 `json:"subfolder_count"` CreatedAt time.Time `json:"created_at"` } // ListFilesResponse is the paginated response for listing files. type ListFilesResponse struct { Files []FileResponse `json:"files"` Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` } // ValidateFileName rejects empty, "..", "/", "\", null bytes, control chars, leading/trailing spaces. func ValidateFileName(name string) error { if name == "" { return fmt.Errorf("file name cannot be empty") } if strings.TrimSpace(name) != name { return fmt.Errorf("file name cannot have leading or trailing spaces") } if name == ".." { return fmt.Errorf("file name cannot be '..'") } if strings.Contains(name, "/") || strings.Contains(name, "\\") { return fmt.Errorf("file name cannot contain '/' or '\\'") } for _, r := range name { if r == 0 { return fmt.Errorf("file name cannot contain null bytes") } if unicode.IsControl(r) { return fmt.Errorf("file name cannot contain control characters") } } return nil } // ValidateFolderName rejects same as ValidateFileName plus ".". func ValidateFolderName(name string) error { if name == "." { return fmt.Errorf("folder name cannot be '.'") } return ValidateFileName(name) }