Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/nexuscli/api/repository/collection.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import json

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):
"""
Given a raw repository configuration, returns its corresponding class.
Expand Down Expand Up @@ -152,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
Expand Down Expand Up @@ -225,6 +235,60 @@ 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, 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 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:
# 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)

# prepare JSON for Groovy:
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)

# 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 is None:
assets_list = []
return assets_list

def create(self, repository):
"""
Creates a Nexus repository with the given format and type.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Original from:
// https://github.com/hlavki/nexus-scripts
// 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
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.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)
if (repo == null) {
log.warn(log_prefix + "Repository ${request.repoName} does not exist")

def result = JsonOutput.toJson([
success : false,
error : "Repository '${request.repoName}' does not exist.",
assets : null
])
return result
}
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.",
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()

try {
tx.begin()

log.info(log_prefix + "Gathering list of assets from repository: ${request.repoName} matching pattern: ${request.assetName} assetMatchType: ${request.assetMatchType}")
Iterable<Asset> assets
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])

def urls = assets.collect { "/${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()

numAssets = urls.size()
log.info(log_prefix + "Transaction committed successfully; number of assets matched: ${numAssets}")

def result = JsonOutput.toJson([
success : true,
error : "",
assets : urls
])
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.")

def result = JsonOutput.toJson([
success : false,
error : "Exception during processing.",
assets : null
])
return result

} finally {
// @todo Fix me! Danger Will Robinson!
tx.close()
}
15 changes: 12 additions & 3 deletions src/nexuscli/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
nexus3 (list|ls) <repository_path>
nexus3 (upload|up) <from_src> <to_repository> [--flatten] [--norecurse]
nexus3 (download|dl) <from_repository> <to_dst> [--flatten] [--nocache]
nexus3 (delete|del) <repository_path>
nexus3 (delete|del) <repository_path> [--regex|--wildcard] [--force]
nexus3 <subcommand> [<arguments>...]

Options:
Expand All @@ -20,14 +20,23 @@
[default: False]
--norecurse Don't process subdirectories on `nexus3 up` transfers
[default: False]
--regex Interpret what follows the first '/' in the
<repository_path> as a regular expression
[default: False]
--wildcard Interpret what follows the first '/' in the
<repository_path> 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 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.
Expand Down
92 changes: 76 additions & 16 deletions src/nexuscli/cli/root_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import sys
import types

from nexuscli import exception
from nexuscli import nexus_config
from nexuscli.nexus_client import NexusClient
from nexuscli.cli import errors, util

from nexuscli.api.repository.collection import AssetMatchOptions

PLURAL = inflect.engine().plural
YESNO_OPTIONS = {
Expand Down Expand Up @@ -54,7 +55,8 @@ def cmd_login(_, __):

config.dump()

sys.stderr.write(f'\nConfiguration 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):
Expand Down Expand Up @@ -96,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')

Expand All @@ -120,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')

Expand All @@ -137,18 +139,76 @@ def cmd_dl(*args, **kwargs):
return cmd_download(*args, **kwargs)


def cmd_delete(nexus_client, options):
"""Performs ``nexus3 delete``"""
repository_path = options['<repository_path>']
delete_count = nexus_client.delete(repository_path)

_cmd_up_down_errors(delete_count, 'delete')
def _cmd_del_assets(nexus_client, repoName, assetName, assetMatchOption,
doForce):
"""Performs ``nexus3 repository delete_assets``"""

# 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} '
f'"{assetName}" from repository "{repoName}"')

assets_list = []
try:
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:'
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)
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

file_word = PLURAL('file', delete_count)
sys.stderr.write(f'Deleted {delete_count} {file_word}\n')
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['<repository_path>'])

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 is None or assetName is None:
sys.stderr.write(
f'Invalid <repository_path> 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')
return errors.CliReturnCode.INVALID_SUBCOMMAND.value

if options.get('--wildcard'):
assetMatch = AssetMatchOptions.WILDCARD
elif options.get('--regex'):
assetMatch = AssetMatchOptions.REGEX

return _cmd_del_assets(nexus_client, repoName, assetName,
assetMatch, options.get('--force'))


def cmd_del(*args, **kwargs):
"""Alias for :func:`cmd_delete`"""
return cmd_delete(*args, **kwargs)
6 changes: 5 additions & 1 deletion src/nexuscli/cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading