Skip to main content
Wings supports live server transfers between nodes, allowing servers to be migrated without manual intervention.

Transfer Architecture

Transfer Structure

type Transfer struct {
    ctx    context.Context
    cancel *context.CancelFunc
    
    Server  *server.Server
    status  *system.Atomic[Status]
    archive *Archive
}
Source: server/transfer/transfer.go:43-56

Transfer States

const (
    StatusPending     Status = "pending"
    StatusProcessing  Status = "processing"
    StatusCancelling  Status = "cancelling"
    StatusCancelled   Status = "cancelled"
    StatusFailed      Status = "failed"
    StatusCompleted   Status = "completed"
)
Source: server/transfer/transfer.go:23-40

Transfer Process

Initiation (Source Node)

Transfers are initiated via API:
POST /api/servers/{server}/transfer
curl -X POST http://localhost:8080/api/servers/{server}/transfer \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://target-node.example.com/api/transfers",
    "token": "transfer-auth-token"
  }'
Source: router/router.go:85

Transfer Instance Creation

func New(ctx context.Context, s *server.Server) *Transfer {
    ctx, cancel := context.WithCancel(ctx)
    
    return &Transfer{
        ctx:    ctx,
        cancel: &cancel,
        Server: s,
        status: system.NewAtomic(StatusPending),
    }
}
Source: server/transfer/transfer.go:59-69

Archive Creation

Archive Structure

type Archive struct {
    archive *filesystem.Archive
}

func NewArchive(t *Transfer, size uint64) *Archive {
    return &Archive{
        archive: &filesystem.Archive{
            Filesystem: t.Server.Filesystem(),
            Progress:   progress.NewProgress(size),
        },
    }
}
Source: server/transfer/archive.go:29-42

Getting Archive Instance

func (t *Transfer) Archive() (*Archive, error) {
    if t.archive == nil {
        // Get server disk usage for progress calculation
        rawSize, err := t.Server.Filesystem().DiskUsage(true)
        if err != nil {
            return nil, fmt.Errorf("transfer: failed to get server disk usage: %w", err)
        }
        
        // Create archive instance
        t.archive = NewArchive(t, uint64(rawSize))
    }
    
    return t.archive, nil
}
Source: server/transfer/archive.go:14-27

Streaming Archive

func (a *Archive) Stream(ctx context.Context, w io.Writer) error {
    return a.archive.Stream(ctx, w)
}
Features:
  • Streams files directly to writer
  • No intermediate disk storage required
  • Progress tracking built-in
  • Context cancellation support
Source: server/transfer/archive.go:45-47

Pushing to Target Node

Push Method

func (t *Transfer) PushArchiveToTarget(url, token string) ([]byte, error) {
    // 1. Set processing status
    t.SetStatus(StatusProcessing)
    
    // 2. Get archive
    a, _ := t.Archive()
    
    // 3. Create HTTP request
    body, writer := io.Pipe()
    req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
    req.Header.Set("Authorization", token)
    
    // 4. Create multipart writer
    mp := multipart.NewWriter(writer)
    req.Header.Set("Content-Type", mp.FormDataContentType())
    
    // 5. Stream archive in goroutine
    go func() {
        src, pw := io.Pipe()
        h := sha256.New()
        tee := io.TeeReader(src, h)
        
        // Create form file
        dest, _ := mp.CreateFormFile("archive", "archive.tar.gz")
        
        // Copy with checksum calculation
        go io.Copy(dest, tee)
        
        // Stream archive to pipe
        a.Stream(ctx, pw)
        
        // Add checksum field
        mp.WriteField("checksum", hex.EncodeToString(h.Sum(nil)))
        mp.Close()
    }()
    
    // 6. Send request
    client := http.Client{Timeout: 0} // No timeout
    res, _ := client.Do(req)
    
    return io.ReadAll(res.Body)
}
Source: server/transfer/source.go:19-166

Progress Reporting

Progress is sent to websocket every 5 seconds:
go func(ctx context.Context, p *progress.Progress, tc *time.Ticker) {
    defer tc.Stop()
    
    for {
        select {
        case <-ctx.Done():
            return
        case <-tc.C:
            t.SendMessage("Uploading " + p.Progress(25))
        }
    }
}(ctx2, a.Progress(), time.NewTicker(5*time.Second))
Example Output:
[Transfer System] [Source Node]: Uploading [=========>          ] 45.2%
Source: server/transfer/source.go:37-48

Transfer Authentication

Token Header

Transfers use bearer token authentication:
req.Header.Set("Authorization", token)
Token Format: Provided by Panel during transfer initialization Source: server/transfer/source.go:58

Multipart Form

Archive sent as multipart form data:
mp := multipart.NewWriter(writer)
req.Header.Set("Content-Type", mp.FormDataContentType())

// Archive file
dest, err := mp.CreateFormFile("archive", "archive.tar.gz")

// Checksum field
mp.WriteField("checksum", hex.EncodeToString(h.Sum(nil)))
Fields:
  • archive - The tar.gz file
  • checksum - SHA256 hash of archive
Source: server/transfer/source.go:61-114

Receiving Transfers (Target Node)

Endpoint

POST /api/transfers
Target node receives:
  • Multipart form with archive file
  • SHA256 checksum for validation
  • Server configuration from Panel
Source: router/router.go:55

Transfer Cancellation

Cancel Method

func (t *Transfer) Cancel() {
    status := t.Status()
    
    // Don't cancel if already in final state
    if status == StatusCancelling ||
        status == StatusCancelled ||
        status == StatusCompleted ||
        status == StatusFailed {
        return
    }
    
    if t.cancel == nil {
        return
    }
    
    t.SetStatus(StatusCancelling)
    (*t.cancel)()
}
Source: server/transfer/transfer.go:77-92

API Endpoint

DELETE /api/servers/{server}/transfer
Cancels an in-progress transfer from source node. Source: router/router.go:86

Deleting Transfer (Target)

DELETE /api/transfers/{server}
Cancels/removes transfer on target node. Source: router/router.go:64

Status Management

Getting Status

func (t *Transfer) Status() Status {
    return t.status.Load()
}
Source: server/transfer/transfer.go:95-97

Setting Status

func (t *Transfer) SetStatus(s Status) {
    t.status.Store(s)
    t.Server.Events().Publish(server.TransferStatusEvent, s)
}
Websocket Event: transfer status Source: server/transfer/transfer.go:100-106

Transfer Messages

Sending Messages

func (t *Transfer) SendMessage(v string) {
    t.Server.Events().Publish(
        server.TransferLogsEvent,
        colorstring.Color("[yellow][bold]"+time.Now().Format(time.RFC1123)+" [Transfer System] [Source Node]:[default] "+v),
    )
}
Example Message:
[yellow][bold]Mon, 02 Jan 2006 15:04:05 MST [Transfer System] [Source Node]:[default] Preparing to stream server data to destination...
Source: server/transfer/transfer.go:109-114

Error Logging

func (t *Transfer) Error(err error, v string) {
    t.Log().WithError(err).Error(v)
    t.SendMessage(v)
}
Source: server/transfer/transfer.go:117-120

Logging

Transfer Logger

func (t *Transfer) Log() *log.Entry {
    if t.Server == nil {
        return log.WithField("subsystem", "transfer")
    }
    return t.Server.Log().WithField("subsystem", "transfer")
}
Log Fields:
  • server - Server UUID
  • subsystem - “transfer”
Source: server/transfer/transfer.go:123-129

Transfer Context

Context Management

func (t *Transfer) Context() context.Context {
    return t.ctx
}
Transfer context:
  • Cancellable via Cancel() method
  • Inherits from server context
  • Cancels all ongoing operations when triggered
Source: server/transfer/transfer.go:72-74

Websocket Events

Status Events

t.Server.Events().Publish(server.TransferStatusEvent, s)
Event Name: transfer status Payload: Status string (pending, processing, completed, etc.)

Log Events

t.Server.Events().Publish(server.TransferLogsEvent, message)
Event Name: transfer logs Payload: Formatted log message with timestamp

Server State During Transfer

Transfer Flag

func (s *Server) IsTransferring() bool {
    return s.transferring.Load()
}

func (s *Server) SetTransferring(state bool) {
    s.transferring.Store(state)
}
Source: server/install.go:146-152

Blocked Operations

Power actions are blocked during transfers:
func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error {
    if s.IsTransferring() {
        return ErrServerIsTransferring
    }
}
Source: server/power.go:57-64

HTTP Configuration

Client Settings

client := http.Client{Timeout: 0}
Timeout: Unlimited (0) - transfers can take hours Source: server/transfer/source.go:129

Request Headers

POST /api/transfers HTTP/1.1
Authorization: {token}
Content-Type: multipart/form-data; boundary={boundary}

Checksum Validation

SHA256 Calculation

h := sha256.New()
tee := io.TeeReader(src, h)

// ... copy data through tee reader ...

checksum := hex.EncodeToString(h.Sum(nil))
mp.WriteField("checksum", checksum)
Algorithm: SHA256 Format: Hexadecimal string Source: server/transfer/source.go:77-114

Error Handling

Common Transfer Errors

if res.StatusCode != http.StatusOK {
    return nil, fmt.Errorf("unexpected status code from destination: %d", res.StatusCode)
}
Error Conditions:
  • Failed to get server disk usage
  • Failed to create archive
  • Failed to stream archive
  • HTTP errors from target node
  • Context cancellation
  • Checksum validation failure
Source: server/transfer/source.go:135-161

Best Practices

Pre-Transfer

  1. Ensure sufficient disk space on target node
  2. Verify network connectivity between nodes
  3. Stop server before transfer (optional but recommended)
  4. Check server isn’t installing/restoring

During Transfer

  1. Monitor websocket events for progress
  2. Don’t attempt power actions
  3. Don’t delete server on either node
  4. Ensure stable network connection

Post-Transfer

  1. Verify server files on target node
  2. Start server to test functionality
  3. Clean up source node (handled by Panel)
  4. Update DNS/proxy configurations if needed

Build docs developers (and LLMs) love