Skip to content

Commit e9b9367

Browse files
committed
File Manager API fixes + Additional Softaculous support
1 parent 84b3f7b commit e9b9367

File tree

4 files changed

+184
-59
lines changed

4 files changed

+184
-59
lines changed

file.go

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,11 @@ type FileMetadata struct {
3636

3737
// CreateDirectory (user) creates the given path, including any missing parent directories.
3838
func (c *UserContext) CreateDirectory(path string) error {
39-
var response apiGenericResponseN
40-
4139
body := map[string]string{
4240
"path": path,
4341
}
4442

45-
if _, err := c.makeRequestNew(http.MethodPost, "/api/filemanager-actions/mkdir", body, &response); err != nil {
43+
if _, err := c.makeRequestNew(http.MethodPost, "filemanager-actions/mkdir", body, nil); err != nil {
4644
return err
4745
}
4846

@@ -101,31 +99,34 @@ func (c *UserContext) DownloadFileToDisk(filePath string, outputPath string) err
10199
return os.WriteFile(outputPath, response, 0o644)
102100
}
103101

104-
// ExtractFile unzips the given file path on the server.
105-
func (c *UserContext) ExtractFile(filePath string, file string) error {
106-
var response apiGenericResponse
107-
108-
// Prepend / to the filePath if it doesn't exist.
109-
if filePath[0] != '/' {
110-
filePath = "/" + filePath
102+
// ExtractFile (user) unzips the given file path on the server.
103+
func (c *UserContext) ExtractFile(destinationDir string, source string, mergeAndOverwrite bool) error {
104+
if destinationDir == "" || source == "" {
105+
return fmt.Errorf("no destination directory or source provided")
111106
}
112107

113-
// Prepend / to the file if it doesn't exist.
114-
if file[0] != '/' {
115-
file = "/" + file
108+
// Prepend / to the filePath if necessary.
109+
if destinationDir[0] != '/' {
110+
destinationDir = "/" + destinationDir
116111
}
117112

118-
body := url.Values{}
119-
body.Set("directory", filePath)
120-
body.Set("path", file)
121-
body.Set("page", "2")
113+
// Prepend / to the file if necessary.
114+
if source[0] != '/' {
115+
source = "/" + source
116+
}
122117

123-
if _, err := c.makeRequestOld(http.MethodPost, "FILE_MANAGER?action=extract", body, &response); err != nil {
124-
return err
118+
body := struct {
119+
DestinationDir string `json:"destinationDir"`
120+
MergeAndOverwrite bool `json:"mergeAndOverwrite"`
121+
Source string `json:"source"`
122+
}{
123+
DestinationDir: destinationDir,
124+
MergeAndOverwrite: mergeAndOverwrite,
125+
Source: source,
125126
}
126127

127-
if response.Success != "File Extracted" {
128-
return fmt.Errorf("failed to extract file: %v", response.Result)
128+
if _, err := c.makeRequestNew(http.MethodPost, "filemanager-actions/extract-archive", body, nil); err != nil {
129+
return err
129130
}
130131

131132
return nil
@@ -135,7 +136,7 @@ func (c *UserContext) ExtractFile(filePath string, file string) error {
135136
func (c *UserContext) GetFileMetadata(filePath string) (*FileMetadata, error) {
136137
var response *FileMetadata
137138

138-
if _, err := c.makeRequestNew(http.MethodGet, "/api/filemanager/metadata?path="+filePath, nil, &response); err != nil {
139+
if _, err := c.makeRequestNew(http.MethodGet, "filemanager/metadata?path="+filePath, nil, &response); err != nil {
139140
return nil, err
140141
}
141142

@@ -144,15 +145,17 @@ func (c *UserContext) GetFileMetadata(filePath string) (*FileMetadata, error) {
144145

145146
// MovePath (user) moves the given file or directory to the new destination.
146147
func (c *UserContext) MovePath(source string, destination string, overwrite bool) error {
147-
var response apiGenericResponseN
148-
149-
body := map[string]string{
150-
"destination": destination,
151-
"overwrite": fmt.Sprintf("%t", overwrite),
152-
"source": source,
148+
body := struct {
149+
Destination string `json:"destination"`
150+
Overwrite bool `json:"overwrite"`
151+
Source string `json:"source"`
152+
}{
153+
Destination: destination,
154+
Overwrite: overwrite,
155+
Source: source,
153156
}
154157

155-
if _, err := c.makeRequestNew(http.MethodPost, "/api/filemanager-actions/move", body, &response); err != nil {
158+
if _, err := c.makeRequestNew(http.MethodPost, "filemanager-actions/move", body, nil); err != nil {
156159
return err
157160
}
158161

http.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ import (
1111
"time"
1212

1313
"github.com/goccy/go-json"
14-
"github.com/spf13/cast"
1514
)
1615

1716
type httpDebug struct {
18-
Body string
19-
Code int
20-
Endpoint string
21-
Start time.Time
17+
Body string
18+
BodyTruncated bool
19+
Code int
20+
Endpoint string
21+
Start time.Time
2222
}
2323

2424
// makeRequest is the underlying function for HTTP requests. It handles debugging statements, and simple error handling
2525
func (c *UserContext) makeRequest(req *http.Request) ([]byte, error) {
2626
debug := httpDebug{
27-
Endpoint: req.URL.Path,
27+
Endpoint: getPathWithQuery(req),
2828
Start: time.Now(),
2929
}
3030
defer c.api.printDebugHTTP(&debug)
@@ -57,7 +57,8 @@ func (c *UserContext) makeRequest(req *http.Request) ([]byte, error) {
5757

5858
if c.api.debug {
5959
if len(responseBytes) > 32768 {
60-
debug.Body = "body too long for debug: " + cast.ToString(len(responseBytes))
60+
debug.BodyTruncated = true
61+
debug.Body = string(responseBytes[:32768])
6162
} else {
6263
debug.Body = string(responseBytes)
6364
}
@@ -190,6 +191,23 @@ func (c *UserContext) uploadFile(method string, endpoint string, data []byte, ob
190191

191192
func (a *API) printDebugHTTP(debug *httpDebug) {
192193
if a.debug {
193-
fmt.Printf("\nENDPOINT: %v\nSTATUS CODE: %v\nTIME STARTED: %v\nTIME ENDED: %v\nTIME TAKEN: %v\nBODY: %v\n", debug.Endpoint, debug.Code, debug.Start, time.Now(), time.Since(debug.Start), debug.Body)
194+
var bodyTruncated string
195+
if debug.BodyTruncated {
196+
bodyTruncated = " (truncated)"
197+
}
198+
199+
fmt.Printf("\nENDPOINT: %v\nSTATUS CODE: %v\nTIME STARTED: %v\nTIME ENDED: %v\nTIME TAKEN: %v\nBODY%s: %v\n", debug.Endpoint, debug.Code, debug.Start, time.Now(), time.Since(debug.Start), bodyTruncated, debug.Body)
200+
}
201+
}
202+
203+
func getPathWithQuery(req *http.Request) string {
204+
if req == nil {
205+
return ""
206+
}
207+
208+
if req.URL.RawQuery != "" {
209+
return req.URL.Path + "?" + req.URL.RawQuery
194210
}
211+
212+
return req.URL.Path
195213
}

plugins_softaculous.go

Lines changed: 124 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,48 @@ const (
1818
SoftaculousScriptIDWordPress = 26
1919
)
2020

21-
type SoftaculousScript struct {
22-
AdminEmail string `url:"admin_email"`
23-
AdminPassword string `url:"admin_pass"`
24-
AdminUsername string `url:"admin_username"`
25-
AutoUpgrade bool `url:"en_auto_upgrade"`
26-
AutoUpgradePlugins bool `url:"auto_upgrade_plugins"`
27-
AutoUpgradeThemes bool `url:"auto_upgrade_plugins"`
28-
DatabaseName string `url:"softdb"`
29-
DatabasePrefix string `url:"dbprefix"` // optional
30-
Directory string `url:"softdirectory"`
31-
Domain string `url:"softdomain"`
32-
Language string `url:"language"`
33-
NotifyOnInstall bool `url:"noemail"`
34-
NotifyOnUpdate bool `url:"disable_notify_update"`
35-
OverwriteExisting bool `url:"overwrite_existing"`
36-
Protocol string `url:"softproto"`
37-
SiteDescription string `url:"site_desc"`
38-
SiteName string `json:"site_name"`
39-
}
21+
type (
22+
SoftaculousInstallation struct {
23+
ID string `json:"insid"`
24+
ScriptID int `json:"sid"`
25+
Ver string `json:"ver"`
26+
ITime int `json:"itime"`
27+
Path string `json:"softpath"`
28+
URL string `json:"softurl"`
29+
Domain string `json:"softdomain"`
30+
FileIndex any `json:"fileindex"` // Sometimes a string slice, other times a map.
31+
SiteName string `json:"site_name"`
32+
SoftDB string `json:"softdb"`
33+
SoftDBuser string `json:"softdbuser"`
34+
SoftDBhost string `json:"softdbhost"`
35+
SoftDBpass string `json:"softdbpass"`
36+
DBCreated bool `json:"dbcreated"`
37+
DBPrefix string `json:"dbprefix"`
38+
ImportSrc string `json:"import_src"`
39+
DisplaySoftDBPass string `json:"display_softdbpass"`
40+
ScriptName string `json:"script_name"`
41+
}
42+
43+
SoftaculousScript struct {
44+
AdminEmail string `url:"admin_email"`
45+
AdminPassword string `url:"admin_pass"`
46+
AdminUsername string `url:"admin_username"`
47+
AutoUpgrade bool `url:"en_auto_upgrade"`
48+
AutoUpgradePlugins bool `url:"auto_upgrade_plugins"`
49+
AutoUpgradeThemes bool `url:"auto_upgrade_plugins"`
50+
DatabaseName string `url:"softdb"`
51+
DatabasePrefix string `url:"dbprefix"` // Optional.
52+
Directory string `url:"softdirectory"`
53+
Domain string `url:"softdomain"`
54+
Language string `url:"language"`
55+
NotifyOnInstall bool `url:"noemail"`
56+
NotifyOnUpdate bool `url:"disable_notify_update"`
57+
OverwriteExisting bool `url:"overwrite_existing"`
58+
Protocol string `url:"softproto"`
59+
SiteDescription string `url:"site_desc"`
60+
SiteName string `json:"site_name"`
61+
}
62+
)
4063

4164
func (s *SoftaculousScript) Parse() (url.Values, error) {
4265
if err := s.Validate(); err != nil {
@@ -160,7 +183,7 @@ func (c *UserContext) SoftaculousInstallScript(script *SoftaculousScript, script
160183

161184
body.Set("softsubmit", "1")
162185

163-
// Softaculous requires a genuine session ID
186+
// Softaculous requires a genuine session ID.
164187
if c.sessionID == "" {
165188
if err = c.CreateSession(); err != nil {
166189
return fmt.Errorf("failed to create user session: %w", err)
@@ -177,3 +200,84 @@ func (c *UserContext) SoftaculousInstallScript(script *SoftaculousScript, script
177200

178201
return nil
179202
}
203+
204+
// SoftaculousListInstallations lists all installations accessible to the authenticated user.
205+
func (c *UserContext) SoftaculousListInstallations() ([]*SoftaculousInstallation, error) {
206+
response := struct {
207+
Error map[string]string `json:"error"`
208+
Installations map[string]map[string]*SoftaculousInstallation `json:"installations"`
209+
}{
210+
Error: make(map[string]string),
211+
Installations: make(map[string]map[string]*SoftaculousInstallation),
212+
}
213+
214+
// Softaculous requires a genuine session ID
215+
if c.sessionID == "" {
216+
if err := c.CreateSession(); err != nil {
217+
return nil, fmt.Errorf("failed to create user session: %w", err)
218+
}
219+
}
220+
221+
if _, err := c.makeRequestOld(http.MethodPost, "PLUGINS/softaculous/index.raw?act=installations&api=json", nil, &response); err != nil {
222+
return nil, err
223+
}
224+
225+
if len(response.Error) > 0 {
226+
return nil, fmt.Errorf("failed to uninstall script: %v", response.Error)
227+
}
228+
229+
installations := make([]*SoftaculousInstallation, 0, len(response.Installations))
230+
for _, userInstallations := range response.Installations {
231+
for _, installation := range userInstallations {
232+
installations = append(installations, installation)
233+
}
234+
}
235+
236+
return installations, nil
237+
}
238+
239+
// SoftaculousUninstallScript calls Softaculous's install script API endpoint.
240+
//
241+
// Docs: https://www.softaculous.com/docs/api/remote-api/#remove-an-installed-script
242+
func (c *UserContext) SoftaculousUninstallScript(installID string, deleteFiles bool, deleteDB bool) error {
243+
if installID == "" {
244+
return errors.New("missing install id")
245+
}
246+
247+
response := struct {
248+
Error map[string]string `json:"error"`
249+
}{
250+
Error: make(map[string]string),
251+
}
252+
253+
body := url.Values{}
254+
body.Set("noemail", "1")
255+
body.Set("removeins", "1")
256+
257+
if deleteFiles {
258+
body.Set("remove_dir", "1")
259+
body.Set("remove_datadir", "1")
260+
}
261+
262+
if deleteDB {
263+
body.Set("remove_db", "1")
264+
body.Set("remove_dbuser", "1")
265+
}
266+
267+
// Softaculous requires a genuine session ID
268+
if c.sessionID == "" {
269+
if err := c.CreateSession(); err != nil {
270+
return fmt.Errorf("failed to create user session: %w", err)
271+
}
272+
}
273+
274+
if _, err := c.makeRequestOld(http.MethodPost, "PLUGINS/softaculous/index.raw?act=remove&insid="+installID+"&api=json", body, &response); err != nil {
275+
return err
276+
}
277+
278+
if len(response.Error) > 0 {
279+
return fmt.Errorf("failed to uninstall script: %v", response.Error)
280+
}
281+
282+
return nil
283+
}

subdomain.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ func (c *UserContext) DeleteSubdomains(deleteData bool, domain string, subdomain
5656
return err
5757
}
5858

59-
if response.Success != "Domain Deletion Results" {
60-
return fmt.Errorf("failed to delete domain: %v", response.Result)
59+
if response.Success != "Subdomains deleted" {
60+
return fmt.Errorf("failed to delete subdomain: %v", response.Result)
6161
}
6262

6363
return nil

0 commit comments

Comments
 (0)