loaddata: add an option that allows to not modify existing users

This commit is contained in:
Nicola Murino 2020-01-31 19:10:45 +01:00
parent 3491717c26
commit e647f3626e
6 changed files with 170 additions and 80 deletions

View file

@ -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
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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")
}

View file

@ -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

View file

@ -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)