Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pterodactyl/wings/llms.txt
Use this file to discover all available pages before exploring further.
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
Transfers use bearer token authentication:
req.Header.Set("Authorization", token)
Token Format: Provided by Panel during transfer initialization
Source: server/transfer/source.go:58
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
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
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
- Ensure sufficient disk space on target node
- Verify network connectivity between nodes
- Stop server before transfer (optional but recommended)
- Check server isn’t installing/restoring
During Transfer
- Monitor websocket events for progress
- Don’t attempt power actions
- Don’t delete server on either node
- Ensure stable network connection
Post-Transfer
- Verify server files on target node
- Start server to test functionality
- Clean up source node (handled by Panel)
- Update DNS/proxy configurations if needed