From e647f3626e4c12026841d6fa91aa389bd116d826 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 31 Jan 2020 19:10:45 +0100 Subject: [PATCH] loaddata: add an option that allows to not modify existing users --- httpd/api_maintenance.go | 44 ++++++++++----- httpd/api_utils.go | 5 +- httpd/httpd_test.go | 71 +++++++++++++++++++++---- httpd/internal_test.go | 4 +- httpd/schema/openapi.yaml | 17 ++++-- scripts/sftpgo_api_cli.py | 109 ++++++++++++++++++++------------------ 6 files changed, 170 insertions(+), 80 deletions(-) diff --git a/httpd/api_maintenance.go b/httpd/api_maintenance.go index 3dcc304e..3098813b 100644 --- a/httpd/api_maintenance.go +++ b/httpd/api_maintenance.go @@ -59,19 +59,10 @@ func dumpData(w http.ResponseWriter, r *http.Request) { } func loadData(w http.ResponseWriter, r *http.Request) { - var inputFile string - var err error - scanQuota := 0 - if _, ok := r.URL.Query()["input_file"]; ok { - inputFile = strings.TrimSpace(r.URL.Query().Get("input_file")) - } - if _, ok := r.URL.Query()["scan_quota"]; ok { - scanQuota, err = strconv.Atoi(r.URL.Query().Get("scan_quota")) - if err != nil { - err = errors.New("Invalid scan_quota") - sendAPIResponse(w, r, err, "", http.StatusBadRequest) - return - } + inputFile, scanQuota, mode, err := getLoaddataOptions(r) + if err != nil { + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return } if !filepath.IsAbs(inputFile) { sendAPIResponse(w, r, fmt.Errorf("Invalid input_file %#v: it must be an absolute path", inputFile), "", http.StatusBadRequest) @@ -103,6 +94,10 @@ func loadData(w http.ResponseWriter, r *http.Request) { for _, user := range dump.Users { u, err := dataprovider.UserExists(dataProvider, user.Username) if err == nil { + if mode == 1 { + logger.Debug(logSender, "", "loaddata mode = 1 existing user: %#v not updated", u.Username) + continue + } user.ID = u.ID user.LastLogin = u.LastLogin user.UsedQuotaSize = u.UsedQuotaSize @@ -136,3 +131,26 @@ func loadData(w http.ResponseWriter, r *http.Request) { func needQuotaScan(scanQuota int, user *dataprovider.User) bool { return scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) } + +func getLoaddataOptions(r *http.Request) (string, int, int, error) { + var inputFile string + var err error + scanQuota := 0 + restoreMode := 0 + if _, ok := r.URL.Query()["input_file"]; ok { + inputFile = strings.TrimSpace(r.URL.Query().Get("input_file")) + } + if _, ok := r.URL.Query()["scan_quota"]; ok { + scanQuota, err = strconv.Atoi(r.URL.Query().Get("scan_quota")) + if err != nil { + err = fmt.Errorf("invalid scan_quota: %v", err) + } + } + if _, ok := r.URL.Query()["mode"]; ok { + restoreMode, err = strconv.Atoi(r.URL.Query().Get("mode")) + if err != nil { + err = fmt.Errorf("invalid mode: %v", err) + } + } + return inputFile, scanQuota, restoreMode, err +} diff --git a/httpd/api_utils.go b/httpd/api_utils.go index 9a9fdc1d..5696df48 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -338,7 +338,7 @@ func Dumpdata(outputFile string, expectedStatusCode int) (map[string]interface{} // Loaddata restores a backup. // New users are added, existing users are updated. Users will be restored one by one and the restore is stopped if a // user cannot be added/updated, so it could happen a partial restore -func Loaddata(inputFile, scanQuota string, expectedStatusCode int) (map[string]interface{}, []byte, error) { +func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[string]interface{}, []byte, error) { var response map[string]interface{} var body []byte url, err := url.Parse(buildURLRelativeToBase(loadDataPath)) @@ -350,6 +350,9 @@ func Loaddata(inputFile, scanQuota string, expectedStatusCode int) (map[string]i if len(scanQuota) > 0 { q.Add("scan_quota", scanQuota) } + if len(mode) > 0 { + q.Add("mode", mode) + } url.RawQuery = q.Encode() resp, err := getHTTPClient().Get(url.String()) if err != nil { diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 4e387d98..8254f48c 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -727,7 +727,7 @@ func TestProviderErrors(t *testing.T) { backupContent, _ := json.Marshal(backupData) backupFilePath := filepath.Join(backupsPath, "backup.json") ioutil.WriteFile(backupFilePath, backupContent, 0666) - _, _, err = httpd.Loaddata(backupFilePath, "", http.StatusInternalServerError) + _, _, err = httpd.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) if err != nil { t.Errorf("get provider status with provider closed must fail: %v", err) } @@ -800,33 +800,37 @@ func TestLoaddata(t *testing.T) { backupContent, _ := json.Marshal(backupData) backupFilePath := filepath.Join(backupsPath, "backup.json") ioutil.WriteFile(backupFilePath, backupContent, 0666) - _, _, err := httpd.Loaddata(backupFilePath, "a", http.StatusBadRequest) + _, _, err := httpd.Loaddata(backupFilePath, "a", "", http.StatusBadRequest) if err != nil { t.Errorf("unexpected error: %v", err) } - _, _, err = httpd.Loaddata("backup.json", "1", http.StatusBadRequest) + _, _, err = httpd.Loaddata(backupFilePath, "", "a", http.StatusBadRequest) if err != nil { t.Errorf("unexpected error: %v", err) } - _, _, err = httpd.Loaddata(backupFilePath+"a", "1", http.StatusBadRequest) + _, _, err = httpd.Loaddata("backup.json", "1", "", http.StatusBadRequest) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + _, _, err = httpd.Loaddata(backupFilePath+"a", "1", "", http.StatusBadRequest) if err != nil { t.Errorf("unexpected error: %v", err) } if runtime.GOOS != "windows" { os.Chmod(backupFilePath, 0111) - _, _, err = httpd.Loaddata(backupFilePath, "1", http.StatusInternalServerError) + _, _, err = httpd.Loaddata(backupFilePath, "1", "", http.StatusInternalServerError) if err != nil { t.Errorf("unexpected error: %v", err) } os.Chmod(backupFilePath, 0644) } // add user from backup - _, _, err = httpd.Loaddata(backupFilePath, "1", http.StatusOK) + _, _, err = httpd.Loaddata(backupFilePath, "1", "", http.StatusOK) if err != nil { t.Errorf("unexpected error: %v", err) } // update user from backup - _, _, err = httpd.Loaddata(backupFilePath, "2", http.StatusOK) + _, _, err = httpd.Loaddata(backupFilePath, "2", "", http.StatusOK) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -844,19 +848,68 @@ func TestLoaddata(t *testing.T) { } os.Remove(backupFilePath) createTestFile(backupFilePath, 10485761) - _, _, err = httpd.Loaddata(backupFilePath, "1", http.StatusBadRequest) + _, _, err = httpd.Loaddata(backupFilePath, "1", "0", http.StatusBadRequest) if err != nil { t.Errorf("unexpected error: %v", err) } os.Remove(backupFilePath) createTestFile(backupFilePath, 65535) - _, _, err = httpd.Loaddata(backupFilePath, "1", http.StatusBadRequest) + _, _, err = httpd.Loaddata(backupFilePath, "1", "0", http.StatusBadRequest) if err != nil { t.Errorf("unexpected error: %v", err) } os.Remove(backupFilePath) } +func TestLoaddataMode(t *testing.T) { + user := getTestUser() + user.ID = 1 + user.Username = "test_user_restore" + backupData := httpd.BackupData{} + backupData.Users = append(backupData.Users, user) + backupContent, _ := json.Marshal(backupData) + backupFilePath := filepath.Join(backupsPath, "backup.json") + ioutil.WriteFile(backupFilePath, backupContent, 0666) + _, _, err := httpd.Loaddata(backupFilePath, "0", "0", http.StatusOK) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + users, _, err := httpd.GetUsers(1, 0, user.Username, http.StatusOK) + if err != nil { + t.Errorf("unable to get users: %v", err) + } + if len(users) != 1 { + t.Error("Unable to get restored user") + } + user = users[0] + oldUploadBandwidth := user.UploadBandwidth + user.UploadBandwidth = oldUploadBandwidth + 128 + user, _, err = httpd.UpdateUser(user, http.StatusOK) + if err != nil { + t.Errorf("unable to update user: %v", err) + } + _, _, err = httpd.Loaddata(backupFilePath, "0", "1", http.StatusOK) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + users, _, err = httpd.GetUsers(1, 0, user.Username, http.StatusOK) + if err != nil { + t.Errorf("unable to get users: %v", err) + } + if len(users) != 1 { + t.Error("Unable to get restored user") + } + user = users[0] + if user.UploadBandwidth == oldUploadBandwidth { + t.Error("user must not be modified") + } + _, err = httpd.RemoveUser(user, http.StatusOK) + if err != nil { + t.Errorf("unable to remove user: %v", err) + } + os.Remove(backupFilePath) +} + // test using mock http server func TestBasicUserHandlingMock(t *testing.T) { diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 062ef608..a0457054 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -340,7 +340,7 @@ func TestApiCallsWithBadURL(t *testing.T) { if err == nil { t.Error("request with invalid URL must fail") } - _, _, err = Loaddata("/tmp/backup.json", "", http.StatusBadRequest) + _, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusBadRequest) if err == nil { t.Error("request with invalid URL must fail") } @@ -399,7 +399,7 @@ func TestApiCallToNotListeningServer(t *testing.T) { if err == nil { t.Errorf("request to an inactive URL must fail") } - _, _, err = Loaddata("/tmp/backup.json", "", http.StatusOK) + _, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusOK) if err == nil { t.Errorf("request to an inactive URL must fail") } diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 3d267ad0..eb1cd4e4 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.0.1 info: title: SFTPGo description: 'SFTPGo REST API' - version: 1.6.0 + version: 1.6.1 servers: - url: /api/v1 @@ -589,7 +589,7 @@ paths: tags: - maintenance summary: Restore SFTPGo data from a JSON backup - description: New users are added, existing users are updated. Users will be restored one by one and the restore is stopped if a user cannot be added/updated, so it could happen a partial restore + description: Users will be restored one by one and the restore is stopped if a user cannot be added or updated, so it could happen a partial restore operationId: loaddata parameters: - in: query @@ -608,10 +608,21 @@ paths: - 2 description: > Quota scan: - * `0` no quota scan is done, the imported user will have used_quota_size and used_quota_file = 0 + * `0` no quota scan is done, the imported user will have used_quota_size and used_quota_file = 0. This is the default * `1` scan quota * `2` scan quota if the user has quota restrictions required: false + - in: query + name: mode + schema: + type: integer + enum: + - 0 + - 1 + description: > + Mode: + * `0` New users are added, existing users are updated. This is the default + * `1` New users are added, existing users are not modified responses: 200: description: successful operation diff --git a/scripts/sftpgo_api_cli.py b/scripts/sftpgo_api_cli.py index 1820fa2b..69ffc7cc 100755 --- a/scripts/sftpgo_api_cli.py +++ b/scripts/sftpgo_api_cli.py @@ -50,50 +50,50 @@ class SFTPGoApiRequests: def formatAsJSON(self, text): if not text: - return "" + return '' json_string = json.dumps(json.loads(text), sort_keys=True, indent=2) if not self.no_color and pygments: return pygments.highlight(json_string, JsonLexer(), TerminalFormatter()) return json_string def printResponse(self, r): - if "content-type" in r.headers and "application/json" in r.headers["content-type"]: + if 'content-type' in r.headers and 'application/json' in r.headers['content-type']: if self.debug: if pygments is None: print('') print('Response color highlight is not available: you need pygments 1.5 or above.') print('') - print("Executed request: {} {} - request body: {}".format( + print('Executed request: {} {} - request body: {}'.format( r.request.method, r.url, self.formatAsJSON(r.request.body))) print('') - print("Got response, status code: {} body:".format(r.status_code)) + print('Got response, status code: {} body:'.format(r.status_code)) print(self.formatAsJSON(r.text)) else: print(r.text) - def buildUserObject(self, user_id=0, username="", password="", public_keys=[], home_dir="", uid=0, gid=0, + def buildUserObject(self, user_id=0, username='', password='', public_keys=[], home_dir='', uid=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions={}, upload_bandwidth=0, download_bandwidth=0, status=1, expiration_date=0, allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='', s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', s3_key_prefix='', gcs_bucket='', gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file=''): - user = {"id":user_id, "username":username, "uid":uid, "gid":gid, - "max_sessions":max_sessions, "quota_size":quota_size, "quota_files":quota_files, - "upload_bandwidth":upload_bandwidth, "download_bandwidth":download_bandwidth, - "status":status, "expiration_date":expiration_date} + user = {'id':user_id, 'username':username, 'uid':uid, 'gid':gid, + 'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files, + 'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth, + 'status':status, 'expiration_date':expiration_date} if password is not None: - user.update({"password":password}) + user.update({'password':password}) if public_keys: if len(public_keys) == 1 and not public_keys[0]: - user.update({"public_keys":[]}) + user.update({'public_keys':[]}) else: - user.update({"public_keys":public_keys}) + user.update({'public_keys':public_keys}) if home_dir: - user.update({"home_dir":home_dir}) + user.update({'home_dir':home_dir}) if permissions: - user.update({"permissions":permissions}) + user.update({'permissions':permissions}) if allowed_ip or denied_ip: - user.update({"filters":self.buildFilters(allowed_ip, denied_ip)}) - user.update({"filesystem":self.buildFsConfig(fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret, + user.update({'filters':self.buildFilters(allowed_ip, denied_ip)}) + user.update({'filesystem':self.buildFsConfig(fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, gcs_key_prefix, gcs_storage_class, gcs_credentials_file)}) return user @@ -101,16 +101,16 @@ class SFTPGoApiRequests: def buildPermissions(self, root_perms, subdirs_perms): permissions = {} if root_perms: - permissions.update({"/":root_perms}) + permissions.update({'/':root_perms}) for p in subdirs_perms: - if ":" in p: + if ':' in p: directory = None values = [] - for value in p.split(":"): + for value in p.split(':'): if directory is None: directory = value else: - values = [v.strip() for v in value.split(",") if v.strip()] + values = [v.strip() for v in value.split(',') if v.strip()] if directory and values: permissions.update({directory:values}) return permissions @@ -145,16 +145,16 @@ class SFTPGoApiRequests: fs_config.update({'provider':2, 'gcsconfig':gcsconfig}) return fs_config - def getUsers(self, limit=100, offset=0, order="ASC", username=""): - r = requests.get(self.userPath, params={"limit":limit, "offset":offset, "order":order, - "username":username}, auth=self.auth, verify=self.verify) + def getUsers(self, limit=100, offset=0, order='ASC', username=''): + r = requests.get(self.userPath, params={'limit':limit, 'offset':offset, 'order':order, + 'username':username}, auth=self.auth, verify=self.verify) self.printResponse(r) def getUserByID(self, user_id): - r = requests.get(urlparse.urljoin(self.userPath, "user/" + str(user_id)), auth=self.auth, verify=self.verify) + r = requests.get(urlparse.urljoin(self.userPath, 'user/' + str(user_id)), auth=self.auth, verify=self.verify) self.printResponse(r) - def addUser(self, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0, quota_size=0, + def addUser(self, username='', password='', public_keys='', home_dir='', uid=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, perms=[], upload_bandwidth=0, download_bandwidth=0, status=1, expiration_date=0, subdirs_permissions=[], allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='', s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', s3_key_prefix='', gcs_bucket='', @@ -167,7 +167,7 @@ class SFTPGoApiRequests: r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify) self.printResponse(r) - def updateUser(self, user_id, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0, + def updateUser(self, user_id, username='', password='', public_keys='', home_dir='', uid=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, perms=[], upload_bandwidth=0, download_bandwidth=0, status=1, expiration_date=0, subdirs_permissions=[], allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='', s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', @@ -177,11 +177,11 @@ class SFTPGoApiRequests: status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, gcs_key_prefix, gcs_storage_class, gcs_credentials_file) - r = requests.put(urlparse.urljoin(self.userPath, "user/" + str(user_id)), json=u, auth=self.auth, verify=self.verify) + r = requests.put(urlparse.urljoin(self.userPath, 'user/' + str(user_id)), json=u, auth=self.auth, verify=self.verify) self.printResponse(r) def deleteUser(self, user_id): - r = requests.delete(urlparse.urljoin(self.userPath, "user/" + str(user_id)), auth=self.auth, verify=self.verify) + r = requests.delete(urlparse.urljoin(self.userPath, 'user/' + str(user_id)), auth=self.auth, verify=self.verify) self.printResponse(r) def getConnections(self): @@ -189,7 +189,7 @@ class SFTPGoApiRequests: self.printResponse(r) def closeConnection(self, connectionID): - r = requests.delete(urlparse.urljoin(self.activeConnectionsPath, "connection/" + str(connectionID)), auth=self.auth) + r = requests.delete(urlparse.urljoin(self.activeConnectionsPath, 'connection/' + str(connectionID)), auth=self.auth) self.printResponse(r) def getQuotaScans(self): @@ -210,12 +210,13 @@ class SFTPGoApiRequests: self.printResponse(r) def dumpData(self, output_file): - r = requests.get(self.dumpDataPath, params={"output_file":output_file}, auth=self.auth, + r = requests.get(self.dumpDataPath, params={'output_file':output_file}, auth=self.auth, verify=self.verify) self.printResponse(r) - def loadData(self, input_file, scan_quota): - r = requests.get(self.loadDataPath, params={"input_file":input_file, "scan_quota":scan_quota}, + def loadData(self, input_file, scan_quota, mode): + r = requests.get(self.loadDataPath, params={'input_file':input_file, 'scan_quota':scan_quota, + 'mode':mode}, auth=self.auth, verify=self.verify) self.printResponse(r) @@ -237,7 +238,7 @@ class ConvertUsers: self.SFTPGoRestAPI = api def addUser(self, user): - user["id"] = len(self.SFTPGoUsers) + 1 + user['id'] = len(self.SFTPGoUsers) + 1 print('') print('New user imported: {}'.format(user)) print('') @@ -245,7 +246,7 @@ class ConvertUsers: def saveUsers(self): if self.SFTPGoUsers: - data = {"users":self.SFTPGoUsers} + data = {'users':self.SFTPGoUsers} jsonData = json.dumps(data) with open(self.output_file, 'w') as f: f.write(jsonData) @@ -259,9 +260,9 @@ class ConvertUsers: sys.exit(1) def convert(self): - if self.users_format == "unix-passwd": + if self.users_format == 'unix-passwd': self.convertFromUnixPasswd() - elif self.users_format == "pure-ftpd": + elif self.users_format == 'pure-ftpd': self.convertFromPureFTPD() else: self.convertFromProFTPD() @@ -333,15 +334,15 @@ class ConvertUsers: result = [] if not fields: return result - for v in fields.split(","): + for v in fields.split(','): ip_mask = v.strip() if not ip_mask: continue - if ip_mask.count(".") < 3 and ip_mask.count(":") < 3: - print("cannot import pure-ftpd IP: {}".format(ip_mask)) + if ip_mask.count('.') < 3 and ip_mask.count(':') < 3: + print('cannot import pure-ftpd IP: {}'.format(ip_mask)) continue - if "/" not in ip_mask: - ip_mask += "/32" + if '/' not in ip_mask: + ip_mask += '/32' result.append(ip_mask) return result @@ -389,9 +390,9 @@ def validDate(s): if not s: return datetime.fromtimestamp(0) try: - return datetime.strptime(s, "%Y-%m-%d") + return datetime.strptime(s, '%Y-%m-%d') except ValueError: - msg = "Not a valid date: '{0}'.".format(s) + msg = 'Not a valid date: "{0}".'.format(s) raise argparse.ArgumentTypeError(msg) @@ -411,7 +412,7 @@ def addCommonUserArguments(parser): help='Maximum concurrent sessions. 0 means unlimited. Default: %(default)s') parser.add_argument('-S', '--quota-size', type=int, default=0, help='Maximum size allowed as bytes. 0 means unlimited. Default: %(default)s') - parser.add_argument('-F', '--quota-files', type=int, default=0, help="default: %(default)s") + parser.add_argument('-F', '--quota-files', type=int, default=0, help='default: %(default)s') parser.add_argument('-G', '--permissions', type=str, nargs='+', default=[], choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs', 'create_symlinks', 'chmod', 'chown', 'chtimes'], help='Permissions for the root directory ' @@ -424,7 +425,7 @@ def addCommonUserArguments(parser): help='Maximum download bandwidth as KB/s, 0 means unlimited. Default: %(default)s') parser.add_argument('--status', type=int, choices=[0, 1], default=1, help='User\'s status. 1 enabled, 0 disabled. Default: %(default)s') - parser.add_argument('-E', '--expiration-date', type=validDate, default="", + parser.add_argument('-E', '--expiration-date', type=validDate, default='', help='Expiration date as YYYY-MM-DD, empty string means no expiration. Default: %(default)s') parser.add_argument('-Y', '--allowed-ip', type=str, nargs='+', default=[], help='Allowed IP/Mask in CIDR notation. For example "192.168.2.0/24" or "2001:db8::/32". Default: %(default)s') @@ -455,7 +456,7 @@ if __name__ == '__main__': help='Base URL for SFTPGo REST API. Default: %(default)s') parser.add_argument('-a', '--auth-type', type=str, default=None, choices=['basic', 'digest'], help='HTTP authentication type. Default: %(default)s') - parser.add_argument("-u", "--auth-user", type=str, default="", + parser.add_argument('-u', '--auth-user', type=str, default='', help='User for HTTP authentication. Default: %(default)s') parser.add_argument('-p', '--auth-password', type=str, default='', help='Password for HTTP authentication. Default: %(default)s') @@ -464,7 +465,7 @@ if __name__ == '__main__': parser.add_argument('-i', '--insecure', dest='secure', action='store_false', help='Set to false to ignore verifying the SSL certificate') parser.set_defaults(secure=True) - has_colors_default = pygments is not None and platform.system() != "Windows" + has_colors_default = pygments is not None and platform.system() != 'Windows' group = parser.add_mutually_exclusive_group(required=False) group.add_argument('-t', '--no-color', dest='no_color', action='store_true', default=(not has_colors_default), help='Disable color highlight for JSON responses. You need python pygments module 1.5 or above to have highlighted output') @@ -519,15 +520,19 @@ if __name__ == '__main__': parserLoadData.add_argument('-Q', '--scan-quota', type=int, choices=[0, 1, 2], default=0, help='0 means no quota scan after a user is added/updated. 1 means always scan quota. 2 ' + 'means scan quota if the user has quota restrictions. Default: %(default)s') + parserLoadData.add_argument('-M', '--mode', type=int, choices=[0, 1], default=0, + help='0 means new users are added, existing users are updated. 1 means new users are added,' + + ' existing users are not modified. Default: %(default)s') - parserConvertUsers = subparsers.add_parser('convert-users', help='Convert users to a JSON format suitable to use with loadddata') + parserConvertUsers = subparsers.add_parser('convert-users', help='Convert users to a JSON format suitable to use ' + + 'with loadddata') supportedUsersFormats = [] help_text = '' if pwd is not None: - supportedUsersFormats.append("unix-passwd") + supportedUsersFormats.append('unix-passwd') help_text = 'To import from unix-passwd format you need the permission to read /etc/shadow that is typically granted to the root user only' - supportedUsersFormats.append("pure-ftpd") - supportedUsersFormats.append("proftpd") + supportedUsersFormats.append('pure-ftpd') + supportedUsersFormats.append('proftpd') parserConvertUsers.add_argument('input_file', type=str) parserConvertUsers.add_argument('users_format', type=str, choices=supportedUsersFormats, help=help_text) parserConvertUsers.add_argument('output_file', type=str) @@ -581,7 +586,7 @@ if __name__ == '__main__': elif args.command == 'dumpdata': api.dumpData(args.output_file) elif args.command == 'loaddata': - api.loadData(args.input_file, args.scan_quota) + api.loadData(args.input_file, args.scan_quota, args.mode) elif args.command == 'convert-users': convertUsers = ConvertUsers(args.input_file, args.users_format, args.output_file, args.min_uid, args.max_uid, args.usernames, args.force_uid, args.force_gid)