Skip to content

Commit

Permalink
Implement Composer hosted repositories (sonatype-nexus-community#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
fjmilens3 authored Apr 5, 2018
1 parent 3ba285e commit 5e84f65
Show file tree
Hide file tree
Showing 22 changed files with 1,305 additions and 66 deletions.
35 changes: 35 additions & 0 deletions docs/COMPOSER_USER_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ in detail. Minimal configuration steps are:
- Define URL for 'Remote storage' e.g. [https://packagist.org/](https://packagist.org/)
- Select a 'Blob store' for 'Storage'

### Hosting Composer Repositories

Creating a Composer hosted repository allows you to register packages in the repository manager. The hosted
repository acts as an authoritative location for these components.

To add a hosted Composer repository, create a new repository with the recipe 'composer (hosted)' as
documented in [Repository Management](https://help.sonatype.com/display/NXRM3/Configuration#Configuration-RepositoryManagement).

Minimal configuration steps are:

- Define 'Name' - e.g. `composer-hosted`
- Select 'Blob store' for 'Storage'

### Configuring Composer

The least-invasive way of configuring the Composer client to communicate with Nexus is to update the `repositories`
Expand All @@ -65,3 +78,25 @@ that all requests are directed to your Nexus repository manager.

You can browse Composer repositories in the user interface inspecting the components and assets and their details, as
described in [Browsing Repositories and Repository Groups](https://help.sonatype.com/display/NXRM3/Browsing+Repositories+and+Repository+Groups).

### Publishing Composer Packages

If you are authoring your own packages and want to distribute them to other users in your organization, you have
to upload them to a hosted repository on the repository manager. The consumers can then download it via the
repository.

A Composer package should consist of a zipped archive of the sources containing a `composer.json` file. You should be
able to determine the vendor and project of the package from the `composer.json` file. The version can be determined
based on your own particular development process (for example, the version is not required in `composer.json` files and
may instead be something like a Git tag for a release depending on your local arrangements and preferences).

With this information known, the package can be uploaded to your hosted Composer repository (replacing the credentials,
source filename, and vendor, project, and version path segments to match your particular distributable):

`curl -v --user 'user:pass' --upload-file example.zip http://localhost:8081/repository/composer-hosted/packages/upload/vendor/project/version`

*Note that the relevant information for vendor, project, and version is obtained from the URL you use to upload the zip,
not the composer.json file contained in the archive.* This flexible upload mechanism allows you to avoid changing the
contents of your `composer.json` in order to upload to Nexus. For example, you could write a script to check out new
tags from your Git repo, construct the appropriate upload URL, then push the tagged releases from your Git repo to your
Nexus hosted repository.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.sonatype.nexus.repository.Facet;
import org.sonatype.nexus.repository.cache.CacheInfo;
import org.sonatype.nexus.repository.view.Content;
import org.sonatype.nexus.repository.view.Payload;

/**
* Content facet used for getting assets from storage and putting assets into storage for a Composer-format repository.
Expand All @@ -30,7 +31,7 @@ public interface ComposerContentFacet
@Nullable
Content get(String path) throws IOException;

Content put(String path, Content content, AssetKind assetKind) throws IOException;
Content put(String path, Payload payload, AssetKind assetKind) throws IOException;

void setCacheInfo(String path, Content content, CacheInfo cacheInfo) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.sonatype.nexus.repository.transaction.TransactionalTouchBlob;
import org.sonatype.nexus.repository.transaction.TransactionalTouchMetadata;
import org.sonatype.nexus.repository.view.Content;
import org.sonatype.nexus.repository.view.Payload;
import org.sonatype.nexus.repository.view.payloads.BlobPayload;
import org.sonatype.nexus.transaction.UnitOfWork;

Expand Down Expand Up @@ -92,18 +93,18 @@ public Content get(final String path) throws IOException {
}

@Override
public Content put(final String path, final Content content, final AssetKind assetKind) throws IOException {
public Content put(final String path, final Payload payload, final AssetKind assetKind) throws IOException {
StorageFacet storageFacet = facet(StorageFacet.class);
try (TempBlob tempBlob = storageFacet.createTempBlob(content, hashAlgorithms)) {
try (TempBlob tempBlob = storageFacet.createTempBlob(payload, hashAlgorithms)) {
switch (assetKind) {
case ZIPBALL:
return doPutContent(path, tempBlob, content, assetKind);
return doPutContent(path, tempBlob, payload, assetKind);
case PACKAGES:
return doPutMetadata(path, tempBlob, content, assetKind);
return doPutMetadata(path, tempBlob, payload, assetKind);
case LIST:
return doPutMetadata(path, tempBlob, content, assetKind);
return doPutMetadata(path, tempBlob, payload, assetKind);
case PROVIDER:
return doPutMetadata(path, tempBlob, content, assetKind);
return doPutMetadata(path, tempBlob, payload, assetKind);
default:
throw new IllegalStateException("Unexpected asset kind: " + assetKind);
}
Expand All @@ -129,21 +130,24 @@ public void setCacheInfo(final String path, final Content content, final CacheIn
@TransactionalStoreBlob
protected Content doPutMetadata(final String path,
final TempBlob tempBlob,
final Content content,
final Payload payload,
final AssetKind assetKind)
throws IOException
{
StorageTx tx = UnitOfWork.currentTx();

Asset asset = getOrCreateAsset(path, assetKind);

Content.applyToAsset(asset, Content.maintainLastModified(asset, content.getAttributes()));
if (payload instanceof Content) {
Content.applyToAsset(asset, Content.maintainLastModified(asset, ((Content) payload).getAttributes()));
}

AssetBlob assetBlob = tx.setBlob(
asset,
path,
tempBlob,
null,
content.getContentType(),
payload.getContentType(),
false
);

Expand Down Expand Up @@ -172,7 +176,7 @@ public Asset getOrCreateAsset(final String path, final AssetKind assetKind) {
@TransactionalStoreBlob
protected Content doPutContent(final String path,
final TempBlob tempBlob,
final Content content,
final Payload payload,
final AssetKind assetKind)
throws IOException
{
Expand All @@ -185,13 +189,16 @@ protected Content doPutContent(final String path,

Asset asset = getOrCreateAsset(path, assetKind, group, name, version);

Content.applyToAsset(asset, Content.maintainLastModified(asset, content.getAttributes()));
if (payload instanceof Content) {
Content.applyToAsset(asset, Content.maintainLastModified(asset, ((Content) payload).getAttributes()));
}

AssetBlob assetBlob = tx.setBlob(
asset,
path,
tempBlob,
null,
content.getContentType(),
payload.getContentType(),
false
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2018-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.composer.internal;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Singleton;

import org.sonatype.nexus.repository.Repository;
import org.sonatype.nexus.repository.http.HttpResponses;
import org.sonatype.nexus.repository.view.Content;
import org.sonatype.nexus.repository.view.Context;
import org.sonatype.nexus.repository.view.Handler;
import org.sonatype.nexus.repository.view.Response;

import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.buildZipballPath;
import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.getProjectToken;
import static org.sonatype.nexus.repository.composer.internal.ComposerPathUtils.getVendorToken;

/**
* Download handler for Composer hosted repositories.
*/
@Named
@Singleton
public class ComposerHostedDownloadHandler
implements Handler
{
@Nonnull
@Override
public Response handle(@Nonnull final Context context) throws Exception {
Repository repository = context.getRepository();
ComposerHostedFacet hostedFacet = repository.facet(ComposerHostedFacet.class);
AssetKind assetKind = context.getAttributes().require(AssetKind.class);
switch (assetKind) {
case PACKAGES:
return HttpResponses.ok(hostedFacet.getPackagesJson());
case LIST:
throw new IllegalStateException("Unsupported assetKind: " + assetKind);
case PROVIDER:
return HttpResponses.ok(hostedFacet.getProviderJson(getVendorToken(context), getProjectToken(context)));
case ZIPBALL:
return responseFor(hostedFacet.getZipball(buildZipballPath(context)));
default:
throw new IllegalStateException("Unexpected assetKind: " + assetKind);
}
}

private Response responseFor(@Nullable final Content content) {
if (content == null) {
return HttpResponses.notFound();
}
return HttpResponses.ok(content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2018-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.composer.internal;

import java.io.IOException;

import javax.annotation.Nullable;

import org.sonatype.nexus.repository.Facet;
import org.sonatype.nexus.repository.view.Content;
import org.sonatype.nexus.repository.view.Payload;

/**
* Interface defining the features supported by Composer repository hosted facets.
*/
@Facet.Exposed
public interface ComposerHostedFacet
extends Facet
{
void upload(String path, Payload payload) throws IOException;

Content getPackagesJson() throws IOException;

Content getProviderJson(String vendor, String project) throws IOException;

@Nullable
Content getZipball(String path) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2018-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.repository.composer.internal;

import java.io.IOException;

import javax.inject.Inject;
import javax.inject.Named;

import org.sonatype.nexus.repository.FacetSupport;
import org.sonatype.nexus.repository.storage.Query;
import org.sonatype.nexus.repository.storage.StorageTx;
import org.sonatype.nexus.repository.transaction.TransactionalTouchMetadata;
import org.sonatype.nexus.repository.view.Content;
import org.sonatype.nexus.repository.view.Payload;
import org.sonatype.nexus.transaction.UnitOfWork;

import com.google.common.annotations.VisibleForTesting;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Collections.singletonList;
import static org.sonatype.nexus.repository.storage.ComponentEntityAdapter.P_GROUP;
import static org.sonatype.nexus.repository.storage.MetadataNodeEntityAdapter.P_NAME;

/**
* Default implementation of a Composer hosted facet.
*/
@Named
public class ComposerHostedFacetImpl
extends FacetSupport
implements ComposerHostedFacet
{
private final ComposerJsonProcessor composerJsonProcessor;

@Inject
public ComposerHostedFacetImpl(final ComposerJsonProcessor composerJsonProcessor) {
this.composerJsonProcessor = checkNotNull(composerJsonProcessor);
}

@Override
public void upload(final String path, final Payload payload) throws IOException {
content().put(path, payload, AssetKind.ZIPBALL);
}

@Override
public Content getZipball(final String path) throws IOException {
return content().get(path);
}

@Override
@TransactionalTouchMetadata
public Content getPackagesJson() throws IOException {
StorageTx tx = UnitOfWork.currentTx();
return composerJsonProcessor
.generatePackagesFromComponents(getRepository(), tx.browseComponents(tx.findBucket(getRepository())));
}

@Override
@TransactionalTouchMetadata
public Content getProviderJson(final String vendor, final String project) throws IOException {
StorageTx tx = UnitOfWork.currentTx();
return composerJsonProcessor.buildProviderJson(getRepository(),
tx.findComponents(buildQuery(vendor, project), singletonList(getRepository())));
}

@VisibleForTesting
protected Query buildQuery(final String vendor, final String project) {
return Query.builder().where(P_GROUP).eq(vendor).and(P_NAME).eq(project).build();
}

private ComposerContentFacet content() {
return getRepository().facet(ComposerContentFacet.class);
}
}
Loading

0 comments on commit 5e84f65

Please sign in to comment.