feat: initial open-source release of Pipely

OTA update server designed for high-concurrency, low-bandwidth deployments.
GORM-backed PostgreSQL, JWT auth, device management, artefact versioning,
deployment rollout with gated rate-limited downloads, and a React admin panel.
This commit is contained in:
2026-07-07 10:29:28 +08:00
commit e070a3fb5f
59 changed files with 6515 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
package model
import (
"time"
"gorm.io/gorm"
)
// Device status constants
const (
DeviceStatusOnline = "online"
DeviceStatusOffline = "offline"
DeviceStatusUpgrading = "upgrading"
)
// Deployment status constants
const (
DeploymentStatusDraft = "draft"
DeploymentStatusActive = "active"
DeploymentStatusPaused = "paused"
DeploymentStatusCompleted = "completed"
DeploymentStatusCancelled = "cancelled"
)
// Deployment device status constants
const (
DeployDeviceStatusPending = "pending"
DeployDeviceStatusDownloading = "downloading"
DeployDeviceStatusDownloaded = "downloaded"
DeployDeviceStatusVerifying = "verifying"
DeployDeviceStatusInstalling = "installing"
DeployDeviceStatusCompleted = "completed"
DeployDeviceStatusFailed = "failed"
DeployDeviceStatusRolledBack = "rolled_back"
)
// Rollout strategy constants
const (
RolloutImmediate = "immediate"
RolloutStaged = "staged"
)
// Target type constants
const (
TargetAll = "all"
TargetDevice = "device"
TargetGroup = "group"
)
// Device represents a connected daemon instance.
type Device struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
DeviceID string `json:"deviceId" gorm:"uniqueIndex;size:256;not null"`
IPAddress string `json:"ipAddress" gorm:"size:64;default:''"`
Hostname string `json:"hostname" gorm:"size:256;default:''"`
OS string `json:"os" gorm:"size:64;default:''"`
CurrentVersion string `json:"currentVersion" gorm:"size:128;default:'0.0.0'"`
Status string `json:"status" gorm:"size:32;default:'online';index"`
GroupName string `json:"groupName" gorm:"size:128;default:'default';index"`
LastHeartbeat time.Time `json:"lastHeartbeat"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Artefact represents an uploaded update package (JAR or patch).
type Artefact struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
Version string `json:"version" gorm:"size:128;not null;index"`
FileName string `json:"fileName" gorm:"size:512;not null"`
FilePath string `json:"-" gorm:"size:1024;not null"`
FileSize int64 `json:"fileSize"`
SHA256Hash string `json:"sha256Hash" gorm:"size:128"`
DeltaRef string `json:"deltaRef" gorm:"size:128;default:''"`
IsDelta bool `json:"isDelta"`
Metadata string `json:"metadata" gorm:"type:text;default:'{}'"`
CreatedAt time.Time `json:"createdAt"`
}
// Deployment defines an update rollout campaign.
type Deployment struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
ArtefactID int64 `json:"artefactId" gorm:"not null;index"`
Artefact *Artefact `json:"artefact,omitempty" gorm:"foreignKey:ArtefactID"`
Name string `json:"name" gorm:"size:256;not null"`
Description string `json:"description" gorm:"type:text;default:''"`
TargetType string `json:"targetType" gorm:"size:32;default:'all'"`
TargetSpec string `json:"targetSpec" gorm:"type:text;default:'{}'"`
RolloutStrategy string `json:"rolloutStrategy" gorm:"size:32;default:'immediate'"`
MaxConcurrent int `json:"maxConcurrent" gorm:"default:50"`
Status string `json:"status" gorm:"size:32;default:'draft';index"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// DeploymentDevice tracks an individual device's progress through a deployment.
type DeploymentDevice struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
DeploymentID int64 `json:"deploymentId" gorm:"not null;uniqueIndex:idx_dd_dep_dev;index"`
DeviceID string `json:"deviceId" gorm:"size:256;not null;uniqueIndex:idx_dd_dep_dev;index"`
Status string `json:"status" gorm:"size:32;default:'pending';index"`
DownloadedAt *time.Time `json:"downloadedAt"`
CompletedAt *time.Time `json:"completedAt"`
Error string `json:"error" gorm:"type:text;default:''"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// AdminUser represents an operator account.
type AdminUser struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username" gorm:"uniqueIndex;size:128;not null"`
PasswordHash string `json:"-" gorm:"size:256;not null"`
Role string `json:"role" gorm:"size:32;default:'admin'"`
CreatedAt time.Time `json:"createdAt"`
}
// -- Request/Response DTOs --
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expiresAt"`
}
type RegisterDeviceRequest struct {
DeviceID string `json:"deviceId" binding:"required"`
IPAddress string `json:"ipAddress"`
Hostname string `json:"hostname"`
OS string `json:"os"`
CurrentVersion string `json:"currentVersion"`
GroupName string `json:"groupName"`
}
type HeartbeatRequest struct {
DeviceID string `json:"deviceId" binding:"required"`
CurrentVersion string `json:"currentVersion"`
Status string `json:"status"`
}
type CheckUpdateResponse struct {
HasUpdate bool `json:"hasUpdate"`
ArtefactID int64 `json:"artefactId,omitempty"`
Version string `json:"version,omitempty"`
FileSize int64 `json:"fileSize,omitempty"`
SHA256Hash string `json:"sha256Hash,omitempty"`
DownloadURL string `json:"downloadUrl,omitempty"`
}
type CreateDeploymentRequest struct {
ArtefactID int64 `json:"artefactId" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
TargetType string `json:"targetType" binding:"required"`
TargetSpec string `json:"targetSpec"`
RolloutStrategy string `json:"rolloutStrategy"`
MaxConcurrent int `json:"maxConcurrent"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
// -- Hooks --
func (d *Device) BeforeCreate(tx *gorm.DB) error {
now := time.Now()
if d.LastHeartbeat.IsZero() {
d.LastHeartbeat = now
}
if d.CreatedAt.IsZero() {
d.CreatedAt = now
}
if d.UpdatedAt.IsZero() {
d.UpdatedAt = now
}
return nil
}