mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-07 22:20:24 +00:00
Publish repo REST API. #116
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/smira/aptly/deb"
|
||||||
|
"github.com/smira/aptly/utils"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SigningOptions struct {
|
||||||
|
Skip bool
|
||||||
|
Batch bool
|
||||||
|
GpgKey string
|
||||||
|
Keyring string
|
||||||
|
SecretKeyring string
|
||||||
|
Passphrase string
|
||||||
|
PassphraseFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSigner(options *SigningOptions) (utils.Signer, error) {
|
||||||
|
if options.Skip {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &utils.GpgSigner{}
|
||||||
|
signer.SetKey(options.GpgKey)
|
||||||
|
signer.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||||
|
signer.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||||
|
signer.SetBatch(options.Batch)
|
||||||
|
|
||||||
|
err := signer.Init()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace '_' with '/' and double '__' with single '_'
|
||||||
|
func parseEscapedPath(path string) string {
|
||||||
|
return strings.Replace(strings.Replace(path, "__", "_", -1), "_", "/", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /publish
|
||||||
|
func apiPublishList(c *gin.Context) {
|
||||||
|
c.JSON(400, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /publish/:prefix/repos | /publish/:prefix/snapshots
|
||||||
|
func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||||
|
param := parseEscapedPath(c.Params.ByName("prefix"))
|
||||||
|
storage, prefix := deb.ParsePrefix(param)
|
||||||
|
|
||||||
|
var b struct {
|
||||||
|
Sources []struct {
|
||||||
|
Component string
|
||||||
|
Name string `binding:"required"`
|
||||||
|
} `binding:"required"`
|
||||||
|
Distribution string
|
||||||
|
Label string
|
||||||
|
Origin string
|
||||||
|
ForceOverwrite bool
|
||||||
|
Signing SigningOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.Bind(&b) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := getSigner(&b.Signing)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(500, fmt.Errorf("unable to initialize GPG signer: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.Sources) == 0 {
|
||||||
|
c.Fail(400, fmt.Errorf("unable to publish: soures are empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var components []string
|
||||||
|
var sources []interface{}
|
||||||
|
|
||||||
|
if strings.HasSuffix(c.Request.URL.Path, "/snapshots") {
|
||||||
|
var snapshot *deb.Snapshot
|
||||||
|
|
||||||
|
snapshotCollection := context.CollectionFactory().SnapshotCollection()
|
||||||
|
snapshotCollection.RLock()
|
||||||
|
defer snapshotCollection.RUnlock()
|
||||||
|
|
||||||
|
for _, source := range b.Sources {
|
||||||
|
components = append(components, source.Component)
|
||||||
|
|
||||||
|
snapshot, err = snapshotCollection.ByName(source.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(404, fmt.Errorf("unable to publish: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snapshotCollection.LoadComplete(snapshot)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(500, fmt.Errorf("unable to publish: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, snapshot)
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(c.Request.URL.Path, "/repos") {
|
||||||
|
var localRepo *deb.LocalRepo
|
||||||
|
|
||||||
|
localCollection := context.CollectionFactory().LocalRepoCollection()
|
||||||
|
localCollection.RLock()
|
||||||
|
defer localCollection.RUnlock()
|
||||||
|
|
||||||
|
for _, source := range b.Sources {
|
||||||
|
components = append(components, source.Component)
|
||||||
|
|
||||||
|
localRepo, err = localCollection.ByName(source.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(404, fmt.Errorf("unable to publish: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = localCollection.LoadComplete(localRepo)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(500, fmt.Errorf("unable to publish: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, localRepo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("unknown command")
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := context.CollectionFactory().PublishedRepoCollection()
|
||||||
|
collection.Lock()
|
||||||
|
defer collection.Unlock()
|
||||||
|
|
||||||
|
published, err := deb.NewPublishedRepo(storage, prefix, b.Distribution, context.ArchitecturesList(), components, sources, context.CollectionFactory())
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(500, fmt.Errorf("unable to publish: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
published.Origin = b.Origin
|
||||||
|
published.Label = b.Label
|
||||||
|
|
||||||
|
duplicate := collection.CheckDuplicate(published)
|
||||||
|
if duplicate != nil {
|
||||||
|
context.CollectionFactory().PublishedRepoCollection().LoadComplete(duplicate, context.CollectionFactory())
|
||||||
|
c.Fail(400, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = published.Publish(context.PackagePool(), context, context.CollectionFactory(), signer, context.Progress(), b.ForceOverwrite)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(500, fmt.Errorf("unable to publish: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.Add(published)
|
||||||
|
if err != nil {
|
||||||
|
c.Fail(500, fmt.Errorf("unable to save to DB: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, published)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /publish/:prefix/:distribution
|
||||||
|
func apiPublishUpdateSwitch(c *gin.Context) {
|
||||||
|
c.JSON(400, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /publish/:prefix/:distribution
|
||||||
|
func apiPublishDrop(c *gin.Context) {
|
||||||
|
c.JSON(400, gin.H{})
|
||||||
|
}
|
||||||
@@ -39,5 +39,13 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
|||||||
root.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
root.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
root.GET("/publish", apiPublishList)
|
||||||
|
root.POST("/publish/:prefix/repos", apiPublishRepoOrSnapshot)
|
||||||
|
root.POST("/publish/:prefix/snapshots", apiPublishRepoOrSnapshot)
|
||||||
|
root.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
|
||||||
|
root.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
|
||||||
|
}
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ Testing aptly REST API
|
|||||||
|
|
||||||
from .repos import *
|
from .repos import *
|
||||||
from .files import *
|
from .files import *
|
||||||
|
from .publish import *
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from api_lib import APITest
|
||||||
|
|
||||||
|
DefaultSigningOptions = {
|
||||||
|
"Keyring": os.path.join(os.path.dirname(inspect.getsourcefile(APITest)), "files") + "/aptly.pub",
|
||||||
|
"SecretKeyring": os.path.join(os.path.dirname(inspect.getsourcefile(APITest)), "files") + "/aptly.sec",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PublishAPITestRepo(APITest):
|
||||||
|
"""
|
||||||
|
POST /publish/:prefix/repos
|
||||||
|
"""
|
||||||
|
fixtureGpg = True
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
repo_name = self.random_name()
|
||||||
|
self.check_equal(self.post("/api/repos", json={"Name": repo_name, "DefaultDistribution": "wheezy"}).status_code, 201)
|
||||||
|
|
||||||
|
d = self.random_name()
|
||||||
|
self.check_equal(self.upload("/api/files/" + d,
|
||||||
|
"libboost-program-options-dev_1.49.0.1_i386.deb", "pyspi_0.6.1-1.3.dsc",
|
||||||
|
"pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz",
|
||||||
|
"pyspi-0.6.1-1.3.stripped.dsc").status_code, 200)
|
||||||
|
|
||||||
|
self.check_equal(self.post("/api/repos/" + repo_name + "/file/" + d).status_code, 200)
|
||||||
|
|
||||||
|
prefix = self.random_name()
|
||||||
|
resp = self.post("/api/publish/" + prefix + "/repos",
|
||||||
|
json={
|
||||||
|
"Sources": [{"Name": repo_name}],
|
||||||
|
"Signing": DefaultSigningOptions,
|
||||||
|
})
|
||||||
|
self.check_equal(resp.status_code, 200)
|
||||||
|
self.check_equal(resp.json(), {
|
||||||
|
'Architectures': ['i386', 'source'],
|
||||||
|
'Distribution': 'wheezy',
|
||||||
|
'Label': '',
|
||||||
|
'Origin': '',
|
||||||
|
'Prefix': prefix,
|
||||||
|
'SourceKind': 'local',
|
||||||
|
'Sources': [{'Component': 'main', 'Name': repo_name}],
|
||||||
|
'Storage': ''})
|
||||||
|
|
||||||
|
|
||||||
|
class PublishSnapshotAPITestRepo(APITest):
|
||||||
|
"""
|
||||||
|
POST /publish/:prefix/snapshot
|
||||||
|
|
||||||
|
XXX: test me when snapshot API becomes available
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user