From 3d9538dcd86d1242ec1a19d7556c00a810af73af Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Mon, 25 Mar 2024 22:13:20 +0000 Subject: [PATCH] Transition all `Database` functions to use the new DA API Support database imports Support getting database processes --- auth.go | 8 ++- database.go | 190 +++++++++++++++++++++++++++++++++++---------------- http.go | 34 +++++++-- user.go | 157 ++++++++++++++++++++++-------------------- wordpress.go | 10 ++- 5 files changed, 257 insertions(+), 142 deletions(-) diff --git a/auth.go b/auth.go index e87fa4d..819f664 100644 --- a/auth.go +++ b/auth.go @@ -32,13 +32,13 @@ type ( ) func (c *UserContext) CreateLoginURL(loginKeyURL *LoginKeyURL) (*LoginKeyURL, error) { - var response LoginKeyURL + var response *LoginKeyURL - if _, err := c.api.makeRequestN(http.MethodPost, "login-keys/urls", c.credentials, loginKeyURL, &response); err != nil { + if _, err := c.api.makeRequestN(http.MethodPost, "login-keys/urls", c.credentials, loginKeyURL, response); err != nil { return nil, fmt.Errorf("failed to create login URL: %v", err) } - return &response, nil + return response, nil } func (c *UserContext) GetLoginURLs() ([]*LoginKeyURL, error) { @@ -65,6 +65,8 @@ func (c *AdminContext) GetLoginHistory() ([]*LoginHistory, error) { return loginHistory, nil } +// GetMyUsername returns the current user's username. This is particularly useful when logging in as another user, as it +// trims the admin/reseller username automatically func (c *UserContext) GetMyUsername() string { // if user is logged in via reseller, we need to remove the reseller username from the context's username if strings.Contains(c.credentials.username, "|") { diff --git a/database.go b/database.go index 9633226..37caf59 100644 --- a/database.go +++ b/database.go @@ -1,14 +1,11 @@ package directadmin import ( - "errors" "fmt" "net/http" - "net/url" "os" + "strconv" "strings" - - "github.com/spf13/cast" ) const ( @@ -16,53 +13,83 @@ const ( DatabaseFormatSql = DatabaseFormat("sql") ) -type DatabaseFormat string - -type Database struct { - Name string `json:"name" yaml:"name"` - Size int `json:"size" yaml:"size"` - Users int `json:"users" yaml:"users"` -} +type ( + DatabaseFormat string + + Database struct { + Name string `json:"database"` + DefaultCharset string `json:"defaultCharset"` + DefaultCollation string `json:"defaultCollation"` + DefinerIssues int `json:"definerIssues"` + EventCount int `json:"eventCount"` + RoutineCount int `json:"routineCount"` + SizeBytes int `json:"sizeBytes"` + TableCount int `json:"tableCount"` + TriggerCount int `json:"triggerCount"` + UserCount int `json:"userCount"` + ViewCount int `json:"viewCount"` + } -func (c *UserContext) CreateDatabase(name string, dbUser string, dbPassword string) error { - var response apiGenericResponse + DatabaseProcess struct { + Command string `json:"command"` + Database string `json:"database"` + Host string `json:"host"` + Id int `json:"id"` + Info string `json:"info"` + State string `json:"state"` + Time int `json:"time"` + User string `json:"user"` + } - body := url.Values{} - body.Set("name", name) - body.Set("user", dbUser) - body.Set("passwd", dbPassword) - body.Set("passwd2", dbPassword) + DatabaseUser struct { + HostPatterns []string `json:"hostPatterns"` + Password string `json:"password"` + User string `json:"dbuser"` + } - if _, err := c.api.makeRequest(http.MethodPost, "API_DATABASES?action=create", c.credentials, body, &response); err != nil { - return err + DatabaseWithUser struct { + Database + Password string `json:"password"` + User string `json:"dbuser"` } +) + +func (c *UserContext) CreateDatabase(database *Database) error { + database.Name = c.addUsernamePrefix(database.Name) - if response.Success != "Database Created" { - return fmt.Errorf("failed to create database: %v", response.Result) + if _, err := c.api.makeRequestN(http.MethodPost, "db-manage/create-db", c.credentials, database, nil); err != nil { + return err } return nil } -func (c *UserContext) DeleteDatabases(names ...string) error { - var response apiGenericResponse +func (c *UserContext) CreateDatabaseWithUser(database *DatabaseWithUser) error { + database.Name = c.addUsernamePrefix(database.Name) + database.User = c.addUsernamePrefix(database.User) - body := url.Values{} + if _, err := c.api.makeRequestN(http.MethodPost, "db-manage/create-db-with-user", c.credentials, database, nil); err != nil { + return err + } - for index, name := range names { - if !strings.Contains(name, c.GetMyUsername()+"_") { - name = c.GetMyUsername() + "_" + name - } + return nil +} - body.Set("select"+cast.ToString(index), name) - } +func (c *UserContext) CreateDatabaseUser(databaseUser *DatabaseUser) error { + databaseUser.User = c.addUsernamePrefix(databaseUser.User) - if _, err := c.api.makeRequest(http.MethodPost, "API_DATABASES?action=delete", c.credentials, body, &response); err != nil { + if _, err := c.api.makeRequestN(http.MethodPost, "db-manage/create-user", c.credentials, databaseUser, nil); err != nil { return err } - if response.Success != "Databases Deleted" { - return fmt.Errorf("failed to delete database: %v", response.Result) + return nil +} + +func (c *UserContext) DeleteDatabase(databaseName string) error { + databaseName = c.addUsernamePrefix(databaseName) + + if _, err := c.api.makeRequestN(http.MethodDelete, "db-manage/databases/"+databaseName, c.credentials, nil, nil); err != nil { + return err } return nil @@ -99,43 +126,88 @@ func (c *UserContext) DownloadDatabase(name string, format DatabaseFormat, fileP return nil } -// GetDatabases (user) returns an array of the session user's databases -func (c *UserContext) GetDatabases() ([]Database, error) { - var databases []Database - rawDatabases := struct { - Databases map[string]struct { - Name string `json:"database"` - Size string `json:"size"` - Users string `json:"nusers"` - } - }{} +// ExportDatabase (user) returns an export of the given database +func (c *UserContext) ExportDatabase(databaseName string, gzip bool) ([]byte, error) { + databaseName = c.addUsernamePrefix(databaseName) - if _, err := c.api.makeRequest(http.MethodGet, "DB", c.credentials, nil, &rawDatabases); err != nil { + export, err := c.api.makeRequestN(http.MethodGet, "db-manage/databases/"+databaseName+"/export?gzip="+strconv.FormatBool(gzip), c.credentials, nil, nil) + if err != nil { return nil, err } - for id, database := range rawDatabases.Databases { - if id != "info" { - databases = append(databases, Database{ - Name: strings.Replace(database.Name, c.credentials.username+"_", "", 1), - Size: cast.ToInt(database.Size), - Users: cast.ToInt(database.Users), - }) - } + return export, nil +} + +// GetDatabase (user) returns the given database +func (c *UserContext) GetDatabase(databaseName string) (*Database, error) { + databaseName = c.addUsernamePrefix(databaseName) + + var database Database + + if _, err := c.api.makeRequestN(http.MethodGet, "db-show/databases/"+databaseName, c.credentials, nil, &database); err != nil { + return nil, err } - if len(databases) == 0 { - return nil, errors.New("no databases were found") + return &database, nil +} + +// GetDatabases (user) returns an array of the session user's databases +func (c *UserContext) GetDatabases() ([]*Database, error) { + var databases []*Database + + if _, err := c.api.makeRequestN(http.MethodGet, "db-show/databases", c.credentials, nil, &databases); err != nil { + return nil, err } return databases, nil } -// ListDatabases (user) returns an array of all databases for the session user -func (c *UserContext) ListDatabases() (databaseList []string, err error) { - if _, err = c.api.makeRequest(http.MethodGet, "API_DATABASES", c.credentials, nil, &databaseList); err != nil { +// GetDatabaseProcesses (admin) returns an array of current database processes +func (c *UserContext) GetDatabaseProcesses() ([]*DatabaseProcess, error) { + var databaseProcesses []*DatabaseProcess + + if _, err := c.api.makeRequestN(http.MethodGet, "db-monitor/processes", c.credentials, nil, &databaseProcesses); err != nil { return nil, err } - return databaseList, nil + return databaseProcesses, nil +} + +// ImportDatabase (user) imports the given database export into the given database +func (c *UserContext) ImportDatabase(databaseName string, emptyExistingDatabase bool, sql []byte) error { + databaseName = c.addUsernamePrefix(databaseName) + + if _, err := c.api.makeRequestN(http.MethodPost, "db-manage/databases/"+databaseName+"/import?clean="+strconv.FormatBool(emptyExistingDatabase), c.credentials, sql, nil, true); err != nil { + return err + } + + return nil +} + +// UpdateDatabaseUserHosts (user) updates the given database user's hosts +func (c *UserContext) UpdateDatabaseUserHosts(username string, hosts []string) error { + username = c.addUsernamePrefix(username) + + if _, err := c.api.makeRequestN(http.MethodPost, "db-manage/users/"+username+"/change-hosts", c.credentials, hosts, nil); err != nil { + return err + } + + return nil +} + +// UpdateDatabaseUserPassword (user) updates the given database user's password +func (c *UserContext) UpdateDatabaseUserPassword(username string, password string) error { + username = c.addUsernamePrefix(username) + + newPassword := struct { + NewPassword string `json:"newPassword"` + }{ + password, + } + + if _, err := c.api.makeRequestN(http.MethodPost, "db-manage/users/"+username+"/change-password", c.credentials, newPassword, nil); err != nil { + return err + } + + return nil } diff --git a/http.go b/http.go index ca7e947..5e326c6 100644 --- a/http.go +++ b/http.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "mime/multipart" "net/http" "net/url" "os" @@ -80,16 +81,39 @@ func (a *API) makeRequest(method string, endpoint string, accountCredentials cre } // makeRequestN supports DA's new API -func (a *API) makeRequestN(method string, endpoint string, accountCredentials credentials, body any, object any) ([]byte, error) { +func (a *API) makeRequestN(method string, endpoint string, accountCredentials credentials, body any, object any, uploadFile ...bool) ([]byte, error) { defer a.queryTime(endpoint, time.Now()) var err error var requestBytes, responseBytes []byte + contentType := "application/json" + if body != nil { - requestBytes, err = json.Marshal(body) - if err != nil { - return nil, fmt.Errorf("error marshalling body: %w", err) + if len(uploadFile) != 1 { + requestBytes, err = json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("error marshalling body: %w", err) + } + } else { + var byteBuffer bytes.Buffer + multipartWriter := multipart.NewWriter(&byteBuffer) + + formFile, err := multipartWriter.CreateFormFile("sqlfile", "filename") + if err != nil { + return nil, err + } + + if _, err = formFile.Write(body.([]byte)); err != nil { + return nil, err + } + + if err = multipartWriter.Close(); err != nil { + return nil, err + } + + requestBytes = byteBuffer.Bytes() + contentType = multipartWriter.FormDataContentType() } } @@ -99,7 +123,7 @@ func (a *API) makeRequestN(method string, endpoint string, accountCredentials cr } req.Header.Set("Accept", "application/json") - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", contentType) req.SetBasicAuth(accountCredentials.username, accountCredentials.passkey) resp, err := a.httpClient.Do(req) diff --git a/user.go b/user.go index 858cfee..45a4dd4 100644 --- a/user.go +++ b/user.go @@ -5,84 +5,87 @@ import ( "fmt" "net/http" "net/url" + "strings" "time" ) -type User struct { - Config UserConfig `json:"config"` - Usage UserUsage `json:"usage"` -} +type ( + User struct { + Config UserConfig `json:"config"` + Usage UserUsage `json:"usage"` + } -type UserContext struct { - api *API - credentials credentials - User User -} + UserContext struct { + api *API + credentials credentials + User User + } -type UserConfig struct { - AftpEnabled bool `json:"aftpEnabled" yaml:"aftpEnabled"` - ApiAccessWithPasswordEnabled bool `json:"apiAccessWithPasswordEnabled" yaml:"apiAccessWithPasswordEnabled"` - CatchAllEnabled bool `json:"catchAllEnabled" yaml:"catchAllEnabled"` - CgiEnabled bool `json:"cgiEnabled" yaml:"cgiEnabled"` - ClamAvEnabled bool `json:"clamAvEnabled" yaml:"clamAvEnabled"` - Created time.Time `json:"created" yaml:"created"` - Creator string `json:"creator" yaml:"creator"` - CronEnabled bool `json:"cronEnabled" yaml:"cronEnabled"` - Domain string `json:"domain" yaml:"domain"` - DNSEnabled bool `json:"dnsEnabled" yaml:"dnsEnabled"` - Email string `json:"email" yaml:"email"` - GitEnabled bool `json:"gitEnabled" yaml:"gitEnabled"` - IpAddresses []string `json:"ipAddresses" yaml:"ipAddresses"` - JailEnabled bool `json:"jailEnabled" yaml:"jailEnabled"` - Language string `json:"language" yaml:"language"` - LoginKeysEnabled bool `json:"loginKeysEnabled" yaml:"loginKeysEnabled"` - NginxEnabled bool `json:"nginxEnabled" yaml:"nginxEnabled"` - NotifyOnQuestionFailures bool `json:"notifyOnQuestionFailures" yaml:"notifyOnQuestionFailures"` - NotifyOnTwoFactorFailures bool `json:"notifyOnTwoFactorFailures" yaml:"notifyOnTwoFactorFailures"` - Ns1 string `json:"ns1" yaml:"ns1"` - Ns2 string `json:"ns2" yaml:"ns2"` - Package string `json:"package" yaml:"package"` - PhpEnabled bool `json:"phpEnabled" yaml:"phpEnabled"` - RedisEnabled bool `json:"redisEnabled" yaml:"redisEnabled"` - SecurityQuestionsEnabled bool `json:"securityQuestionsEnabled" yaml:"securityQuestionsEnabled"` - Skin string `json:"skin" yaml:"skin"` - SpamEnabled bool `json:"spamEnabled" yaml:"spamEnabled"` - SshEnabled bool `json:"sshEnabled" yaml:"sshEnabled"` - SslEnabled bool `json:"sslEnabled" yaml:"sslEnabled"` - Suspended bool `json:"suspended" yaml:"suspended"` - SuspendAtLimitEnabled bool `json:"suspendAtLimitEnabled" yaml:"suspendAtLimitEnabled"` - SysInfoEnabled bool `json:"sysInfoEnabled" yaml:"sysInfoEnabled"` - TwoFactorAuthenticationEnabled bool `json:"twoFactorAuthenticationEnabled" yaml:"twoFactorAuthenticationEnabled"` - Username string `json:"username" yaml:"username"` - UserType string `json:"userType" yaml:"userType"` - WordPressToolkitEnabled bool `json:"wordPressToolkitEnabled" yaml:"wordPressToolkitEnabled"` -} + UserConfig struct { + AftpEnabled bool `json:"aftpEnabled" yaml:"aftpEnabled"` + ApiAccessWithPasswordEnabled bool `json:"apiAccessWithPasswordEnabled" yaml:"apiAccessWithPasswordEnabled"` + CatchAllEnabled bool `json:"catchAllEnabled" yaml:"catchAllEnabled"` + CgiEnabled bool `json:"cgiEnabled" yaml:"cgiEnabled"` + ClamAvEnabled bool `json:"clamAvEnabled" yaml:"clamAvEnabled"` + Created time.Time `json:"created" yaml:"created"` + Creator string `json:"creator" yaml:"creator"` + CronEnabled bool `json:"cronEnabled" yaml:"cronEnabled"` + Domain string `json:"domain" yaml:"domain"` + DNSEnabled bool `json:"dnsEnabled" yaml:"dnsEnabled"` + Email string `json:"email" yaml:"email"` + GitEnabled bool `json:"gitEnabled" yaml:"gitEnabled"` + IpAddresses []string `json:"ipAddresses" yaml:"ipAddresses"` + JailEnabled bool `json:"jailEnabled" yaml:"jailEnabled"` + Language string `json:"language" yaml:"language"` + LoginKeysEnabled bool `json:"loginKeysEnabled" yaml:"loginKeysEnabled"` + NginxEnabled bool `json:"nginxEnabled" yaml:"nginxEnabled"` + NotifyOnQuestionFailures bool `json:"notifyOnQuestionFailures" yaml:"notifyOnQuestionFailures"` + NotifyOnTwoFactorFailures bool `json:"notifyOnTwoFactorFailures" yaml:"notifyOnTwoFactorFailures"` + Ns1 string `json:"ns1" yaml:"ns1"` + Ns2 string `json:"ns2" yaml:"ns2"` + Package string `json:"package" yaml:"package"` + PhpEnabled bool `json:"phpEnabled" yaml:"phpEnabled"` + RedisEnabled bool `json:"redisEnabled" yaml:"redisEnabled"` + SecurityQuestionsEnabled bool `json:"securityQuestionsEnabled" yaml:"securityQuestionsEnabled"` + Skin string `json:"skin" yaml:"skin"` + SpamEnabled bool `json:"spamEnabled" yaml:"spamEnabled"` + SshEnabled bool `json:"sshEnabled" yaml:"sshEnabled"` + SslEnabled bool `json:"sslEnabled" yaml:"sslEnabled"` + Suspended bool `json:"suspended" yaml:"suspended"` + SuspendAtLimitEnabled bool `json:"suspendAtLimitEnabled" yaml:"suspendAtLimitEnabled"` + SysInfoEnabled bool `json:"sysInfoEnabled" yaml:"sysInfoEnabled"` + TwoFactorAuthenticationEnabled bool `json:"twoFactorAuthenticationEnabled" yaml:"twoFactorAuthenticationEnabled"` + Username string `json:"username" yaml:"username"` + UserType string `json:"userType" yaml:"userType"` + WordPressToolkitEnabled bool `json:"wordPressToolkitEnabled" yaml:"wordPressToolkitEnabled"` + } -type UserUsage struct { - BandwidthQuota int `json:"bandwidthQuota" yaml:"bandwidthQuota"` - BandwidthUsage int `json:"bandwidthUsage" yaml:"bandwidthUsage"` - DbQuota int `json:"dbQuota" yaml:"dbQuota"` - DbUsage int `json:"dbUsage" yaml:"dbUsage"` - DiskQuota int `json:"diskQuota" yaml:"diskQuota"` - DiskUsage int `json:"diskUsage" yaml:"diskUsage"` - Domains []domainUsage `json:"domains" yaml:"domains"` - DomainPointersQuota int `json:"domainPointersQuota" yaml:"domainPointersQuota"` - DomainPointersUsage int `json:"domainPointersUsage" yaml:"domainPointersUsage"` - DomainQuota int `json:"domainQuota" yaml:"domainQuota"` - DomainUsage int `json:"domainUsage" yaml:"domainUsage"` - EmailQuota int `json:"emailQuota" yaml:"emailQuota"` - EmailUsage int `json:"emailUsage" yaml:"emailUsage"` - EmailForwardersQuota int `json:"emailForwardersQuota" yaml:"emailForwardersQuota"` - EmailForwardersUsage int `json:"emailForwardersUsage" yaml:"emailForwardersUsage"` - EmailMailingListQuota int `json:"emailMailingListQuota" yaml:"emailMailingListQuota"` - EmailMailingListUsage int `json:"emailMailingListUsage" yaml:"emailMailingListUsage"` - FtpQuota int `json:"ftpQuota" yaml:"ftpQuota"` - FtpUsage int `json:"ftpUsed" yaml:"ftpUsed"` - InodeQuota int `json:"inodeQuota" yaml:"inodeQuota"` - InodeUsage int `json:"inodeUsage" yaml:"inodeUsage"` - SubdomainQuota int `json:"subdomainQuota" yaml:"subdomainQuota"` - SubdomainUsage int `json:"subdomainUsage" yaml:"subdomainUsage"` -} + UserUsage struct { + BandwidthQuota int `json:"bandwidthQuota" yaml:"bandwidthQuota"` + BandwidthUsage int `json:"bandwidthUsage" yaml:"bandwidthUsage"` + DbQuota int `json:"dbQuota" yaml:"dbQuota"` + DbUsage int `json:"dbUsage" yaml:"dbUsage"` + DiskQuota int `json:"diskQuota" yaml:"diskQuota"` + DiskUsage int `json:"diskUsage" yaml:"diskUsage"` + Domains []domainUsage `json:"domains" yaml:"domains"` + DomainPointersQuota int `json:"domainPointersQuota" yaml:"domainPointersQuota"` + DomainPointersUsage int `json:"domainPointersUsage" yaml:"domainPointersUsage"` + DomainQuota int `json:"domainQuota" yaml:"domainQuota"` + DomainUsage int `json:"domainUsage" yaml:"domainUsage"` + EmailQuota int `json:"emailQuota" yaml:"emailQuota"` + EmailUsage int `json:"emailUsage" yaml:"emailUsage"` + EmailForwardersQuota int `json:"emailForwardersQuota" yaml:"emailForwardersQuota"` + EmailForwardersUsage int `json:"emailForwardersUsage" yaml:"emailForwardersUsage"` + EmailMailingListQuota int `json:"emailMailingListQuota" yaml:"emailMailingListQuota"` + EmailMailingListUsage int `json:"emailMailingListUsage" yaml:"emailMailingListUsage"` + FtpQuota int `json:"ftpQuota" yaml:"ftpQuota"` + FtpUsage int `json:"ftpUsed" yaml:"ftpUsed"` + InodeQuota int `json:"inodeQuota" yaml:"inodeQuota"` + InodeUsage int `json:"inodeUsage" yaml:"inodeUsage"` + SubdomainQuota int `json:"subdomainQuota" yaml:"subdomainQuota"` + SubdomainUsage int `json:"subdomainUsage" yaml:"subdomainUsage"` + } +) // GetMyUserConfig (user) returns the session user's config func (c *UserContext) GetMyUserConfig() (*UserConfig, error) { @@ -90,7 +93,7 @@ func (c *UserContext) GetMyUserConfig() (*UserConfig, error) { var err error var rawConfig rawUserConfig - if _, err := c.api.makeRequest(http.MethodGet, "API_SHOW_USER_CONFIG", c.credentials, nil, &rawConfig); err != nil { + if _, err = c.api.makeRequest(http.MethodGet, "API_SHOW_USER_CONFIG", c.credentials, nil, &rawConfig); err != nil { return nil, err } @@ -120,6 +123,14 @@ func (c *UserContext) GetMyUserUsage() (*UserUsage, error) { return &usage, nil } +func (c *UserContext) addUsernamePrefix(check string) string { + if !strings.HasPrefix(check, c.GetMyUsername()+"_") { + check = c.GetMyUsername() + "_" + check + } + + return check +} + func (c *UserContext) checkObjectExists(body url.Values) error { var response apiGenericResponse diff --git a/wordpress.go b/wordpress.go index 05ed5b3..7ee2173 100644 --- a/wordpress.go +++ b/wordpress.go @@ -74,7 +74,13 @@ func (c *UserContext) ChangeWordPressUserPassword(locationId string, userId int, func (c *UserContext) CreateWordPressInstall(install WordPressInstall, createDatabase bool) error { if createDatabase { - if err := c.CreateDatabase(install.DbName, install.DbUser, install.DbPass); err != nil { + if err := c.CreateDatabaseWithUser(&DatabaseWithUser{ + Database: Database{ + Name: install.DbName, + }, + Password: install.DbPass, + User: install.DbUser, + }); err != nil { return fmt.Errorf("failed to create database: %v", err) } } @@ -101,7 +107,7 @@ func (c *UserContext) CreateWordPressInstall(install WordPressInstall, createDat if _, err := c.api.makeRequestN(http.MethodPost, "wordpress/install", c.credentials, install, nil); err != nil { if createDatabase { - if dbErr := c.DeleteDatabases(install.DbName); dbErr != nil { + if dbErr := c.DeleteDatabase(install.DbName); dbErr != nil { err = fmt.Errorf("%v: %v", dbErr, err) } }