11package wordpress
22
33import (
4+ "encoding/json"
45 "fmt"
56 "io"
67 "net/http"
78 "os"
9+ "strings"
10+ "time"
811)
912
1013// Client handles communication with the WordPress plugin
@@ -14,44 +17,139 @@ type Client struct {
1417 Client * http.Client
1518}
1619
20+ // WPError represents an error response from WordPress REST API
21+ type WPError struct {
22+ Code string `json:"code"`
23+ Message string `json:"message"`
24+ Data struct {
25+ Status int `json:"status"`
26+ } `json:"data"`
27+ }
28+
29+ // PingResponse represents the response from the ping endpoint
30+ type PingResponse struct {
31+ Success bool `json:"success"`
32+ Message string `json:"message"`
33+ Version string `json:"version"`
34+ PHPVersion string `json:"php_version"`
35+ WPVersion string `json:"wp_version"`
36+ CanUseShell bool `json:"can_use_shell"`
37+ DBConnected bool `json:"db_connected"`
38+ UploadDirWritable bool `json:"upload_dir_writable"`
39+ MemoryLimit string `json:"memory_limit"`
40+ MaxExecutionTime string `json:"max_execution_time"`
41+ }
42+
1743// NewClient creates a new WordPress client
1844func NewClient (url , key string ) * Client {
45+ // Remove trailing slash from URL
46+ url = strings .TrimRight (url , "/" )
1947 return & Client {
2048 BaseURL : url ,
2149 Key : key ,
22- Client : & http.Client {},
50+ Client : & http.Client {
51+ Timeout : 30 * time .Minute , // Long timeout for large DB exports
52+ },
2353 }
2454}
2555
56+ // Ping tests connectivity and returns server info
57+ func (c * Client ) Ping () (* PingResponse , error ) {
58+ apiURL := fmt .Sprintf ("%s/wp-json/dback/v1/ping" , c .BaseURL )
59+
60+ req , err := http .NewRequest ("GET" , apiURL , nil )
61+ if err != nil {
62+ return nil , fmt .Errorf ("failed to create request: %w" , err )
63+ }
64+ req .Header .Set ("X-DBACK-KEY" , c .Key )
65+
66+ // Use shorter timeout for ping
67+ client := & http.Client {Timeout : 30 * time .Second }
68+ resp , err := client .Do (req )
69+ if err != nil {
70+ return nil , fmt .Errorf ("connection failed: %w" , err )
71+ }
72+ defer resp .Body .Close ()
73+
74+ body , err := io .ReadAll (resp .Body )
75+ if err != nil {
76+ return nil , fmt .Errorf ("failed to read response: %w" , err )
77+ }
78+
79+ // Check for error response
80+ if resp .StatusCode != http .StatusOK {
81+ var wpErr WPError
82+ if json .Unmarshal (body , & wpErr ) == nil && wpErr .Code != "" {
83+ return nil , fmt .Errorf ("API error [%s]: %s" , wpErr .Code , wpErr .Message )
84+ }
85+ return nil , fmt .Errorf ("ping failed (HTTP %d): %s" , resp .StatusCode , string (body ))
86+ }
87+
88+ // Parse success response
89+ var pingResp PingResponse
90+ if err := json .Unmarshal (body , & pingResp ); err != nil {
91+ return nil , fmt .Errorf ("invalid response format: %s" , string (body ))
92+ }
93+
94+ return & pingResp , nil
95+ }
96+
2697// Export triggers a DB dump on WP and streams it to destPath
2798func (c * Client ) Export (destPath string , progressCallback func (int64 )) error {
2899 // URL: /wp-json/dback/v1/export
29- // We need to ensure BaseURL doesn't have trailing slash, and has wp-json if not provided?
30- // Usually user provides site root. We append /wp-json/...
31- // Let's assume BaseURL is site root.
32100 apiURL := fmt .Sprintf ("%s/wp-json/dback/v1/export" , c .BaseURL )
33101
34102 req , err := http .NewRequest ("GET" , apiURL , nil )
35103 if err != nil {
36- return err
104+ return fmt . Errorf ( "failed to create request: %w" , err )
37105 }
38106 req .Header .Set ("X-DBACK-KEY" , c .Key )
39107
40108 resp , err := c .Client .Do (req )
41109 if err != nil {
42- return err
110+ return fmt . Errorf ( "request failed: %w" , err )
43111 }
44112 defer resp .Body .Close ()
45113
114+ // Check Content-Type first - WordPress plugin sends application/gzip for success
115+ contentType := resp .Header .Get ("Content-Type" )
116+
117+ // If response is JSON, it's likely an error (even with 200 status)
118+ if strings .Contains (contentType , "application/json" ) {
119+ body , _ := io .ReadAll (resp .Body )
120+ var wpErr WPError
121+ if json .Unmarshal (body , & wpErr ) == nil && wpErr .Code != "" {
122+ return fmt .Errorf ("WordPress API error [%s]: %s" , wpErr .Code , wpErr .Message )
123+ }
124+ // Fallback if not a structured WP error
125+ return fmt .Errorf ("unexpected JSON response (status %d): %s" , resp .StatusCode , string (body ))
126+ }
127+
128+ // Handle non-200 status codes
46129 if resp .StatusCode != http .StatusOK {
47130 body , _ := io .ReadAll (resp .Body )
48- return fmt .Errorf ("export failed (status %d): %s" , resp .StatusCode , string (body ))
131+ bodyStr := string (body )
132+ if bodyStr == "" {
133+ bodyStr = "(empty response)"
134+ }
135+ return fmt .Errorf ("export failed (HTTP %d): %s" , resp .StatusCode , bodyStr )
136+ }
137+
138+ // Verify we're getting gzip content (expected for successful export)
139+ if ! strings .Contains (contentType , "application/gzip" ) && ! strings .Contains (contentType , "application/octet-stream" ) {
140+ body , _ := io .ReadAll (resp .Body )
141+ // Limit body preview to avoid huge error messages
142+ bodyPreview := string (body )
143+ if len (bodyPreview ) > 500 {
144+ bodyPreview = bodyPreview [:500 ] + "..."
145+ }
146+ return fmt .Errorf ("unexpected Content-Type '%s'. Expected 'application/gzip'. Response: %s" , contentType , bodyPreview )
49147 }
50148
51149 // Create local file
52150 out , err := os .Create (destPath )
53151 if err != nil {
54- return err
152+ return fmt . Errorf ( "failed to create local file: %w" , err )
55153 }
56154 defer out .Close ()
57155
@@ -65,8 +163,25 @@ func (c *Client) Export(destPath string, progressCallback func(int64)) error {
65163 },
66164 }
67165
68- _ , err = io .Copy (out , pr )
69- return err
166+ written , err := io .Copy (out , pr )
167+ if err != nil {
168+ // Clean up partial file
169+ out .Close ()
170+ os .Remove (destPath )
171+ return fmt .Errorf ("failed to download: %w" , err )
172+ }
173+
174+ // Validate file size - a valid gzip SQL dump should be at least a few hundred bytes
175+ // 100 bytes is extremely small and likely indicates an error
176+ if written < 100 {
177+ // Read what we got to provide better error message
178+ out .Close ()
179+ content , _ := os .ReadFile (destPath )
180+ os .Remove (destPath )
181+ return fmt .Errorf ("export file too small (%d bytes). This usually indicates an error on the server. Content: %s" , written , string (content ))
182+ }
183+
184+ return nil
70185}
71186
72187// Import uploads a SQL dump to WP
@@ -75,40 +190,58 @@ func (c *Client) Import(sourcePath string, progressCallback func(int64)) error {
75190
76191 file , err := os .Open (sourcePath )
77192 if err != nil {
78- return err
193+ return fmt . Errorf ( "failed to open source file: %w" , err )
79194 }
80195 defer file .Close ()
81196
82- fileInfo , _ := file .Stat ()
197+ fileInfo , err := file .Stat ()
198+ if err != nil {
199+ return fmt .Errorf ("failed to stat source file: %w" , err )
200+ }
83201 totalSize := fileInfo .Size ()
84202
85203 pr := & ProgressReader {
86204 Reader : file ,
87205 Callback : func (curr int64 ) {
88206 if progressCallback != nil {
89- progressCallback (curr ) // % can be calc with totalSize
207+ progressCallback (curr )
90208 }
91209 },
92210 }
93211
94212 // We send raw body (application/gzip)
95213 req , err := http .NewRequest ("POST" , apiURL , pr )
96214 if err != nil {
97- return err
215+ return fmt . Errorf ( "failed to create request: %w" , err )
98216 }
99217 req .Header .Set ("X-DBACK-KEY" , c .Key )
100218 req .Header .Set ("Content-Type" , "application/gzip" )
101219 req .ContentLength = totalSize // Important for streaming upload
102220
103221 resp , err := c .Client .Do (req )
104222 if err != nil {
105- return err
223+ return fmt . Errorf ( "request failed: %w" , err )
106224 }
107225 defer resp .Body .Close ()
108226
227+ // Read response body
228+ body , _ := io .ReadAll (resp .Body )
229+
230+ // Check for JSON error response (even on 200)
231+ contentType := resp .Header .Get ("Content-Type" )
232+ if strings .Contains (contentType , "application/json" ) {
233+ var wpErr WPError
234+ if json .Unmarshal (body , & wpErr ) == nil && wpErr .Code != "" {
235+ return fmt .Errorf ("WordPress API error [%s]: %s" , wpErr .Code , wpErr .Message )
236+ }
237+ }
238+
109239 if resp .StatusCode != http .StatusOK {
110- body , _ := io .ReadAll (resp .Body )
111- return fmt .Errorf ("import failed (status %d): %s" , resp .StatusCode , string (body ))
240+ bodyStr := string (body )
241+ if bodyStr == "" {
242+ bodyStr = "(empty response)"
243+ }
244+ return fmt .Errorf ("import failed (HTTP %d): %s" , resp .StatusCode , bodyStr )
112245 }
113246
114247 return nil
0 commit comments