From 6ac9c06f9531c4d31c70f3d1be06f2f7d31769f7 Mon Sep 17 00:00:00 2001 From: fmontorsi Date: Fri, 8 Nov 2019 01:42:00 +0100 Subject: [PATCH 01/17] Add repository del_assets command --- src/nexuscli/api/repository/collection.py | 31 ++++++++ ...nexus3-cli-repository-delete-assets.groovy | 79 +++++++++++++++++++ src/nexuscli/cli/subcommand_repository.py | 38 ++++++++- 3 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index 60323a5..d5d47eb 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -5,6 +5,7 @@ SCRIPT_NAME_CREATE = 'nexus3-cli-repository-create' SCRIPT_NAME_DELETE = 'nexus3-cli-repository-delete' +SCRIPT_NAME_DELETE_ASSETS = 'nexus3-cli-repository-delete-assets' def get_repository_class(raw_repo): @@ -146,6 +147,36 @@ def delete(self, name): self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE, content) self._client.scripts.run(SCRIPT_NAME_DELETE, data=name) + def delete_assets(self, reponame, assetRegex, dryRun): + """ + Delete assets from a repository through a Groovy script + + :param reponame: name of the repository to delete assets from. + :type reponame: str + :param assetRegex: wildcard for assets to delete + :type assetRegex: str + """ + content = nexus_util.groovy_script(SCRIPT_NAME_DELETE_ASSETS) + self._client.scripts.delete(SCRIPT_NAME_DELETE_ASSETS) + self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE_ASSETS, content) + + # prepare JSON for Groovy: + jsonData = {} + jsonData['repoName']=reponame + jsonData['assetRegex']=assetRegex + jsonData['dryRun']=dryRun + groovy_returned_json = self._client.scripts.run(SCRIPT_NAME_DELETE_ASSETS, data=json.dumps(jsonData)) + + # parse the JSON we got back + if 'result' not in groovy_returned_json: + raise exception.NexusClientAPIError(groovy_returned_json) + script_result = json.loads(groovy_returned_json['result']) # this is actually a JSON: convert to Python dict + if 'assets' not in script_result: + raise exception.NexusClientAPIError(groovy_returned_json) + + assets_list = script_result['assets'] + return assets_list + def create(self, repository): """ Creates a Nexus repository with the given format and type. diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy new file mode 100644 index 0000000..1e37db8 --- /dev/null +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -0,0 +1,79 @@ +// Original from: +// https://github.com/hlavki/nexus-scripts +// Modified to include some improvements to logging, option to do a "dry run",etc + +import org.sonatype.nexus.repository.storage.Asset +import org.sonatype.nexus.repository.storage.Query +import org.sonatype.nexus.repository.storage.StorageFacet +import org.sonatype.nexus.repository.raw.internal.RawFormat + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper + +def log_prefix = "nexus3-cli GROOVY SCRIPT: " + +// https://gist.github.com/kellyrob99/2d1483828c5de0e41732327ded3ab224 +// https://gist.github.com/emexelem/bcf6b504d81ea9019ad4ab2369006e66 + +def request = new JsonSlurper().parseText(args) +assert request.repoName: 'repoName parameter is required' +assert request.assetRegex: 'name regular expression parameter is required, format: regexp' +assert request.dryRun != null: 'dryRun parameter is required' + +def repo = repository.repositoryManager.get(request.repoName) +if (repo == null) { + log.warn(log_prefix + "Repository ${request.repoName} does not exist") + return +} +//assert repo.format instanceof RawFormat: "Repository ${request.repoName} is not raw, but ${repo.format}" +log.info(log_prefix + "Valid repository: ${request.repoName}") + +StorageFacet storageFacet = repo.facet(StorageFacet) +def tx = storageFacet.txSupplier().get() + +try { + tx.begin() + + log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching regex pattern: ${request.assetRegex}") + Iterable assets = tx. + findAssets(Query.builder().where('name MATCHES ').param(request.assetRegex).build(), [repo]) + + def urls = assets.collect { "/repository/${repo.name}/${it.name()}" } + + if (request.dryRun == false) { + // add in the transaction a delete command for each asset + assets.each { asset -> + log.info(log_prefix + "Deleting asset ${asset.name()}") + tx.deleteAsset(asset); + + def assetId = asset.componentId() + if (assetId != null) { + def component = tx.findComponent(assetId); + if (component != null) { + log.info(log_prefix + "Deleting component with ID ${assetId} that belongs to asset ${asset.name()}") + tx.deleteComponent(component); + } + } + } + } + + tx.commit() + log.info(log_prefix + "Transaction committed successfully") + + def result = JsonOutput.toJson([ + assets : urls, + assetRegex : request.assetRegex, + repoName : request.repoName + ]) + return result + +} catch (all) { + log.warn(log_prefix + "Exception: ${all}") + all.printStackTrace() + log.info(log_prefix + "Rolling back changes...") + tx.rollback() + log.info(log_prefix + "Rollback done.") +} finally { + // @todo Fix me! Danger Will Robinson! + tx.close() +} \ No newline at end of file diff --git a/src/nexuscli/cli/subcommand_repository.py b/src/nexuscli/cli/subcommand_repository.py index 4e25940..0869aac 100644 --- a/src/nexuscli/cli/subcommand_repository.py +++ b/src/nexuscli/cli/subcommand_repository.py @@ -2,7 +2,6 @@ Usage: nexus3 repository --help nexus3 repository list - nexus3 repository (delete|del) [--force] nexus3 repository create hosted (bower|npm|nuget|pypi|raw|rubygems) [--blob=] [--strict-content] [--cleanup=] @@ -24,6 +23,8 @@ [--blob=] [--strict-content] [--cleanup=] [--write=] [--depth=] + nexus3 repository (delete|del) [--force] + nexus3 repository del_assets [--force] Options: -h --help This screen @@ -37,12 +38,15 @@ -f --force Do not ask for confirmation before deleting Commands: - repository create Create a repository using the format and options provided - repository list List all repositories available on the server - repository delete Delete a repository. + repository create Create a repository using the format and options provided + repository list List all repositories available on the server + repository delete Delete an entire repository (use with care!). + repository del_assets Delete assets matching the provided regex from a repository """ from docopt import docopt from texttable import Texttable +import json +import sys from nexuscli.api import repository from nexuscli.cli import errors, util @@ -129,6 +133,32 @@ def cmd_delete(nexus_client, args): return errors.CliReturnCode.SUCCESS.value +def cmd_del_assets(nexus_client, args): + """Performs ``nexus3 repository delete_assets``""" + + repoName = args.get('') + assetRegex = args.get('') + + if not args.get('--force'): + sys.stdout.write('Retrieving assets matching regex {}\n'.format(assetRegex)) + assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, True) + if len(assets_list) == 0: + sys.stdout.write('Found 0 matching assets: aborting delete\n') + return errors.CliReturnCode.SUCCESS.value + + sys.stdout.write('Found {} matching assets:\n{}\n'.format(len(assets_list), '\n'.join(assets_list))) + util.input_with_default( + 'Press ENTER to confirm deletion', 'ctrl+c to cancel') + + assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, False) + if len(assets_list) == 0: + sys.stdout.write('Found 0 matching assets: aborting delete\n') + return errors.CliReturnCode.SUCCESS.value + + sys.stdout.write('Deleted {} matching assets:\n{}\n'.format(len(assets_list), '\n'.join(assets_list))) + return errors.CliReturnCode.SUCCESS.value + + def main(argv=None): """Entrypoint for ``nexus3 repository`` subcommand.""" arguments = docopt(__doc__, argv=argv) From 0cca23d079ac6181b45d852dfd12d9e89e031f24 Mon Sep 17 00:00:00 2001 From: fmontorsi Date: Fri, 8 Nov 2019 02:01:10 +0100 Subject: [PATCH 02/17] Add variant to use wildcard --- src/nexuscli/api/repository/collection.py | 7 +++- ...nexus3-cli-repository-delete-assets.groovy | 10 +++-- src/nexuscli/cli/subcommand_repository.py | 37 ++++++++++++++----- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index d5d47eb..0314a77 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -147,7 +147,7 @@ def delete(self, name): self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE, content) self._client.scripts.run(SCRIPT_NAME_DELETE, data=name) - def delete_assets(self, reponame, assetRegex, dryRun): + def delete_assets(self, reponame, assetRegex, isWildcard, dryRun): """ Delete assets from a repository through a Groovy script @@ -155,6 +155,10 @@ def delete_assets(self, reponame, assetRegex, dryRun): :type reponame: str :param assetRegex: wildcard for assets to delete :type assetRegex: str + :param isWildcard: is the assetRegex a regex or a wildcard? + :type isWildcard: bool + :param dryRun: do a dry run or delete for real? + :type dryRun: bool """ content = nexus_util.groovy_script(SCRIPT_NAME_DELETE_ASSETS) self._client.scripts.delete(SCRIPT_NAME_DELETE_ASSETS) @@ -164,6 +168,7 @@ def delete_assets(self, reponame, assetRegex, dryRun): jsonData = {} jsonData['repoName']=reponame jsonData['assetRegex']=assetRegex + jsonData['isWildcard']=isWildcard jsonData['dryRun']=dryRun groovy_returned_json = self._client.scripts.run(SCRIPT_NAME_DELETE_ASSETS, data=json.dumps(jsonData)) diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index 1e37db8..6d9648a 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -18,6 +18,7 @@ def log_prefix = "nexus3-cli GROOVY SCRIPT: " def request = new JsonSlurper().parseText(args) assert request.repoName: 'repoName parameter is required' assert request.assetRegex: 'name regular expression parameter is required, format: regexp' +assert request.isWildcard != null: 'isWildcard parameter is required' assert request.dryRun != null: 'dryRun parameter is required' def repo = repository.repositoryManager.get(request.repoName) @@ -34,9 +35,12 @@ def tx = storageFacet.txSupplier().get() try { tx.begin() - log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching regex pattern: ${request.assetRegex}") - Iterable assets = tx. - findAssets(Query.builder().where('name MATCHES ').param(request.assetRegex).build(), [repo]) + log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching pattern: ${request.assetRegex} isWildcard: ${request.isWildcard}") + Iterable assets + if (request.isWildcard) + assets = tx.findAssets(Query.builder().where('name like ').param(request.assetRegex).build(), [repo]) + else + assets = tx.findAssets(Query.builder().where('name MATCHES ').param(request.assetRegex).build(), [repo]) def urls = assets.collect { "/repository/${repo.name}/${it.name()}" } diff --git a/src/nexuscli/cli/subcommand_repository.py b/src/nexuscli/cli/subcommand_repository.py index 0869aac..56f5be7 100644 --- a/src/nexuscli/cli/subcommand_repository.py +++ b/src/nexuscli/cli/subcommand_repository.py @@ -24,7 +24,8 @@ [--write=] [--depth=] nexus3 repository (delete|del) [--force] - nexus3 repository del_assets [--force] + nexus3 repository del_assets_regex [--force] + nexus3 repository del_assets_wildcard [--force] Options: -h --help This screen @@ -41,7 +42,8 @@ repository create Create a repository using the format and options provided repository list List all repositories available on the server repository delete Delete an entire repository (use with care!). - repository del_assets Delete assets matching the provided regex from a repository + repository del_assets_regex Delete assets matching the provided regex from a repository + repository del_assets_wildcard Delete assets matching the provided wildcard (using % as wildcard) from a repository """ from docopt import docopt from texttable import Texttable @@ -133,15 +135,12 @@ def cmd_delete(nexus_client, args): return errors.CliReturnCode.SUCCESS.value -def cmd_del_assets(nexus_client, args): +def _cmd_del_assets(nexus_client, repoName, assetRegex, isWildcard, doForce): """Performs ``nexus3 repository delete_assets``""" - repoName = args.get('') - assetRegex = args.get('') - - if not args.get('--force'): - sys.stdout.write('Retrieving assets matching regex {}\n'.format(assetRegex)) - assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, True) + if not doForce: + sys.stdout.write('Retrieving assets matching {}\n'.format(assetRegex)) + assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, isWildcard, True) if len(assets_list) == 0: sys.stdout.write('Found 0 matching assets: aborting delete\n') return errors.CliReturnCode.SUCCESS.value @@ -150,7 +149,7 @@ def cmd_del_assets(nexus_client, args): util.input_with_default( 'Press ENTER to confirm deletion', 'ctrl+c to cancel') - assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, False) + assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, isWildcard, False) if len(assets_list) == 0: sys.stdout.write('Found 0 matching assets: aborting delete\n') return errors.CliReturnCode.SUCCESS.value @@ -159,6 +158,24 @@ def cmd_del_assets(nexus_client, args): return errors.CliReturnCode.SUCCESS.value +def cmd_del_assets_regex(nexus_client, args): + """Performs ``nexus3 repository delete_assets``""" + + repoName = args.get('') + assetRegex = args.get('') + doForce = args.get('--force') + return _cmd_del_assets(nexus_client, repoName, assetRegex, False, doForce) + + +def cmd_del_assets_wildcard(nexus_client, args): + """Performs ``nexus3 repository delete_assets``""" + + repoName = args.get('') + assetWildcard = args.get('') + doForce = args.get('--force') + return _cmd_del_assets(nexus_client, repoName, assetWildcard, True, doForce) + + def main(argv=None): """Entrypoint for ``nexus3 repository`` subcommand.""" arguments = docopt(__doc__, argv=argv) From a67c7772650a60604fc8751b7519c5a59763d576 Mon Sep 17 00:00:00 2001 From: fmontorsi Date: Sat, 9 Nov 2019 00:43:29 +0100 Subject: [PATCH 03/17] Improve error handling from Groovy script; use f-strings to address review comment --- src/nexuscli/api/repository/collection.py | 7 +++++- ...nexus3-cli-repository-delete-assets.groovy | 22 +++++++++++++++---- src/nexuscli/cli/subcommand_repository.py | 19 +++++++++++----- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index 0314a77..9a37d40 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -176,10 +176,15 @@ def delete_assets(self, reponame, assetRegex, isWildcard, dryRun): if 'result' not in groovy_returned_json: raise exception.NexusClientAPIError(groovy_returned_json) script_result = json.loads(groovy_returned_json['result']) # this is actually a JSON: convert to Python dict - if 'assets' not in script_result: + if script_result == None or 'assets' not in script_result: raise exception.NexusClientAPIError(groovy_returned_json) + if 'success' in script_result and script_result['success']==False: + raise exception.NexusClientAPIError(script_result['error']) + assets_list = script_result['assets'] + if assets_list == None: + assets_list = [] return assets_list def create(self, repository): diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index 6d9648a..8cc9992 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -24,7 +24,13 @@ assert request.dryRun != null: 'dryRun parameter is required' def repo = repository.repositoryManager.get(request.repoName) if (repo == null) { log.warn(log_prefix + "Repository ${request.repoName} does not exist") - return + + def result = JsonOutput.toJson([ + success : false, + error : "Repository ${request.repoName} does not exist", + assets : null + ]) + return result } //assert repo.format instanceof RawFormat: "Repository ${request.repoName} is not raw, but ${repo.format}" log.info(log_prefix + "Valid repository: ${request.repoName}") @@ -65,9 +71,9 @@ try { log.info(log_prefix + "Transaction committed successfully") def result = JsonOutput.toJson([ - assets : urls, - assetRegex : request.assetRegex, - repoName : request.repoName + success : true, + error : "", + assets : urls ]) return result @@ -77,6 +83,14 @@ try { log.info(log_prefix + "Rolling back changes...") tx.rollback() log.info(log_prefix + "Rollback done.") + + def result = JsonOutput.toJson([ + success : false, + error : "Exception during processing", + assets : null + ]) + return result + } finally { // @todo Fix me! Danger Will Robinson! tx.close() diff --git a/src/nexuscli/cli/subcommand_repository.py b/src/nexuscli/cli/subcommand_repository.py index 56f5be7..8e2c6e4 100644 --- a/src/nexuscli/cli/subcommand_repository.py +++ b/src/nexuscli/cli/subcommand_repository.py @@ -52,7 +52,7 @@ from nexuscli.api import repository from nexuscli.cli import errors, util - +from nexuscli import exception, nexus_util def cmd_list(nexus_client, _): """Performs ``nexus3 repository list``""" @@ -138,14 +138,23 @@ def cmd_delete(nexus_client, args): def _cmd_del_assets(nexus_client, repoName, assetRegex, isWildcard, doForce): """Performs ``nexus3 repository delete_assets``""" + nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 + if not doForce: - sys.stdout.write('Retrieving assets matching {}\n'.format(assetRegex)) - assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, isWildcard, True) + sys.stdout.write(f'Retrieving assets matching {assetRegex}\n') + + assets_list = [] + try: + assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, isWildcard, True) + except exception.NexusClientAPIError as e: + sys.stderr.write(f'Error while running API: {e}\n') + return errors.CliReturnCode.API_ERROR.value + if len(assets_list) == 0: sys.stdout.write('Found 0 matching assets: aborting delete\n') return errors.CliReturnCode.SUCCESS.value - sys.stdout.write('Found {} matching assets:\n{}\n'.format(len(assets_list), '\n'.join(assets_list))) + sys.stdout.write(f'Found {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') util.input_with_default( 'Press ENTER to confirm deletion', 'ctrl+c to cancel') @@ -154,7 +163,7 @@ def _cmd_del_assets(nexus_client, repoName, assetRegex, isWildcard, doForce): sys.stdout.write('Found 0 matching assets: aborting delete\n') return errors.CliReturnCode.SUCCESS.value - sys.stdout.write('Deleted {} matching assets:\n{}\n'.format(len(assets_list), '\n'.join(assets_list))) + sys.stdout.write(f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') return errors.CliReturnCode.SUCCESS.value From 6f19be998de85d720a8b384cc52a5b5a1973efec Mon Sep 17 00:00:00 2001 From: fmontorsi Date: Sat, 23 Nov 2019 13:10:04 +0100 Subject: [PATCH 04/17] Merge new delete command with previous one --- src/nexuscli/cli/__init__.py | 12 ++-- src/nexuscli/cli/root_commands.py | 67 ++++++++++++++++++++--- src/nexuscli/cli/subcommand_repository.py | 55 +------------------ src/nexuscli/nexus_client.py | 65 +++++++++++----------- 4 files changed, 102 insertions(+), 97 deletions(-) diff --git a/src/nexuscli/cli/__init__.py b/src/nexuscli/cli/__init__.py index 8ef7e76..618f3dd 100644 --- a/src/nexuscli/cli/__init__.py +++ b/src/nexuscli/cli/__init__.py @@ -7,7 +7,7 @@ nexus3 (list|ls) nexus3 (upload|up) [--flatten] [--norecurse] nexus3 (download|dl) [--flatten] [--nocache] - nexus3 (delete|del) + nexus3 (delete|del) [--regex|--wildcard] nexus3 [...] Options: @@ -19,14 +19,18 @@ [default: False] --norecurse Don't process subdirectories on `nexus3 up` transfers [default: False] - + --regex Intepret what follows the first '/' in the + as a regular expression [default: False] + --wildcard Intepret what follows the first '/' in the + as a wildcard expression (wildcard is '%' symbol but note it + will only match artefacts prefixes or postfixes) [default: False] Commands: login Test login and save credentials to ~/.nexus-cli list List all files within a path in the repository upload Upload file(s) to designated repository download Download an artefact or a directory to local file system - delete Delete artefact(s) from repository - + delete Delete artefact(s) from a repository; optionally use regex or wildcard + expressions to match artefact names Sub-commands: cleanup_policy Cleanup Policy management. repository Repository management. diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index 1f0b9e5..4d432fd 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -136,16 +136,67 @@ def cmd_dl(*args, **kwargs): return cmd_download(*args, **kwargs) -def cmd_delete(nexus_client, options): - """Performs ``nexus3 delete``""" - repository_path = options[''] - delete_count = nexus_client.delete(repository_path) +def _cmd_del_assets(nexus_client, repoName, componentPath, isWildcard, doForce): + """Performs ``nexus3 repository delete_assets``""" + + nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 + + if not doForce: + sys.stdout.write(f'Retrieving assets matching {componentPath}\n') + + assets_list = [] + try: + assets_list = nexus_client.repositories.delete_assets(repoName, componentPath, isWildcard, True) + except exception.NexusClientAPIError as e: + sys.stderr.write(f'Error while running API: {e}\n') + return errors.CliReturnCode.API_ERROR.value + + if len(assets_list) == 0: + sys.stdout.write('Found 0 matching assets: aborting delete\n') + return errors.CliReturnCode.SUCCESS.value + + sys.stdout.write(f'Found {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') + util.input_with_default( + 'Press ENTER to confirm deletion', 'ctrl+c to cancel') + + assets_list = nexus_client.repositories.delete_assets(repoName, componentPath, isWildcard, False) + if len(assets_list) == 0: + sys.stdout.write('Found 0 matching assets: aborting delete\n') + return errors.CliReturnCode.SUCCESS.value + + sys.stdout.write(f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') + return errors.CliReturnCode.SUCCESS.value - _cmd_up_down_errors(delete_count, 'delete') - file_word = PLURAL('file', delete_count) - sys.stderr.write(f'Deleted {delete_count} {file_word}\n') - return errors.CliReturnCode.SUCCESS.value +def cmd_delete(nexus_client, options): + """Performs ``nexus3 repository delete_assets``""" + + [repoName, componentMatchStr] = nexus_client.split_component_from_repo(options['']) + + assetWildcard = options.get('--wildcard') + assetRegex = args.get('--regex') + if assetWildcard and assetRegex: + sys.stderr.write( + f'Cannot provide both --regex and --wildcard\n') + return errors.CliReturnCode.INVALID_SUBCOMMAND.value + + if not assetWildcard and not assetRegex: + assetWildcard = True + + doForce = args.get('--force') + return _cmd_del_assets(nexus_client, repoName, componentMatchStr, assetWildcard, doForce) + +# +# def cmd_delete(nexus_client, options): +# """Performs ``nexus3 delete``""" +# repository_path = options[''] +# delete_count = nexus_client.delete(repository_path) +# +# _cmd_up_down_errors(delete_count, 'delete') +# +# file_word = PLURAL('file', delete_count) +# sys.stderr.write(f'Deleted {delete_count} {file_word}\n') +# return errors.CliReturnCode.SUCCESS.value def cmd_del(*args, **kwargs): diff --git a/src/nexuscli/cli/subcommand_repository.py b/src/nexuscli/cli/subcommand_repository.py index 8e2c6e4..4ed1ec7 100644 --- a/src/nexuscli/cli/subcommand_repository.py +++ b/src/nexuscli/cli/subcommand_repository.py @@ -2,6 +2,7 @@ Usage: nexus3 repository --help nexus3 repository list + nexus3 repository (delete|del) [--force] nexus3 repository create hosted (bower|npm|nuget|pypi|raw|rubygems) [--blob=] [--strict-content] [--cleanup=] @@ -24,8 +25,6 @@ [--write=] [--depth=] nexus3 repository (delete|del) [--force] - nexus3 repository del_assets_regex [--force] - nexus3 repository del_assets_wildcard [--force] Options: -h --help This screen @@ -42,8 +41,6 @@ repository create Create a repository using the format and options provided repository list List all repositories available on the server repository delete Delete an entire repository (use with care!). - repository del_assets_regex Delete assets matching the provided regex from a repository - repository del_assets_wildcard Delete assets matching the provided wildcard (using % as wildcard) from a repository """ from docopt import docopt from texttable import Texttable @@ -135,56 +132,6 @@ def cmd_delete(nexus_client, args): return errors.CliReturnCode.SUCCESS.value -def _cmd_del_assets(nexus_client, repoName, assetRegex, isWildcard, doForce): - """Performs ``nexus3 repository delete_assets``""" - - nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 - - if not doForce: - sys.stdout.write(f'Retrieving assets matching {assetRegex}\n') - - assets_list = [] - try: - assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, isWildcard, True) - except exception.NexusClientAPIError as e: - sys.stderr.write(f'Error while running API: {e}\n') - return errors.CliReturnCode.API_ERROR.value - - if len(assets_list) == 0: - sys.stdout.write('Found 0 matching assets: aborting delete\n') - return errors.CliReturnCode.SUCCESS.value - - sys.stdout.write(f'Found {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') - util.input_with_default( - 'Press ENTER to confirm deletion', 'ctrl+c to cancel') - - assets_list = nexus_client.repositories.delete_assets(repoName, assetRegex, isWildcard, False) - if len(assets_list) == 0: - sys.stdout.write('Found 0 matching assets: aborting delete\n') - return errors.CliReturnCode.SUCCESS.value - - sys.stdout.write(f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') - return errors.CliReturnCode.SUCCESS.value - - -def cmd_del_assets_regex(nexus_client, args): - """Performs ``nexus3 repository delete_assets``""" - - repoName = args.get('') - assetRegex = args.get('') - doForce = args.get('--force') - return _cmd_del_assets(nexus_client, repoName, assetRegex, False, doForce) - - -def cmd_del_assets_wildcard(nexus_client, args): - """Performs ``nexus3 repository delete_assets``""" - - repoName = args.get('') - assetWildcard = args.get('') - doForce = args.get('--force') - return _cmd_del_assets(nexus_client, repoName, assetWildcard, True, doForce) - - def main(argv=None): """Entrypoint for ``nexus3 repository`` subcommand.""" arguments = docopt(__doc__, argv=argv) diff --git a/src/nexuscli/nexus_client.py b/src/nexuscli/nexus_client.py index 1d8bc9e..5aa61d6 100755 --- a/src/nexuscli/nexus_client.py +++ b/src/nexuscli/nexus_client.py @@ -364,6 +364,9 @@ def split_component_path(self, component_path): return repository, directory, filename + def split_component_from_repo(self, component_path): + return self._pop_repository(component_path) + def _upload_dir_or_file(self, file_or_dir, dst_repo, dst_dir, dst_file, **kwargs): """ @@ -567,34 +570,34 @@ def download(self, source, destination, flatten=False, nocache=False): continue return download_count - - def delete(self, repository_path): - """ - Delete artefacts, recursively if ``repository_path`` is a directory. - - :param repository_path: location on the repository service. - :type repository_path: str - :return: number of deleted files. Negative number for errors. - :rtype: int - """ - - delete_count = 0 - death_row = self.list_raw(repository_path) - - death_row = progress.bar([a for a in death_row], label='Deleting') - - for artefact in death_row: - id_ = artefact['id'] - artefact_path = artefact['path'] - - response = self.http_delete(f'assets/{id_}') - LOG.info('Deleted: %s (%s)', artefact_path, id_) - delete_count += 1 - if response.status_code == 404: - LOG.warning('File disappeared while deleting') - LOG.debug(response.reason) - elif response.status_code != 204: - LOG.error(response.reason) - return -1 - - return delete_count +# +# def delete(self, repository_path): +# """ +# Delete artefacts, recursively if ``repository_path`` is a directory. +# +# :param repository_path: location on the repository service. +# :type repository_path: str +# :return: number of deleted files. Negative number for errors. +# :rtype: int +# """ +# +# delete_count = 0 +# death_row = self.list_raw(repository_path) +# +# death_row = progress.bar([a for a in death_row], label='Deleting') +# +# for artefact in death_row: +# id_ = artefact['id'] +# artefact_path = artefact['path'] +# +# response = self.http_delete(f'assets/{id_}') +# LOG.info('Deleted: %s (%s)', artefact_path, id_) +# delete_count += 1 +# if response.status_code == 404: +# LOG.warning('File disappeared while deleting') +# LOG.debug(response.reason) +# elif response.status_code != 204: +# LOG.error(response.reason) +# return -1 +# +# return delete_count From 6569a9c7d0a50c6ed759e94e5eece12785436f89 Mon Sep 17 00:00:00 2001 From: fmontorsi Date: Sat, 23 Nov 2019 18:56:26 +0100 Subject: [PATCH 05/17] fix whitespaces --- ...nexus3-cli-repository-delete-assets.groovy | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index 8cc9992..b0572a4 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -44,28 +44,28 @@ try { log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching pattern: ${request.assetRegex} isWildcard: ${request.isWildcard}") Iterable assets if (request.isWildcard) - assets = tx.findAssets(Query.builder().where('name like ').param(request.assetRegex).build(), [repo]) + assets = tx.findAssets(Query.builder().where('name like ').param(request.assetRegex).build(), [repo]) else - assets = tx.findAssets(Query.builder().where('name MATCHES ').param(request.assetRegex).build(), [repo]) + assets = tx.findAssets(Query.builder().where('name MATCHES ').param(request.assetRegex).build(), [repo]) def urls = assets.collect { "/repository/${repo.name}/${it.name()}" } - if (request.dryRun == false) { - // add in the transaction a delete command for each asset - assets.each { asset -> - log.info(log_prefix + "Deleting asset ${asset.name()}") - tx.deleteAsset(asset); - - def assetId = asset.componentId() - if (assetId != null) { - def component = tx.findComponent(assetId); - if (component != null) { - log.info(log_prefix + "Deleting component with ID ${assetId} that belongs to asset ${asset.name()}") - tx.deleteComponent(component); - } - } - } - } + if (request.dryRun == false) { + // add in the transaction a delete command for each asset + assets.each { asset -> + log.info(log_prefix + "Deleting asset ${asset.name()}") + tx.deleteAsset(asset); + + def assetId = asset.componentId() + if (assetId != null) { + def component = tx.findComponent(assetId); + if (component != null) { + log.info(log_prefix + "Deleting component with ID ${assetId} that belongs to asset ${asset.name()}") + tx.deleteComponent(component); + } + } + } + } tx.commit() log.info(log_prefix + "Transaction committed successfully") @@ -83,15 +83,15 @@ try { log.info(log_prefix + "Rolling back changes...") tx.rollback() log.info(log_prefix + "Rollback done.") - + def result = JsonOutput.toJson([ success : false, error : "Exception during processing", assets : null ]) return result - + } finally { // @todo Fix me! Danger Will Robinson! tx.close() -} \ No newline at end of file +} From d8913b6d212a0c2f06ef3a7051713718238c3880 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sun, 24 Nov 2019 13:01:04 +0100 Subject: [PATCH 06/17] Fix some obvious problems --- setup.py | 2 +- src/nexuscli/api/repository/collection.py | 1 + .../groovy/nexus3-cli-repository-delete-assets.groovy | 4 ++-- src/nexuscli/cli/__init__.py | 1 + src/nexuscli/cli/root_commands.py | 11 ++++++++--- src/nexuscli/cli/subcommand_repository.py | 4 ---- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 77dbcd2..cf0e4f3 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup package_name = 'nexus3-cli' -package_version = '2.1.0' +package_version = '2.1.1' requires = [ 'clint', diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index 1878ca0..dc5797f 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -1,6 +1,7 @@ import json from nexuscli import exception +from nexuscli import nexus_util from nexuscli.api.repository import model SCRIPT_NAME_CREATE = 'nexus3-cli-repository-create' diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index b0572a4..a663c79 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -55,7 +55,7 @@ try { assets.each { asset -> log.info(log_prefix + "Deleting asset ${asset.name()}") tx.deleteAsset(asset); - + def assetId = asset.componentId() if (assetId != null) { def component = tx.findComponent(assetId); @@ -92,6 +92,6 @@ try { return result } finally { - // @todo Fix me! Danger Will Robinson! + // @todo Fix me! Danger Will Robinson! tx.close() } diff --git a/src/nexuscli/cli/__init__.py b/src/nexuscli/cli/__init__.py index 9798d3a..adb9846 100644 --- a/src/nexuscli/cli/__init__.py +++ b/src/nexuscli/cli/__init__.py @@ -97,6 +97,7 @@ def _run_subcommand(arguments, subcommand): def main(argv=None): """Entrypoint for the setuptools CLI console script""" arguments = docopt(__doc__, argv=argv, options_first=True) + print('FIXME1') if arguments.get('--version'): print(pkg_resources.get_distribution('nexus3-cli').version) diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index 562d861..b8f3664 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -4,9 +4,13 @@ import sys import types +from nexuscli import exception from nexuscli import nexus_config +from nexuscli import nexus_util from nexuscli.nexus_client import NexusClient from nexuscli.cli import errors, util +import json +import sys PLURAL = inflect.engine().plural @@ -143,7 +147,7 @@ def _cmd_del_assets(nexus_client, repoName, componentPath, isWildcard, doForce): nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 if not doForce: - sys.stdout.write(f'Retrieving assets matching {componentPath}\n') + sys.stdout.write(f'Retrieving assets matching {componentPath} from repository {repoName}\n') assets_list = [] try: @@ -172,10 +176,11 @@ def _cmd_del_assets(nexus_client, repoName, componentPath, isWildcard, doForce): def cmd_delete(nexus_client, options): """Performs ``nexus3 repository delete_assets``""" + print('FIXME') [repoName, componentMatchStr] = nexus_client.split_component_from_repo(options['']) assetWildcard = options.get('--wildcard') - assetRegex = args.get('--regex') + assetRegex = options.get('--regex') if assetWildcard and assetRegex: sys.stderr.write( f'Cannot provide both --regex and --wildcard\n') @@ -184,7 +189,7 @@ def cmd_delete(nexus_client, options): if not assetWildcard and not assetRegex: assetWildcard = True - doForce = args.get('--force') + doForce = options.get('--force') return _cmd_del_assets(nexus_client, repoName, componentMatchStr, assetWildcard, doForce) # diff --git a/src/nexuscli/cli/subcommand_repository.py b/src/nexuscli/cli/subcommand_repository.py index e318362..0704103 100644 --- a/src/nexuscli/cli/subcommand_repository.py +++ b/src/nexuscli/cli/subcommand_repository.py @@ -25,7 +25,6 @@ [--blob=] [--strict-content] [--cleanup=] [--write=] [--depth=] - nexus3 repository (delete|del) [--force] Options: -h --help This screen @@ -47,13 +46,10 @@ import json from docopt import docopt from texttable import Texttable -import json -import sys from nexuscli import exception from nexuscli.api import repository from nexuscli.cli import errors, util -from nexuscli import exception, nexus_util def cmd_list(nexus_client, _): """Performs ``nexus3 repository list``""" From 40874020c28c324d61e55c0acc11bc59b184165b Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sat, 14 Dec 2019 21:46:11 +0100 Subject: [PATCH 07/17] Restore functionalities after integrating fixes from the MASTER branch. Add repotype check (delete only from HOSTED repositories) Add enumeration to distinguish EXACT NAME, WILDCARD, REGEX asset matching techniques --- src/nexuscli/api/repository/collection.py | 19 +++++---- ...nexus3-cli-repository-delete-assets.groovy | 42 +++++++++++++------ src/nexuscli/cli/__init__.py | 6 ++- src/nexuscli/cli/root_commands.py | 35 ++++++++-------- src/nexuscli/cli/subcommand_repository.py | 1 + src/nexuscli/cli/util.py | 6 ++- src/nexuscli/nexus_client.py | 3 -- src/nexuscli/nexus_util.py | 5 +++ 8 files changed, 74 insertions(+), 43 deletions(-) diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index dcdc450..58717b8 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -227,28 +227,31 @@ def delete(self, name): self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE) self._client.scripts.run(SCRIPT_NAME_DELETE, data=name) - def delete_assets(self, reponame, assetRegex, isWildcard, dryRun): + def delete_assets(self, reponame, assetName, assetMatchType, dryRun): """ Delete assets from a repository through a Groovy script :param reponame: name of the repository to delete assets from. :type reponame: str - :param assetRegex: wildcard for assets to delete - :type assetRegex: str - :param isWildcard: is the assetRegex a regex or a wildcard? - :type isWildcard: bool + :param assetName: name of the asset to delete + :type assetName: str + :param assetMatchType: is the assetName string an exact name, a regex or a wildcard? + :type assetMatchType: AssetMatchOptions :param dryRun: do a dry run or delete for real? :type dryRun: bool """ content = nexus_util.groovy_script(SCRIPT_NAME_DELETE_ASSETS) - self._client.scripts.delete(SCRIPT_NAME_DELETE_ASSETS) + try: + self._client.scripts.delete(SCRIPT_NAME_DELETE_ASSETS) # in case an older version is present + except: + pass self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE_ASSETS, content) # prepare JSON for Groovy: jsonData = {} jsonData['repoName']=reponame - jsonData['assetRegex']=assetRegex - jsonData['isWildcard']=isWildcard + jsonData['assetName']=assetName + jsonData['assetMatchType']=assetMatchType.name jsonData['dryRun']=dryRun groovy_returned_json = self._client.scripts.run(SCRIPT_NAME_DELETE_ASSETS, data=json.dumps(jsonData)) diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index a663c79..eaef045 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -1,6 +1,9 @@ // Original from: // https://github.com/hlavki/nexus-scripts -// Modified to include some improvements to logging, option to do a "dry run",etc +// Modified to include some improvements to +// - logging +// - option to do a "dry run" +// - support for EXACT_NAME, WILDCARD or REGEX matching methods import org.sonatype.nexus.repository.storage.Asset import org.sonatype.nexus.repository.storage.Query @@ -17,8 +20,9 @@ def log_prefix = "nexus3-cli GROOVY SCRIPT: " def request = new JsonSlurper().parseText(args) assert request.repoName: 'repoName parameter is required' -assert request.assetRegex: 'name regular expression parameter is required, format: regexp' -assert request.isWildcard != null: 'isWildcard parameter is required' +assert request.assetName: 'name regular expression parameter is required, format: regexp' +assert request.assetMatchType != null: 'assetMatchType parameter is required' +assert request.assetMatchType == 'EXACT_NAME' || request.assetMatchType == 'WILDCARD' || request.assetMatchType == 'REGEX': 'assetMatchType parameter value is invalid: ${request.assetName}' assert request.dryRun != null: 'dryRun parameter is required' def repo = repository.repositoryManager.get(request.repoName) @@ -27,13 +31,23 @@ if (repo == null) { def result = JsonOutput.toJson([ success : false, - error : "Repository ${request.repoName} does not exist", + error : "Repository '${request.repoName}' does not exist.", assets : null ]) return result } -//assert repo.format instanceof RawFormat: "Repository ${request.repoName} is not raw, but ${repo.format}" -log.info(log_prefix + "Valid repository: ${request.repoName}") +else if (repo.type != 'hosted') { + log.warn(log_prefix + "Repository ${request.repoName} has type ${repo.type}; only HOSTED repositories are supported for delete operations.") + + def result = JsonOutput.toJson([ + success : false, + error : "Repository '${request.repoName}' has invalid type '${repo.type}'; expecting an HOSTED repository.", + assets : null + ]) + return result +} + +log.info(log_prefix + "Valid repository: ${request.repoName}, of type: ${repo.type} and format: ${repo.format}") StorageFacet storageFacet = repo.facet(StorageFacet) def tx = storageFacet.txSupplier().get() @@ -41,14 +55,18 @@ def tx = storageFacet.txSupplier().get() try { tx.begin() - log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching pattern: ${request.assetRegex} isWildcard: ${request.isWildcard}") + log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching pattern: ${request.assetName} assetMatchType: ${request.assetMatchType}") Iterable assets - if (request.isWildcard) - assets = tx.findAssets(Query.builder().where('name like ').param(request.assetRegex).build(), [repo]) + if (request.assetMatchType == 'EXACT_NAME') + assets = tx.findAssets(Query.builder().where('name = ').param(request.assetName).build(), [repo]) + else if (request.assetMatchType == 'WILDCARD') + assets = tx.findAssets(Query.builder().where('name like ').param(request.assetName).build(), [repo]) + else if (request.assetMatchType == 'REGEX') + assets = tx.findAssets(Query.builder().where('name MATCHES ').param(request.assetName).build(), [repo]) else - assets = tx.findAssets(Query.builder().where('name MATCHES ').param(request.assetRegex).build(), [repo]) + assert false: 'we should never get here' - def urls = assets.collect { "/repository/${repo.name}/${it.name()}" } + def urls = assets.collect { "/${repo.name}/${it.name()}" } if (request.dryRun == false) { // add in the transaction a delete command for each asset @@ -86,7 +104,7 @@ try { def result = JsonOutput.toJson([ success : false, - error : "Exception during processing", + error : "Exception during processing.", assets : null ]) return result diff --git a/src/nexuscli/cli/__init__.py b/src/nexuscli/cli/__init__.py index 3a5bbb8..f8852e0 100644 --- a/src/nexuscli/cli/__init__.py +++ b/src/nexuscli/cli/__init__.py @@ -7,7 +7,7 @@ nexus3 (list|ls) nexus3 (upload|up) [--flatten] [--norecurse] nexus3 (download|dl) [--flatten] [--nocache] - nexus3 (delete|del) [--regex|--wildcard] + nexus3 (delete|del) [--regex|--wildcard] [--force] nexus3 [...] Options: @@ -24,7 +24,9 @@ as a regular expression [default: False] --wildcard Intepret what follows the first '/' in the as a wildcard expression (wildcard is '%' symbol but note it - will only match artefacts prefixes or postfixes) [default: False] + will only match artefacts prefixes or postfixes) [default: False] + --force When deleting, do not ask for confirmation first [default: False] + Commands: login Test login and save credentials to ~/.nexus-cli list List all files within a path in the repository diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index b8f3664..63dcc12 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -9,17 +9,16 @@ from nexuscli import nexus_util from nexuscli.nexus_client import NexusClient from nexuscli.cli import errors, util +from nexuscli.nexus_util import AssetMatchOptions import json import sys - PLURAL = inflect.engine().plural YESNO_OPTIONS = { "true": True, "t": True, "yes": True, "y": True, "false": False, "f": False, "no": False, "n": False, } - def _input_yesno(prompt, default): """ Prompts for a yes/true/no/false answer. @@ -58,7 +57,7 @@ def cmd_login(_, __): config.dump() - sys.stderr.write(f'\nConfiguration saved to {config.config_file}\n') + sys.stderr.write(f'\nLogged in successfully. Configuration saved to {config.config_file}\n') def cmd_list(nexus_client, args): @@ -141,17 +140,17 @@ def cmd_dl(*args, **kwargs): return cmd_download(*args, **kwargs) -def _cmd_del_assets(nexus_client, repoName, componentPath, isWildcard, doForce): +def _cmd_del_assets(nexus_client, repoName, assetName, assetMatchOption, doForce): """Performs ``nexus3 repository delete_assets``""" nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 if not doForce: - sys.stdout.write(f'Retrieving assets matching {componentPath} from repository {repoName}\n') + sys.stdout.write(f'Retrieving assets matching {assetMatchOption.name} "{assetName}" from repository "{repoName}"\n') assets_list = [] try: - assets_list = nexus_client.repositories.delete_assets(repoName, componentPath, isWildcard, True) + assets_list = nexus_client.repositories.delete_assets(repoName, assetName, assetMatchOption, True) except exception.NexusClientAPIError as e: sys.stderr.write(f'Error while running API: {e}\n') return errors.CliReturnCode.API_ERROR.value @@ -164,7 +163,7 @@ def _cmd_del_assets(nexus_client, repoName, componentPath, isWildcard, doForce): util.input_with_default( 'Press ENTER to confirm deletion', 'ctrl+c to cancel') - assets_list = nexus_client.repositories.delete_assets(repoName, componentPath, isWildcard, False) + assets_list = nexus_client.repositories.delete_assets(repoName, assetName, assetMatchOption, False) if len(assets_list) == 0: sys.stdout.write('Found 0 matching assets: aborting delete\n') return errors.CliReturnCode.SUCCESS.value @@ -176,21 +175,23 @@ def _cmd_del_assets(nexus_client, repoName, componentPath, isWildcard, doForce): def cmd_delete(nexus_client, options): """Performs ``nexus3 repository delete_assets``""" - print('FIXME') - [repoName, componentMatchStr] = nexus_client.split_component_from_repo(options['']) + [repoName, repoDir, assetName] = nexus_client.split_component_path(options['']) - assetWildcard = options.get('--wildcard') - assetRegex = options.get('--regex') - if assetWildcard and assetRegex: + if repoDir != None: + # we don't need to keep repoDir separated from the assetName + assetName = repoDir + '/' + assetName + + assetMatch = AssetMatchOptions.EXACT_NAME + if options.get('--wildcard') and options.get('--regex'): sys.stderr.write( f'Cannot provide both --regex and --wildcard\n') return errors.CliReturnCode.INVALID_SUBCOMMAND.value + elif options.get('--wildcard'): + assetMatch = AssetMatchOptions.WILDCARD + elif options.get('--regex'): + assetMatch = AssetMatchOptions.REGEX - if not assetWildcard and not assetRegex: - assetWildcard = True - - doForce = options.get('--force') - return _cmd_del_assets(nexus_client, repoName, componentMatchStr, assetWildcard, doForce) + return _cmd_del_assets(nexus_client, repoName, assetName, assetMatch, options.get('--force')) # # def cmd_delete(nexus_client, options): diff --git a/src/nexuscli/cli/subcommand_repository.py b/src/nexuscli/cli/subcommand_repository.py index fbcf180..de8d0fb 100644 --- a/src/nexuscli/cli/subcommand_repository.py +++ b/src/nexuscli/cli/subcommand_repository.py @@ -51,6 +51,7 @@ from nexuscli.api import repository from nexuscli.cli import errors, util + def cmd_list(nexus_client, _): """Performs ``nexus3 repository list``""" repositories = nexus_client.repositories.raw_list() diff --git a/src/nexuscli/cli/util.py b/src/nexuscli/cli/util.py index 491fb45..9331db4 100644 --- a/src/nexuscli/cli/util.py +++ b/src/nexuscli/cli/util.py @@ -61,7 +61,11 @@ def input_with_default(prompt, default=None): :return: user-provided answer or None, if default not provided. :rtype: Union[str,None] """ - value = input(f'{prompt} ({default}):') + try: + value = input(f'{prompt} ({default}):') + except KeyboardInterrupt: + print('\nInterrupted') + sys.exit(1) if value: return str(value) diff --git a/src/nexuscli/nexus_client.py b/src/nexuscli/nexus_client.py index b1b09fc..b8b82a5 100755 --- a/src/nexuscli/nexus_client.py +++ b/src/nexuscli/nexus_client.py @@ -401,9 +401,6 @@ def split_component_path(self, component_path): return repository, directory, filename - def split_component_from_repo(self, component_path): - return self._pop_repository(component_path) - def _upload_dir_or_file(self, file_or_dir, dst_repo, dst_dir, dst_file, **kwargs): """ diff --git a/src/nexuscli/nexus_util.py b/src/nexuscli/nexus_util.py index 37a16b8..989c07d 100644 --- a/src/nexuscli/nexus_util.py +++ b/src/nexuscli/nexus_util.py @@ -3,7 +3,12 @@ import mmap import os import pkg_resources +from enum import Enum +class AssetMatchOptions(Enum): + EXACT_NAME = 1 + WILDCARD = 2 + REGEX = 3 def _resource_filename(resource_name): """wrapper for pkg_resources.resource_filename""" From b9870723c5dfdb99b83c4174eccc1ec6a0b37efc Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sat, 14 Dec 2019 21:48:21 +0100 Subject: [PATCH 08/17] Remove previous delete command implementation (not going through APIs and limited to exact name match) --- src/nexuscli/cli/root_commands.py | 12 ------------ src/nexuscli/nexus_client.py | 31 ------------------------------- 2 files changed, 43 deletions(-) diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index 63dcc12..d91690c 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -193,18 +193,6 @@ def cmd_delete(nexus_client, options): return _cmd_del_assets(nexus_client, repoName, assetName, assetMatch, options.get('--force')) -# -# def cmd_delete(nexus_client, options): -# """Performs ``nexus3 delete``""" -# repository_path = options[''] -# delete_count = nexus_client.delete(repository_path) -# -# _cmd_up_down_errors(delete_count, 'delete') -# -# file_word = PLURAL('file', delete_count) -# sys.stderr.write(f'Deleted {delete_count} {file_word}\n') -# return errors.CliReturnCode.SUCCESS.value - def cmd_del(*args, **kwargs): """Alias for :func:`cmd_delete`""" diff --git a/src/nexuscli/nexus_client.py b/src/nexuscli/nexus_client.py index b8b82a5..48a39f4 100755 --- a/src/nexuscli/nexus_client.py +++ b/src/nexuscli/nexus_client.py @@ -605,34 +605,3 @@ def download(self, source, destination, flatten=False, nocache=False): continue return download_count -# -# def delete(self, repository_path): -# """ -# Delete artefacts, recursively if ``repository_path`` is a directory. -# -# :param repository_path: location on the repository service. -# :type repository_path: str -# :return: number of deleted files. Negative number for errors. -# :rtype: int -# """ -# -# delete_count = 0 -# death_row = self.list_raw(repository_path) -# -# death_row = progress.bar([a for a in death_row], label='Deleting') -# -# for artefact in death_row: -# id_ = artefact['id'] -# artefact_path = artefact['path'] -# -# response = self.http_delete(f'assets/{id_}') -# LOG.info('Deleted: %s (%s)', artefact_path, id_) -# delete_count += 1 -# if response.status_code == 404: -# LOG.warning('File disappeared while deleting') -# LOG.debug(response.reason) -# elif response.status_code != 204: -# LOG.error(response.reason) -# return -1 -# -# return delete_count From d7e6ebf17135a0604c3d8bfec43fa26e383f8c59 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sat, 14 Dec 2019 21:53:48 +0100 Subject: [PATCH 09/17] Fix linter warnings --- .../script/groovy/nexus3-cli-repository-delete-assets.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index eaef045..730e9af 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -1,6 +1,6 @@ // Original from: // https://github.com/hlavki/nexus-scripts -// Modified to include some improvements to +// Modified to include some improvements to // - logging // - option to do a "dry run" // - support for EXACT_NAME, WILDCARD or REGEX matching methods @@ -63,8 +63,6 @@ try { assets = tx.findAssets(Query.builder().where('name like ').param(request.assetName).build(), [repo]) else if (request.assetMatchType == 'REGEX') assets = tx.findAssets(Query.builder().where('name MATCHES ').param(request.assetName).build(), [repo]) - else - assert false: 'we should never get here' def urls = assets.collect { "/${repo.name}/${it.name()}" } From 806cfa0ab38fb7a40934133d1f9709e4569601df Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sun, 15 Dec 2019 10:40:34 +0100 Subject: [PATCH 10/17] Fix for tests --- src/nexuscli/cli/root_commands.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index d91690c..a50a653 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -177,9 +177,13 @@ def cmd_delete(nexus_client, options): [repoName, repoDir, assetName] = nexus_client.split_component_path(options['']) - if repoDir != None: + if repoDir != None and assetName != None: # we don't need to keep repoDir separated from the assetName assetName = repoDir + '/' + assetName + elif repoDir == None and assetName == None: + sys.stderr.write( + f'Invalid provided\n') + return errors.CliReturnCode.INVALID_SUBCOMMAND.value assetMatch = AssetMatchOptions.EXACT_NAME if options.get('--wildcard') and options.get('--regex'): From acf6586d6d3591b0e09a7bbb733877d1b98ff0d7 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sun, 29 Dec 2019 17:27:48 +0100 Subject: [PATCH 11/17] Address reviewers comments --- src/nexuscli/api/repository/collection.py | 19 +++++++++++++------ src/nexuscli/cli/root_commands.py | 21 +++++++++++---------- src/nexuscli/nexus_util.py | 6 ------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index 58717b8..6e4ed24 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -3,12 +3,17 @@ from nexuscli import exception from nexuscli import nexus_util from nexuscli.api.repository import model +from enum import Enum SCRIPT_NAME_CREATE = 'nexus3-cli-repository-create' SCRIPT_NAME_DELETE = 'nexus3-cli-repository-delete' SCRIPT_NAME_DELETE_ASSETS = 'nexus3-cli-repository-delete-assets' SCRIPT_NAME_GET = 'nexus3-cli-repository-get' +class AssetMatchOptions(Enum): + EXACT_NAME = 1 + WILDCARD = 2 + REGEX = 3 def get_repository_class(raw_configuration): """ @@ -248,21 +253,23 @@ def delete_assets(self, reponame, assetName, assetMatchType, dryRun): self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE_ASSETS, content) # prepare JSON for Groovy: - jsonData = {} - jsonData['repoName']=reponame - jsonData['assetName']=assetName - jsonData['assetMatchType']=assetMatchType.name - jsonData['dryRun']=dryRun + jsonData = { + 'repoName': reponame, + 'assetName': assetName, + 'assetMatchType': assetMatchType.name, + 'dryRun': dryRun + } groovy_returned_json = self._client.scripts.run(SCRIPT_NAME_DELETE_ASSETS, data=json.dumps(jsonData)) # parse the JSON we got back if 'result' not in groovy_returned_json: raise exception.NexusClientAPIError(groovy_returned_json) + script_result = json.loads(groovy_returned_json['result']) # this is actually a JSON: convert to Python dict if script_result == None or 'assets' not in script_result: raise exception.NexusClientAPIError(groovy_returned_json) - if 'success' in script_result and script_result['success']==False: + if not script_result.get('success', False): raise exception.NexusClientAPIError(script_result['error']) assets_list = script_result['assets'] diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index a50a653..a3a4bf2 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -9,7 +9,7 @@ from nexuscli import nexus_util from nexuscli.nexus_client import NexusClient from nexuscli.cli import errors, util -from nexuscli.nexus_util import AssetMatchOptions +from nexuscli.api.repository.collection import AssetMatchOptions import json import sys @@ -146,7 +146,7 @@ def _cmd_del_assets(nexus_client, repoName, assetName, assetMatchOption, doForce nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 if not doForce: - sys.stdout.write(f'Retrieving assets matching {assetMatchOption.name} "{assetName}" from repository "{repoName}"\n') + print(f'Retrieving assets matching {assetMatchOption.name} "{assetName}" from repository "{repoName}"') assets_list = [] try: @@ -156,19 +156,21 @@ def _cmd_del_assets(nexus_client, repoName, assetName, assetMatchOption, doForce return errors.CliReturnCode.API_ERROR.value if len(assets_list) == 0: - sys.stdout.write('Found 0 matching assets: aborting delete\n') + print('Found 0 matching assets: aborting delete') return errors.CliReturnCode.SUCCESS.value - sys.stdout.write(f'Found {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') + print(f'Found {len(assets_list)} matching assets:\n{nl.join(assets_list)}') util.input_with_default( 'Press ENTER to confirm deletion', 'ctrl+c to cancel') assets_list = nexus_client.repositories.delete_assets(repoName, assetName, assetMatchOption, False) - if len(assets_list) == 0: - sys.stdout.write('Found 0 matching assets: aborting delete\n') + delete_count = len(assets_list) + if delete_count == 0: + file_word = PLURAL('file', delete_count) + sys.stderr.write(f'Deleted {delete_count} {file_word}\n') return errors.CliReturnCode.SUCCESS.value - sys.stdout.write(f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}\n') + print(f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}') return errors.CliReturnCode.SUCCESS.value @@ -180,15 +182,14 @@ def cmd_delete(nexus_client, options): if repoDir != None and assetName != None: # we don't need to keep repoDir separated from the assetName assetName = repoDir + '/' + assetName - elif repoDir == None and assetName == None: + elif repoDir == None or assetName == None: sys.stderr.write( f'Invalid provided\n') return errors.CliReturnCode.INVALID_SUBCOMMAND.value assetMatch = AssetMatchOptions.EXACT_NAME if options.get('--wildcard') and options.get('--regex'): - sys.stderr.write( - f'Cannot provide both --regex and --wildcard\n') + sys.stderr.write('Cannot provide both --regex and --wildcard\n') return errors.CliReturnCode.INVALID_SUBCOMMAND.value elif options.get('--wildcard'): assetMatch = AssetMatchOptions.WILDCARD diff --git a/src/nexuscli/nexus_util.py b/src/nexuscli/nexus_util.py index 989c07d..0b5a619 100644 --- a/src/nexuscli/nexus_util.py +++ b/src/nexuscli/nexus_util.py @@ -3,12 +3,6 @@ import mmap import os import pkg_resources -from enum import Enum - -class AssetMatchOptions(Enum): - EXACT_NAME = 1 - WILDCARD = 2 - REGEX = 3 def _resource_filename(resource_name): """wrapper for pkg_resources.resource_filename""" From 32e59be6bb773dd3e35776d0a4f420262c8d2da7 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Sun, 29 Dec 2019 17:28:08 +0100 Subject: [PATCH 12/17] Fix integration tests --- ...nexus3-cli-repository-delete-assets.groovy | 4 ++-- tests/cli/test_cli.py | 8 +++---- tests/cli/test_delete.py | 24 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index 730e9af..8a2ed20 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -36,12 +36,12 @@ if (repo == null) { ]) return result } -else if (repo.type != 'hosted') { +else if (!repo.type.toString().equals('hosted')) { log.warn(log_prefix + "Repository ${request.repoName} has type ${repo.type}; only HOSTED repositories are supported for delete operations.") def result = JsonOutput.toJson([ success : false, - error : "Repository '${request.repoName}' has invalid type '${repo.type}'; expecting an HOSTED repository.", + error : "Repository '${request.repoName}' has invalid type '${repo.type}'; expecting an 'hosted' repository.", assets : null ]) return result diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 8c22b07..b3f7f19 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -71,18 +71,18 @@ def test_download(hosted_raw_repo_empty, deep_file_tree, faker, tmpdir): def test_delete(hosted_raw_repo_empty, deep_file_tree, faker): """Ensure that `nexus3 delete` command works""" src_dir, x_file_set = deep_file_tree - dst_dir = faker.uri_path() + '/' + dst_dir = faker.uri_path() repo_name = hosted_raw_repo_empty - dest_repo_path = '{}/{}/'.format(repo_name, dst_dir) - upload_command = f'nexus3 upload {src_dir} {dest_repo_path}' + dest_repo_path = '{}/{}'.format(repo_name, dst_dir) + upload_command = f'nexus3 upload {src_dir} {dest_repo_path}/' retcode = check_call(upload_command.split()) assert retcode == 0 # FIXME: force Nexus 3 to reindex so there's no need to sleep sleep(5) - delete_command = f'nexus3 delete {dest_repo_path}' + delete_command = f'nexus3 delete --force --wildcard {dest_repo_path}/*' retcode = check_call(delete_command.split()) assert retcode == 0 diff --git a/tests/cli/test_delete.py b/tests/cli/test_delete.py index 060e419..1465c32 100644 --- a/tests/cli/test_delete.py +++ b/tests/cli/test_delete.py @@ -10,19 +10,19 @@ def test_delete(faker, nexus_mock_client, mocker): x_repository = faker.uri_path() x_count = faker.random_int(20, 100) # list with random count of artefact paths without the leading / - x_artefacts = [ - faker.file_path( - depth=faker.random_int(2, 10))[1:] for _ in range(x_count) - ] +# x_artefacts = [ +# faker.file_path( +# depth=faker.random_int(2, 10))[1:] for _ in range(x_count) +# ] +# +# # Use list instead of generator so we can inspect contents +# raw_response = [ +# a for a in pytest.helpers.nexus_raw_response(x_artefacts) +# ] +# nexus.list_raw = mocker.Mock(return_value=raw_response) - # Use list instead of generator so we can inspect contents - raw_response = [ - a for a in pytest.helpers.nexus_raw_response(x_artefacts) - ] - nexus.list_raw = mocker.Mock(return_value=raw_response) - - ResponseMock = pytest.helpers.get_ResponseMock() - nexus.http_delete = mocker.Mock(return_value=ResponseMock(204, 'All OK')) +# ResponseMock = pytest.helpers.get_ResponseMock() +# nexus.http_delete = mocker.Mock(return_value=ResponseMock(204, 'All OK')) # call actual method being tested delete_count = nexus.delete(x_repository) From 0ccbed4e1c7df1aee312d1c167eaa0822bb9c26f Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Mon, 30 Dec 2019 01:57:14 +0100 Subject: [PATCH 13/17] Fix unit tests --- src/nexuscli/api/repository/collection.py | 5 ++- tests/cli/test_delete.py | 37 ++++++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index 6e4ed24..d44a43a 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -238,12 +238,15 @@ def delete_assets(self, reponame, assetName, assetMatchType, dryRun): :param reponame: name of the repository to delete assets from. :type reponame: str - :param assetName: name of the asset to delete + :param assetName: name of the asset(s) to delete :type assetName: str :param assetMatchType: is the assetName string an exact name, a regex or a wildcard? :type assetMatchType: AssetMatchOptions :param dryRun: do a dry run or delete for real? :type dryRun: bool + + Returns: + list: assets that have been found and deleted (if dryRun==false) """ content = nexus_util.groovy_script(SCRIPT_NAME_DELETE_ASSETS) try: diff --git a/tests/cli/test_delete.py b/tests/cli/test_delete.py index 1465c32..33b0015 100644 --- a/tests/cli/test_delete.py +++ b/tests/cli/test_delete.py @@ -1,6 +1,8 @@ import pytest +from nexuscli.api.repository.collection import AssetMatchOptions +# unit test for repository.delete_assets() def test_delete(faker, nexus_mock_client, mocker): """ Given a repository_path and a response from the service, ensure that the @@ -9,24 +11,23 @@ def test_delete(faker, nexus_mock_client, mocker): nexus = nexus_mock_client x_repository = faker.uri_path() x_count = faker.random_int(20, 100) + # list with random count of artefact paths without the leading / -# x_artefacts = [ -# faker.file_path( -# depth=faker.random_int(2, 10))[1:] for _ in range(x_count) -# ] -# -# # Use list instead of generator so we can inspect contents -# raw_response = [ -# a for a in pytest.helpers.nexus_raw_response(x_artefacts) -# ] -# nexus.list_raw = mocker.Mock(return_value=raw_response) + x_artefacts = [ + faker.file_path( + depth=faker.random_int(2, 10))[1:] for _ in range(x_count) + ] + + # patch the function that should run the Groovy script: + nexus.scripts.run = mocker.Mock(return_value={ + 'name': 'nexus3-cli-repository-delete-assets', + 'result': '{"success":true,"error":"","assets":["/reponame/assetname"]}' + }) -# ResponseMock = pytest.helpers.get_ResponseMock() -# nexus.http_delete = mocker.Mock(return_value=ResponseMock(204, 'All OK')) + for artifact in x_artefacts: + # call actual method being tested + deleted = nexus.repositories.delete_assets(x_repository, artifact, AssetMatchOptions.EXACT_NAME, False) + delete_count = len(deleted) + assert delete_count == 1 - # call actual method being tested - delete_count = nexus.delete(x_repository) - - assert delete_count == x_count - nexus.list_raw.assert_called_with(x_repository) - nexus.http_delete.assert_called() + nexus.scripts.run.assert_called() From 2a70340590c2364d0f5a220d842b0b5c159afc99 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Mon, 30 Dec 2019 17:29:33 +0100 Subject: [PATCH 14/17] Fix flake8 errors/warnings; improve integration test to verify that the repo at the end of the test is indeed empty --- src/nexuscli/api/repository/collection.py | 39 ++++++---- ...nexus3-cli-repository-delete-assets.groovy | 4 +- src/nexuscli/cli/root_commands.py | 71 +++++++++++-------- src/nexuscli/nexus_util.py | 1 + tests/cli/test_cli.py | 15 ++-- tests/cli/test_delete.py | 15 ++-- 6 files changed, 87 insertions(+), 58 deletions(-) diff --git a/src/nexuscli/api/repository/collection.py b/src/nexuscli/api/repository/collection.py index d44a43a..1f0a7e9 100755 --- a/src/nexuscli/api/repository/collection.py +++ b/src/nexuscli/api/repository/collection.py @@ -10,11 +10,13 @@ SCRIPT_NAME_DELETE_ASSETS = 'nexus3-cli-repository-delete-assets' SCRIPT_NAME_GET = 'nexus3-cli-repository-get' + class AssetMatchOptions(Enum): EXACT_NAME = 1 WILDCARD = 2 REGEX = 3 + def get_repository_class(raw_configuration): """ Given a raw repository configuration, returns its corresponding class. @@ -159,6 +161,7 @@ class RepositoryCollection: must provide this at instantiation or set it before calling any methods that require connectivity to Nexus. """ + def __init__(self, client=None): self._client = client self._repositories_json = None @@ -240,21 +243,25 @@ def delete_assets(self, reponame, assetName, assetMatchType, dryRun): :type reponame: str :param assetName: name of the asset(s) to delete :type assetName: str - :param assetMatchType: is the assetName string an exact name, a regex or a wildcard? + :param assetMatchType: is the assetName string an exact name, a regex + or a wildcard? :type assetMatchType: AssetMatchOptions :param dryRun: do a dry run or delete for real? :type dryRun: bool - + Returns: list: assets that have been found and deleted (if dryRun==false) """ content = nexus_util.groovy_script(SCRIPT_NAME_DELETE_ASSETS) try: - self._client.scripts.delete(SCRIPT_NAME_DELETE_ASSETS) # in case an older version is present - except: + # in case an older version is present + self._client.scripts.delete(SCRIPT_NAME_DELETE_ASSETS) + except exception.NexusClientAPIError: + # can't delete the script -- probably it's not there at all (yet) pass - self._client.scripts.create_if_missing(SCRIPT_NAME_DELETE_ASSETS, content) - + self._client.scripts.create_if_missing( + SCRIPT_NAME_DELETE_ASSETS, content) + # prepare JSON for Groovy: jsonData = { 'repoName': reponame, @@ -262,24 +269,26 @@ def delete_assets(self, reponame, assetName, assetMatchType, dryRun): 'assetMatchType': assetMatchType.name, 'dryRun': dryRun } - groovy_returned_json = self._client.scripts.run(SCRIPT_NAME_DELETE_ASSETS, data=json.dumps(jsonData)) - + groovy_returned_json = self._client.scripts.run( + SCRIPT_NAME_DELETE_ASSETS, data=json.dumps(jsonData)) + # parse the JSON we got back if 'result' not in groovy_returned_json: raise exception.NexusClientAPIError(groovy_returned_json) - - script_result = json.loads(groovy_returned_json['result']) # this is actually a JSON: convert to Python dict - if script_result == None or 'assets' not in script_result: + + # this is actually a JSON: convert to Python dict + script_result = json.loads(groovy_returned_json['result']) + if script_result is None or 'assets' not in script_result: raise exception.NexusClientAPIError(groovy_returned_json) - + if not script_result.get('success', False): raise exception.NexusClientAPIError(script_result['error']) - + assets_list = script_result['assets'] - if assets_list == None: + if assets_list is None: assets_list = [] return assets_list - + def create(self, repository): """ Creates a Nexus repository with the given format and type. diff --git a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy index 8a2ed20..8ea01b8 100644 --- a/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy +++ b/src/nexuscli/api/script/groovy/nexus3-cli-repository-delete-assets.groovy @@ -84,7 +84,9 @@ try { } tx.commit() - log.info(log_prefix + "Transaction committed successfully") + + numAssets = urls.size() + log.info(log_prefix + "Transaction committed successfully; number of assets matched: ${numAssets}") def result = JsonOutput.toJson([ success : true, diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index a3a4bf2..3796b9f 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -6,12 +6,9 @@ from nexuscli import exception from nexuscli import nexus_config -from nexuscli import nexus_util from nexuscli.nexus_client import NexusClient from nexuscli.cli import errors, util from nexuscli.api.repository.collection import AssetMatchOptions -import json -import sys PLURAL = inflect.engine().plural YESNO_OPTIONS = { @@ -19,6 +16,7 @@ "false": False, "f": False, "no": False, "n": False, } + def _input_yesno(prompt, default): """ Prompts for a yes/true/no/false answer. @@ -57,7 +55,8 @@ def cmd_login(_, __): config.dump() - sys.stderr.write(f'\nLogged in successfully. Configuration saved to {config.config_file}\n') + sys.stderr.write(f'\nLogged in successfully. ' + f'Configuration saved to {config.config_file}\n') def cmd_list(nexus_client, args): @@ -99,9 +98,9 @@ def cmd_upload(nexus_client, args): sys.stderr.write(f'Uploading {source} to {destination}\n') upload_count = nexus_client.upload( - source, destination, - flatten=args.get('--flatten'), - recurse=(not args.get('--norecurse'))) + source, destination, + flatten=args.get('--flatten'), + recurse=(not args.get('--norecurse'))) _cmd_up_down_errors(upload_count, 'upload') @@ -123,9 +122,9 @@ def cmd_download(nexus_client, args): sys.stderr.write(f'Downloading {source} to {destination}\n') download_count = nexus_client.download( - source, destination, - flatten=args.get('--flatten'), - nocache=args.get('--nocache')) + source, destination, + flatten=args.get('--flatten'), + nocache=args.get('--nocache')) _cmd_up_down_errors(download_count, 'download') @@ -140,53 +139,62 @@ def cmd_dl(*args, **kwargs): return cmd_download(*args, **kwargs) -def _cmd_del_assets(nexus_client, repoName, assetName, assetMatchOption, doForce): +def _cmd_del_assets(nexus_client, repoName, assetName, assetMatchOption, + doForce): """Performs ``nexus3 repository delete_assets``""" - - nl = '\n' # see https://stackoverflow.com/questions/44780357/how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 - + + # see https://stackoverflow.com/questions/44780357/ + # how-to-use-newline-n-in-f-string-to-format-output-in-python-3-6 + nl = '\n' + if not doForce: - print(f'Retrieving assets matching {assetMatchOption.name} "{assetName}" from repository "{repoName}"') - + print(f'Retrieving assets matching {assetMatchOption.name} ' + f'"{assetName}" from repository "{repoName}"') + assets_list = [] try: - assets_list = nexus_client.repositories.delete_assets(repoName, assetName, assetMatchOption, True) + assets_list = nexus_client.repositories.delete_assets( + repoName, assetName, assetMatchOption, True) except exception.NexusClientAPIError as e: sys.stderr.write(f'Error while running API: {e}\n') return errors.CliReturnCode.API_ERROR.value - + if len(assets_list) == 0: print('Found 0 matching assets: aborting delete') return errors.CliReturnCode.SUCCESS.value - - print(f'Found {len(assets_list)} matching assets:\n{nl.join(assets_list)}') + + print(f'Found {len(assets_list)} matching assets:' + f'\n{nl.join(assets_list)}') util.input_with_default( 'Press ENTER to confirm deletion', 'ctrl+c to cancel') - assets_list = nexus_client.repositories.delete_assets(repoName, assetName, assetMatchOption, False) + assets_list = nexus_client.repositories.delete_assets( + repoName, assetName, assetMatchOption, False) delete_count = len(assets_list) if delete_count == 0: file_word = PLURAL('file', delete_count) sys.stderr.write(f'Deleted {delete_count} {file_word}\n') return errors.CliReturnCode.SUCCESS.value - - print(f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}') + + print( + f'Deleted {len(assets_list)} matching assets:\n{nl.join(assets_list)}') return errors.CliReturnCode.SUCCESS.value def cmd_delete(nexus_client, options): """Performs ``nexus3 repository delete_assets``""" - - [repoName, repoDir, assetName] = nexus_client.split_component_path(options['']) - - if repoDir != None and assetName != None: + + [repoName, repoDir, assetName] = nexus_client.split_component_path( + options['']) + + if repoDir is not None and assetName is not None: # we don't need to keep repoDir separated from the assetName assetName = repoDir + '/' + assetName - elif repoDir == None or assetName == None: + elif repoDir is None or assetName is None: sys.stderr.write( f'Invalid provided\n') return errors.CliReturnCode.INVALID_SUBCOMMAND.value - + assetMatch = AssetMatchOptions.EXACT_NAME if options.get('--wildcard') and options.get('--regex'): sys.stderr.write('Cannot provide both --regex and --wildcard\n') @@ -195,8 +203,9 @@ def cmd_delete(nexus_client, options): assetMatch = AssetMatchOptions.WILDCARD elif options.get('--regex'): assetMatch = AssetMatchOptions.REGEX - - return _cmd_del_assets(nexus_client, repoName, assetName, assetMatch, options.get('--force')) + + return _cmd_del_assets(nexus_client, repoName, assetName, + assetMatch, options.get('--force')) def cmd_del(*args, **kwargs): diff --git a/src/nexuscli/nexus_util.py b/src/nexuscli/nexus_util.py index 0b5a619..37a16b8 100644 --- a/src/nexuscli/nexus_util.py +++ b/src/nexuscli/nexus_util.py @@ -4,6 +4,7 @@ import os import pkg_resources + def _resource_filename(resource_name): """wrapper for pkg_resources.resource_filename""" return pkg_resources.resource_filename('nexuscli', resource_name) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index b3f7f19..7373656 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -68,7 +68,7 @@ def test_download(hosted_raw_repo_empty, deep_file_tree, faker, tmpdir): @pytest.mark.integration -def test_delete(hosted_raw_repo_empty, deep_file_tree, faker): +def test_delete(nexus_client, hosted_raw_repo_empty, deep_file_tree, faker): """Ensure that `nexus3 delete` command works""" src_dir, x_file_set = deep_file_tree dst_dir = faker.uri_path() @@ -80,9 +80,16 @@ def test_delete(hosted_raw_repo_empty, deep_file_tree, faker): retcode = check_call(upload_command.split()) assert retcode == 0 + # delete all files, one by one: + for file in x_file_set: + delete_command = \ + f'nexus3 delete --force {dest_repo_path}{src_dir}/{file}' + retcode = check_call(delete_command.split()) + assert retcode == 0 + # FIXME: force Nexus 3 to reindex so there's no need to sleep sleep(5) - delete_command = f'nexus3 delete --force --wildcard {dest_repo_path}/*' - retcode = check_call(delete_command.split()) - assert retcode == 0 + # now check that the repo is actually empty: + file_list = list(nexus_client.list(repo_name)) + assert len(file_list) == 0 diff --git a/tests/cli/test_delete.py b/tests/cli/test_delete.py index 33b0015..1b11112 100644 --- a/tests/cli/test_delete.py +++ b/tests/cli/test_delete.py @@ -1,4 +1,3 @@ -import pytest from nexuscli.api.repository.collection import AssetMatchOptions @@ -11,22 +10,24 @@ def test_delete(faker, nexus_mock_client, mocker): nexus = nexus_mock_client x_repository = faker.uri_path() x_count = faker.random_int(20, 100) - + # list with random count of artefact paths without the leading / x_artefacts = [ faker.file_path( depth=faker.random_int(2, 10))[1:] for _ in range(x_count) ] - + # patch the function that should run the Groovy script: - nexus.scripts.run = mocker.Mock(return_value={ - 'name': 'nexus3-cli-repository-delete-assets', - 'result': '{"success":true,"error":"","assets":["/reponame/assetname"]}' + nexus.scripts.run = mocker.Mock(return_value={ + 'name': 'nexus3-cli-repository-delete-assets', + 'result': '{"success":true,"error":"","assets":["/reponame/assetname"]}' }) + matchMode = AssetMatchOptions.EXACT_NAME for artifact in x_artefacts: # call actual method being tested - deleted = nexus.repositories.delete_assets(x_repository, artifact, AssetMatchOptions.EXACT_NAME, False) + deleted = nexus.repositories.delete_assets(x_repository, artifact, + matchMode, False) delete_count = len(deleted) assert delete_count == 1 From 0b1d9dae847dc6fc9c8dea53c6c3b432133205b5 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Mon, 30 Dec 2019 17:40:50 +0100 Subject: [PATCH 15/17] Fix more flake8 errors --- src/nexuscli/cli/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/nexuscli/cli/__init__.py b/src/nexuscli/cli/__init__.py index 0f002be..33c6c4a 100644 --- a/src/nexuscli/cli/__init__.py +++ b/src/nexuscli/cli/__init__.py @@ -20,20 +20,23 @@ [default: False] --norecurse Don't process subdirectories on `nexus3 up` transfers [default: False] - --regex Intepret what follows the first '/' in the - as a regular expression [default: False] - --wildcard Intepret what follows the first '/' in the - as a wildcard expression (wildcard is '%' symbol but note it - will only match artefacts prefixes or postfixes) [default: False] - --force When deleting, do not ask for confirmation first [default: False] + --regex Interpret what follows the first '/' in the + as a regular expression + [default: False] + --wildcard Interpret what follows the first '/' in the + as a wildcard expression (wildcard + is '%' symbol but note it will only match artefacts + prefixes or postfixes) [default: False] + --force When deleting, do not ask for confirmation first + [default: False] Commands: login Test login and save credentials to ~/.nexus-cli list List all files within a path in the repository upload Upload file(s) to designated repository download Download an artefact or a directory to local file system - delete Delete artefact(s) from a repository; optionally use regex or wildcard - expressions to match artefact names + delete Delete artefact(s) from a repository; optionally use regex or + wildcard expressions to match artefact names Sub-commands: cleanup_policy Cleanup Policy management. repository Repository management. From b9fe5cc2635b808986f855cd645513865fd313ae Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Mon, 30 Dec 2019 17:50:25 +0100 Subject: [PATCH 16/17] Fix CodeFactor error --- src/nexuscli/cli/root_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index 3796b9f..3efa308 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -199,7 +199,8 @@ def cmd_delete(nexus_client, options): if options.get('--wildcard') and options.get('--regex'): sys.stderr.write('Cannot provide both --regex and --wildcard\n') return errors.CliReturnCode.INVALID_SUBCOMMAND.value - elif options.get('--wildcard'): + + if options.get('--wildcard'): assetMatch = AssetMatchOptions.WILDCARD elif options.get('--regex'): assetMatch = AssetMatchOptions.REGEX From 0506853da59ce9a05505d769c286eb27bd447689 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Mon, 30 Dec 2019 18:26:44 +0100 Subject: [PATCH 17/17] more flake8 fixes --- src/nexuscli/cli/root_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nexuscli/cli/root_commands.py b/src/nexuscli/cli/root_commands.py index 3efa308..6493acb 100644 --- a/src/nexuscli/cli/root_commands.py +++ b/src/nexuscli/cli/root_commands.py @@ -199,7 +199,7 @@ def cmd_delete(nexus_client, options): if options.get('--wildcard') and options.get('--regex'): sys.stderr.write('Cannot provide both --regex and --wildcard\n') return errors.CliReturnCode.INVALID_SUBCOMMAND.value - + if options.get('--wildcard'): assetMatch = AssetMatchOptions.WILDCARD elif options.get('--regex'):